diff --git a/.config/nextest.toml b/.config/nextest.toml index 889c3165..21562281 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -30,10 +30,26 @@ filter = 'binary(differential) & test(/^gate::/)' test-group = 'corpus-compilers' slow-timeout = { period = "120s", terminate-after = 64 } +# The tier-equivalence suite is the same shape at five effect-tier positions: +# the sharded relation lowers and interprets a quarter of the corpus five +# times, and the generated sweep adds a fuzz corpus on top. Same allowance as +# the optimizer relation above. +[[profile.default.overrides]] +filter = 'binary(differential) & test(/^tier_gate::/)' +test-group = 'corpus-compilers' +slow-timeout = { period = "120s", terminate-after = 64 } + +# The typed-spine gates compile every corpus program twice, once with Core lint on +# and once without, then diff the two Core dumps, and the group below pins them to +# one thread. Measured at 1142s standalone in the debug profile on an M-series +# laptop against the previous 1920s cap: headroom on a quiet machine, none left +# once the rest of the suite competes for the cores, which is where it timed out. +# Same rule as every other entry here: the timeout catches a hang, it does not +# bound honest work. [[profile.default.overrides]] filter = 'binary(differential) & test(/^typed_spine::/)' test-group = 'corpus-compilers' -slow-timeout = { period = "120s", terminate-after = 16 } +slow-timeout = { period = "120s", terminate-after = 32 } [[profile.default.overrides]] filter = 'binary(compiler) & test(core_lint_clean_on_corpus)' @@ -86,15 +102,31 @@ filter = 'binary(snapshots)' test-group = 'snapshot-goldens' slow-timeout = { period = "120s", terminate-after = 16 } +# Several of this binary's modules drive the whole compiler over a corpus: the +# receipt lane encodes every corpus file with the authority parser and re-encodes +# it with the Prism-implemented one, and the bootstrap lanes compile the +# compiler's own sources. That is minutes of honest work in the debug profile, and +# the receipt lane crossed the shared default the first time the rest of the suite +# competed for the machine while it ran. Same rule as every other entry here: the +# timeout catches a hang, it does not bound honest work. +[[profile.default.overrides]] +filter = 'binary(tooling)' +slow-timeout = { period = "120s", terminate-after = 16 } + [[profile.ci.overrides]] filter = 'binary(differential) & test(/^gate::/)' test-group = 'corpus-compilers' slow-timeout = { period = "120s", terminate-after = 64 } +[[profile.ci.overrides]] +filter = 'binary(differential) & test(/^tier_gate::/)' +test-group = 'corpus-compilers' +slow-timeout = { period = "120s", terminate-after = 64 } + [[profile.ci.overrides]] filter = 'binary(differential) & test(/^typed_spine::/)' test-group = 'corpus-compilers' -slow-timeout = { period = "120s", terminate-after = 16 } +slow-timeout = { period = "120s", terminate-after = 32 } [[profile.ci.overrides]] filter = 'binary(compiler) & test(core_lint_clean_on_corpus)' @@ -124,3 +156,7 @@ slow-timeout = { period = "120s", terminate-after = 16 } filter = 'binary(snapshots)' test-group = 'snapshot-goldens' slow-timeout = { period = "120s", terminate-after = 16 } + +[[profile.ci.overrides]] +filter = 'binary(tooling)' +slow-timeout = { period = "120s", terminate-after = 16 } diff --git a/.github/actions/setup-llvm/action.yml b/.github/actions/setup-llvm/action.yml index 036bbfcc..8cfcc31d 100644 --- a/.github/actions/setup-llvm/action.yml +++ b/.github/actions/setup-llvm/action.yml @@ -8,8 +8,8 @@ inputs: packages: description: > Extra apt packages beyond the base set (llvm-22-dev, libpolly-22-dev, - clang-22), space-separated. e.g. "clang-tidy-22" or "libmlir-22-dev - mlir-22-tools". + clang-22, lld-22), space-separated. e.g. "clang-tidy-22" or + "libmlir-22-dev mlir-22-tools". required: false default: "" @@ -35,13 +35,22 @@ runs: - name: Install LLVM 22 (cached) uses: awalsh128/cache-apt-pkgs-action@v1 with: - packages: llvm-22-dev libpolly-22-dev clang-22 ${{ inputs.packages }} - version: llvm22 + packages: llvm-22-dev libpolly-22-dev clang-22 lld-22 ${{ inputs.packages }} + # Bumped whenever the base package set changes, so a cache entry resolved + # from the old set can never be restored in place of the new one. + version: llvm22-lld # mold is a far faster linker than ld; the compiler links libLLVM on every # build, so linking is a real chunk of wall-clock. make-default routes rustc's # cc linking through mold with no RUSTFLAGS, so it adds no sccache-key churn. # (Pinned to a tag like the action above; pin to a SHA to harden further.) + # + # make-default replaces the host `ld`, so it would otherwise also link the + # binaries prism itself emits. That link runs under ThinLTO, where the + # linker is the code generator and decides final layout, and its bytes are + # pinned by oracles. lld-22 above is what keeps it on the same LLVM the rest + # of the pipeline uses: prism selects it explicitly rather than inheriting + # whichever linker happens to own `ld` on the host. - name: Install mold linker uses: rui314/setup-mold@v1 with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c4292c3..289c08c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,7 +89,10 @@ jobs: "$bin" --version # docs/src cross-references anchors and {{#include}}s by hand; mdbook never - # validates either, so a rename or a moved example silently rots a link. + # validates either, so a rename or a moved example silently rots a link. Every + # other committed Markdown file is checked too, and those are the worse case: + # nothing builds them at all, so an index of the tree can point at files that + # were renamed away a release ago and still read as current. doc-links: name: Doc links resolve runs-on: ubuntu-latest @@ -158,8 +161,11 @@ jobs: - name: default command: cargo check --all-targets wasm: false + # Lint the browser feature on its real target. A native + # `--all-features` run combines the wasm and MLIR/native backends into + # a configuration no shipped compiler uses. - name: wasm - command: cargo check --no-default-features --features wasm --target wasm32-unknown-unknown + command: cargo clippy --no-default-features --features wasm --target wasm32-unknown-unknown --lib -- -D warnings wasm: true - name: mlir command: cargo check --features mlir @@ -384,6 +390,11 @@ jobs: # skipped, so a stale cache can never mask a regression. PRISM_GATE_CACHE: "1" PRISM_GATE_FINGERPRINT: source + # Names the solver installed below, so a workflow that stops installing it + # fails the solver-accept gate instead of silently skipping it. cvc5 is not + # listed: it is not packaged for this runner, so the two-solver agreement + # gate stays opt-in for a developer who has one. + PRISM_REQUIRE_SOLVERS: z3 steps: - uses: actions/checkout@v7 @@ -395,6 +406,12 @@ jobs: with: prefix-key: "v1-libm" + # The certificate subsystem mints `prism-smt-certificate-v1` receipts by + # discharging obligations through an external solver, so without one + # installed its end-to-end path has no CI evidence behind it. + - name: Install z3 + run: sudo apt-get update && sudo apt-get install -y z3 + # Per-shard gate-verdict cache: each partition owns a distinct key so the # parallel runs do not overwrite each other's snapshots. - name: Cache gate verdicts @@ -436,7 +453,7 @@ jobs: strategy: fail-fast: false matrix: - oracle: [fusion, tier, optimizer, typed-spine] + oracle: [fusion, tier, optimizer, tier-equiv, typed-spine] shard: [0, 1, 2, 3] env: LLVM_SYS_221_PREFIX: /usr/lib/llvm-22 @@ -488,6 +505,10 @@ jobs: if: ${{ matrix.oracle == 'optimizer' }} run: cargo nextest run --profile ci --test differential -E 'test(optimizer_configurations_have_identical_observation_traces)' + - name: Tier-equivalence relation (shard ${{ matrix.shard }} of 4) + if: ${{ matrix.oracle == 'tier-equiv' }} + run: cargo nextest run --profile ci --test differential -E 'test(tier_configurations_have_identical_observation_traces)' + - name: Typed-spine relations (shard ${{ matrix.shard }} of 4) if: ${{ matrix.oracle == 'typed-spine' }} run: cargo nextest run --profile ci --test differential -E 'test(typed_erasure_preserves_corpus_core_identity) | test(full_front_crosses_typed_newtype_prefix_across_corpus)' @@ -528,8 +549,8 @@ jobs: uses: mozilla-actions/sccache-action@v0.0.10 # The viewer page reads a generated artifact (`prism index`), so the native - # binary is needed here, and with it LLVM — the same install the other - # compiling jobs share. + # binary is needed here, and with it LLVM. The other compiling jobs use the + # same installation. - name: Install LLVM 22 uses: ./.github/actions/setup-llvm @@ -628,7 +649,7 @@ jobs: steps: - uses: actions/checkout@v7 - # Toolchain pinned via rust-toolchain.toml (single source of truth). + # Toolchain pinned by rust-toolchain.toml. - name: Setup Rust toolchain run: rustup show @@ -655,7 +676,7 @@ jobs: # progress, effect-safety) and the differential `Certificates` whose `rfl` # proofs pin model output to live `prism run` output, and builds the `oracle` # executable the differential harness runs. The toolchain is read from - # models/lean-toolchain (single source of truth); no mathlib, so the build is + # models/lean-toolchain; no mathlib, so the build is # self-contained and fast. This is the one formal-methods gate that used to be # able to silently break (Lean was outside CI). - name: Build Lean model @@ -713,7 +734,7 @@ jobs: steps: - uses: actions/checkout@v7 - # Toolchain pinned via rust-toolchain.toml (single source of truth). + # Toolchain pinned by rust-toolchain.toml. - name: Setup Rust toolchain run: rustup show @@ -754,6 +775,13 @@ jobs: MLIR_SYS_220_PREFIX: /usr/lib/llvm-22 TABLEGEN_220_PREFIX: /usr/lib/llvm-22 + - name: Test shared ABI plans with mlir + run: cargo test -p prism-native --features mlir codegen::abi::tests + env: + LLVM_SYS_221_PREFIX: /usr/lib/llvm-22 + MLIR_SYS_220_PREFIX: /usr/lib/llvm-22 + TABLEGEN_220_PREFIX: /usr/lib/llvm-22 + - name: Run mlir parity test run: PATH=/usr/lib/llvm-22/bin:$PATH cargo nextest run --profile ci --features mlir --test native mlir_matches_interpreter env: diff --git a/CHANGELOG.md b/CHANGELOG.md index db0da62a..dc31cf4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # Changelog +## 0.20.0 + +- Representations: one layout query decides storage, ABI, zero words, and ownership for every consumer. +- Compiler contracts: phase doors, inference solves, and prompt facts now fail closed. +- Typed Core: construction, verification, and reference-count insertion gained stages and named failures. +- Reference counting: borrowed parameters lower as true loans, so read-only traversals retain nothing. +- Memory: fixed two leaks, a shadowed match arm binder and a range expression in the syntax encoder. +- Strings and bytes: slicing is now a constant-time window rather than a copy. +- Strings: literals are static cells, allocated once and shared by every mention. +- Performance: layout and parse throughput improved, the adversarially nested case tenfold. +- Builds: native links select lld where the toolchain has it, so an artifact's bytes follow only from its inputs. +- Lint: a new rule catches the recursive codepoint scan that turns traversals quadratic. +- Effect tiers: shapes that used to fall to the free monad now hold the evidence tier. +- Effect rows: callbacks stored in data keep exact witnesses, and pure ones stay direct. +- Effect rows: an element row with no local witness compiles by subsumption with a warning. +- Effect lowering: a refused rewrite names the declaration and the form it stopped at. +- Handlers: one without a return arm now answers with its body's type, not an over-general scheme. +- Arenas: promotion preserves shared structure instead of copying it exponentially. +- Records: sum variants may reuse field names at different types. +- Records: partial sum reads and constructor spreads now fail during checking. +- Patterns: record arms can use `C { .. }` to ignore every field. +- Checker bootstrap: the Prism shadow now covers effect rows, parameterized effects, and generalization. +- Self-hosted parser: the whole corpus now parses to identical trees, with no known divergences. +- Parser handover: froze the oracle terms and recorded a receipt for the shadow's corpus run. +- Tier accountability: a gate diffs every lowering rung against the interpreter, corpus and fuzzed. +- Code index: hover facts no longer perturb definition identity. +- Store: every layer is sharded and bounds itself by entry count and bytes. +- Store: collection reports what it reclaimed, and a runaway layer is retired wholesale. + ## 0.19.0 - Lint: added `prism lint`, twelve Prism-written house rules with coded suppressions, JSON output, and an advisory mode. @@ -320,7 +349,7 @@ - Instance coherence: each `(class, type-head)` has one canonical instance that implicit resolution always selects, so ambiguity-at-use is gone. A lone instance is canonical by default; when several share a head one is named with a top-level `canonical Class(Head) = name`, and two undesignated instances are a coherence error at definition (caret plus designation hint). `f(args, using name)` stays the visible override, and a `newtype` is the way to a different default. No Core, runtime, or backend change, so the parity oracle is byte-identical. `canonical` is reserved. - A right-associative power operator `^` (tighter than `*`), the method of a new `Pow` class: `2 ^ 10` is bignum-correct `Int`, `2.0 ^ 10.0` is `Float`, mixed `Int ^ Float` a type error. The prior integer `pow` is now `int_pow`. - Imperative loops (`while`/`loop`) lower to a tail-recursive prelude driver (constant stack, no per-iteration allocation), an unconditional `loop` to the bottom-typed `forever`. `break`/`continue`/`return` compile to non-resumable performs of internal, fully-handled effects, so none surfaces in a function's row, and a loop installs a handler only for the keyword it uses. The prelude's old `while` is now `repeat_while`. -- Principal effect-row inference: a lambda delimits its effects onto its own arrow row, the arrow is covariant in that row (a pure function fits any effectful context via row subsumption), and the call-graph set pass is dropped as a row seed so the inferred row is the single source of truth. Builtins carry their effect row on the type, so inference attributes `IO`/`Exn`/`Fail` directly; rows display in canonical name-sorted order; and definitions are inferred in dependency-SCC order so a forward reference sees a generalized type. +- Principal effect-row inference: a lambda delimits its effects onto its own arrow row, the arrow is covariant in that row (a pure function fits any effectful context via row subsumption), and the call-graph set pass is dropped as a row seed so the inferred row alone determines the effect set. Builtins carry their effect row on the type, so inference attributes `IO`/`Exn`/`Fail` directly; rows display in canonical name-sorted order; and definitions are inferred in dependency-SCC order so a forward reference sees a generalized type. - `mask` over the sole handler of an effect now leaves the operation genuinely unhandled (the label stays in the row) instead of inferring it pure and hitting an effect-reconciliation ICE at lowering. - Two source warnings (the prelude is exempt): an unused local binding and a name shadowing one in scope (a leading `_` and a consuming rebind `let s = f(s)` are exempt). Annotations are name-checked uniformly across parameters, returns, constraints, and rows: an undeclared effect or constructor is a hard error, and an annotation broader than the inferred row warns. - Standard library split into on-demand modules under `lib/std` (`Data.Char`/`List`/`Map`/`Maybe`/`Result`/`Set`/`String`), shrinking the always-loaded prelude; `Set` gains `set_union`/`set_intersection`/`set_difference`, and a project may replace the built-in prelude via `[package] prelude` in `prism.toml`. diff --git a/Cargo.lock b/Cargo.lock index d4e0b286..69807fb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1114,7 +1114,7 @@ dependencies = [ [[package]] name = "prism" -version = "0.19.0" +version = "0.20.0" dependencies = [ "anstyle", "ariadne", @@ -1151,14 +1151,14 @@ dependencies = [ [[package]] name = "prism-common" -version = "0.19.0" +version = "0.20.0" dependencies = [ "serde", ] [[package]] name = "prism-core" -version = "0.19.0" +version = "0.20.0" dependencies = [ "blake3", "im", @@ -1175,7 +1175,7 @@ dependencies = [ [[package]] name = "prism-lineage" -version = "0.19.0" +version = "0.20.0" dependencies = [ "blake3", "prism-common", @@ -1188,7 +1188,7 @@ dependencies = [ [[package]] name = "prism-native" -version = "0.19.0" +version = "0.20.0" dependencies = [ "blake3", "cc", @@ -1202,7 +1202,7 @@ dependencies = [ [[package]] name = "prism-store" -version = "0.19.0" +version = "0.20.0" dependencies = [ "blake3", "prism-common", @@ -1213,7 +1213,7 @@ dependencies = [ [[package]] name = "prism-syntax" -version = "0.19.0" +version = "0.20.0" dependencies = [ "anstyle", "ariadne", @@ -1646,18 +1646,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 0e048531..a83fb22b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "prism" -version = "0.19.0" +version = "0.20.0" authors = ["Stephen Diehl "] categories = ["compilers"] default-run = "prism" @@ -190,7 +190,7 @@ exclude = ["tools/prismup"] resolver = "2" [workspace.package] -version = "0.19.0" +version = "0.20.0" authors = ["Stephen Diehl "] edition = "2021" license = "MIT" diff --git a/Dockerfile b/Dockerfile index 56899940..37dc9a42 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# Bundles LLVM 22 + clang so prism runs with no host toolchain. +# Bundles LLVM 22 + clang + lld so prism runs with no host toolchain. FROM rust:1-bookworm AS builder RUN set -eux; \ apt-get update; \ @@ -25,7 +25,7 @@ RUN set -eux; \ echo "deb http://apt.llvm.org/bookworm/ llvm-toolchain-bookworm-22 main" \ > /etc/apt/sources.list.d/llvm.list; \ apt-get update; \ - apt-get install -y --no-install-recommends llvm-22 clang-22; \ + apt-get install -y --no-install-recommends llvm-22 clang-22 lld-22; \ apt-get purge -y wget gnupg; apt-get autoremove -y; \ rm -rf /var/lib/apt/lists/* COPY --from=builder /src/target/release/prism /usr/bin/prism diff --git a/README.md b/README.md index cba47e47..a7fb4be5 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Prism is an impure functional programming language whose type system tracks side effects. Effect sets are inferred, extensible rows that compose through ordinary calls instead of monads, and they track observability rather than implementation: an effect handled inside a function vanishes from its type, so internally effectful code can still be analyzed, optimized, and reused as pure code. The language also has rank-N polymorphism, typeclasses, derived lenses and optic paths, fusing streams, deterministic reference counting, and native codegen through LLVM, with an optional textual MLIR backend for parity checking. -The compiler is built around deterministic simulation testing at the language level. Prism programs elaborate to a strict A-normal-form call-by-push-value core, definitions and packages are content-addressed by hash, project builds can explain their lineage, and suspended continuations carry the code identity they may resume against. The compiler also builds to WebAssembly, so the playground and gallery run in the browser; the interpreter is a CEK machine modeled in Lean and serves as the differential oracle every native backend must match byte-for-byte. +The compiler is built around deterministic simulation testing at the language level. Prism programs elaborate to a strict A-normal-form call-by-push-value core, definitions and packages are content-addressed by hash, project builds can explain their lineage, and suspended continuations carry the code identity they may resume against. The compiler also builds to WebAssembly, so the playground and gallery run in the browser; the interpreter is a CEK machine modeled in Lean and is the differential oracle every native backend must match byte-for-byte. Try it in the browser at the [Prism playground](https://sdiehl.github.io/prism/play/). @@ -38,7 +38,7 @@ curl -fsSL https://apt.llvm.org/llvm.sh | sudo bash -s 22 # Debian/Ubuntu Then install prism (macOS Apple Silicon; Linux x86_64, aarch64): ```shell -curl --proto '=https' --tlsv1.2 -fsSL https://sdiehl.github.io/prism/install.sh | PRISM_VERSION=v0.19.0 sh +curl --proto '=https' --tlsv1.2 -fsSL https://sdiehl.github.io/prism/install.sh | PRISM_VERSION=v0.20.0 sh ``` The installer verifies each release asset's SHA-256 against the release manifest (and its build-provenance attestation when an authenticated `gh` is available) before unpacking. It installs `prismup`, the Prism toolchain manager, to `~/.prismup/bin` and uses it to install the compiler; installed versions live under `~/.prismup/prism/` and `prismup -s ` switches between them. No sudo. If Nix is present it uses the flake instead, with hashes verified by the Nix store. @@ -54,14 +54,14 @@ docker run ghcr.io/sdiehl/prism --version # Debian / Ubuntu (LLVM repository, then package) curl -fsSL https://apt.llvm.org/llvm.sh | sudo bash -s 22 -curl -fLO https://github.com/sdiehl/prism/releases/download/v0.19.0/prism_0.19.0_amd64.deb && sudo apt install ./prism_0.19.0_amd64.deb +curl -fLO https://github.com/sdiehl/prism/releases/download/v0.20.0/prism_0.20.0_amd64.deb && sudo apt install ./prism_0.20.0_amd64.deb # Fedora / RHEL -sudo dnf install https://github.com/sdiehl/prism/releases/download/v0.19.0/prism-0.19.0-1.x86_64.rpm +sudo dnf install https://github.com/sdiehl/prism/releases/download/v0.20.0/prism-0.20.0-1.x86_64.rpm # Arch (prebuilt package or local PKGBUILD) -sudo pacman -U https://github.com/sdiehl/prism/releases/download/v0.19.0/prism-0.19.0-1-x86_64.pkg.tar.zst -curl -fLO https://github.com/sdiehl/prism/releases/download/v0.19.0/PKGBUILD && makepkg -si +sudo pacman -U https://github.com/sdiehl/prism/releases/download/v0.20.0/prism-0.20.0-1-x86_64.pkg.tar.zst +curl -fLO https://github.com/sdiehl/prism/releases/download/v0.20.0/PKGBUILD && makepkg -si ``` ### From Source @@ -76,7 +76,7 @@ LLVM_SYS_221_PREFIX="$(brew --prefix llvm@22)" \ cargo install --git https://github.com/sdiehl/prism # Debian/Ubuntu (after enabling apt.llvm.org as above) -sudo apt install llvm-22-dev libpolly-22-dev clang-22 +sudo apt install llvm-22-dev libpolly-22-dev clang-22 lld-22 LLVM_SYS_221_PREFIX=/usr/lib/llvm-22 \ PRISM_CC=/usr/lib/llvm-22/bin/clang \ cargo install --git https://github.com/sdiehl/prism diff --git a/bin/prism.rs b/bin/prism.rs index 85b88eb9..452221cb 100644 --- a/bin/prism.rs +++ b/bin/prism.rs @@ -448,8 +448,9 @@ enum Cmd { enum BootstrapCmd { /// Compare the T1 Prism checker with authoritative Rust facts Check { - /// A `.pr` file or project to shadow-check - file: PathBuf, + /// One or more `.pr` files or projects to shadow-check + #[arg(required = true)] + files: Vec, /// Emit the parity and coverage report as JSON #[arg(long)] json: bool, @@ -659,6 +660,16 @@ enum StoreCmd { /// The `.pr` file whose locked `stable` families to derive or verify file: PathBuf, }, + /// Garbage-collect cache entries the store's own query and index layers no + /// longer reference + Gc { + /// Only remove entries older than this many days + #[arg(long, default_value_t = 30)] + days: u64, + /// Report what would be removed without deleting anything + #[arg(long)] + dry_run: bool, + }, } /// Semantic patches: inspect, judge, stage, and atomically commit. @@ -856,6 +867,11 @@ fn main() -> ExitCode { // config) stay silent. if cfg.flags.time_compile { cfg.timing = Some(prism::TimingSink::new()); + // The work counters ride the same flag rather than a second knob: they + // answer "how much" where the row's wall time answers "how long", and a + // reader who asked for one wants both. Counting is process-wide, so it is + // switched on here, once, beside the sink that reports it. + prism::core::work::enable(); } let result = match (cli.cmd, cli.file) { (Some(cmd), _) => dispatch(cmd, &cfg), @@ -1081,8 +1097,8 @@ fn dispatch(cmd: Cmd, cfg: &prism::Config) -> CmdResult { limit, json, } => cli::type_query::synth_cmd(file.as_deref(), &at_hole, depth, limit, json, cfg), - Cmd::Bootstrap(BootstrapCmd::Check { file, json }) => { - cli::bootstrap::check_cmd(&file, json, cfg) + Cmd::Bootstrap(BootstrapCmd::Check { files, json }) => { + cli::bootstrap::check_cmd(&files, json, cfg) } Cmd::Explain { code } => cli::explain::explain_cmd(&code), Cmd::Test { @@ -1275,6 +1291,7 @@ fn dispatch_store(store: StoreCmd, cfg: &prism::Config) -> CmdResult { StoreCmd::Query { kind, name, file } => cli::store::query(&kind, &name, &file, cfg), StoreCmd::Wire { accept, file } => cli::store::wire(accept, &file), StoreCmd::Lock { accept, file } => cli::store::lock(accept, &file, cfg), + StoreCmd::Gc { days, dry_run } => cli::store::gc(days, dry_run, cfg), } } diff --git a/crates/prism-common/src/binary.rs b/crates/prism-common/src/binary.rs index 785bc853..150adf26 100644 --- a/crates/prism-common/src/binary.rs +++ b/crates/prism-common/src/binary.rs @@ -120,9 +120,9 @@ pub fn put_indices(out: &mut Vec, idxs: &[u32]) { /// The wire number of a table entry. /// -/// Its position in an ordered table that is the single source of truth for the -/// numbering. Each codec keeps its own tables (op families, node tags) and -/// numbers them through here, so encode and decode cannot drift. +/// Its position in the codec's canonical ordered table. Each codec keeps its own +/// tables (op families, node tags) and numbers them through here, so encode and +/// decode cannot drift. /// /// # Panics /// When `entry` is absent from `table`: a codec bug on trusted input (a new diff --git a/crates/prism-common/src/fresh.rs b/crates/prism-common/src/fresh.rs index 7068f4e2..c17f7abd 100644 --- a/crates/prism-common/src/fresh.rs +++ b/crates/prism-common/src/fresh.rs @@ -24,4 +24,12 @@ impl Fresh { self.0 += 1; n } + + /// The raw counter, for helpers that draw ids through a `&mut u32`. + /// + /// Sharing the counter rather than a second supply is what keeps those ids + /// disjoint from the ones [`Self::bump`] hands out under the same prefix. + pub const fn counter(&mut self) -> &mut u32 { + &mut self.0 + } } diff --git a/crates/prism-core/src/core/builtins.rs b/crates/prism-core/src/core/builtins.rs index 3a572833..12b101c5 100644 --- a/crates/prism-core/src/core/builtins.rs +++ b/crates/prism-core/src/core/builtins.rs @@ -1,4 +1,4 @@ -//! Single source of truth for builtins: surface name, arity, lowering kind. +//! Canonical builtin table: surface name, arity, and lowering kind. //! //! Consumed by the elaborator (arity map, head dispatch), the REPL session, //! and the backend preludes (runtime declares). @@ -447,6 +447,7 @@ builtins! { StrEq "str_eq" "StrEq" 2 RETAG surface 2 Str "(String, String) -> Bool"; StrCmp "str_cmp" "StrCmp" 3 RETAG surface 2 Str "(String, String) -> Int"; Substring "substring" "Substring" 4 IDX12 surface 3 Str "(String, Int, Int) -> String"; + StrSlice "prim_str_slice" "StrSlice" 137 IDX12 surface 3 Str "(String, Int, Int) -> String"; CharAt "char_at" "CharAt" 5 IDX1_RETAG surface 2 Str "(String, Int) -> Int"; ShowChar "show_char" "ShowChar" 6 IMM0 surface 1 Str "(Char) -> String"; Blake3 "blake3" "Blake3" 7 RAW surface 1 Str "(String) -> String"; @@ -527,6 +528,7 @@ builtins! { BufSet "buf_set" "BufSet" 82 IDX12 surface 3 Str "(Buf, Int, Int) -> Buf"; BufPush "buf_push" "BufPush" 83 IDX1 surface 2 Str "(Buf, Int) -> Buf"; BufSlice "buf_slice" "BufSlice" 84 IDX12 surface 3 Str "(Buf, Int, Int) -> Buf"; + BufAppendStr "buf_append_str" "BufAppendStr" 138 RAW surface 2 Str "(Buf, String) -> Buf"; BufCat "buf_cat" "BufCat" 85 RAW surface 2 Str "(Buf, Buf) -> Buf"; BufEq "buf_eq" "BufEq" 86 RETAG surface 2 Str "(Buf, Buf) -> Bool"; BufCmp "buf_cmp" "BufCmp" 87 RETAG surface 2 Str "(Buf, Buf) -> Int"; @@ -569,7 +571,7 @@ builtins! { // matches the pinned registry (`simd_builtin_tags_match_registry` guards it). // The interpreter defines the bit-exact semantics; native lowers each to its // `prism_simd_*` runtime symbol over a two-word vector cell. A `splat` unboxes - // its scalar (`F0`/`RAW`); `extract` untags its lane index (`IDX1`); the + // its scalar (`F0`/`RAW`). `extract` untags its lane index (`IDX1`). The // lane-wise binary ops thread two vector cells raw. SimdFSplat "simd_fsplat" "SimdFSplat" 108 F0 surface 1 Str "(Float) -> F64x2"; SimdFExtract "simd_fextract" "SimdFExtract" 109 IDX1 surface 2 Str "(F64x2, Int) -> Float"; @@ -757,6 +759,7 @@ mod tag_tests { (Builtin::StrEq, "StrEq"), (Builtin::StrCmp, "StrCmp"), (Builtin::Substring, "Substring"), + (Builtin::StrSlice, "StrSlice"), (Builtin::CharAt, "CharAt"), (Builtin::ShowChar, "ShowChar"), (Builtin::Blake3, "Blake3"), @@ -837,6 +840,7 @@ mod tag_tests { (Builtin::BufSet, "BufSet"), (Builtin::BufPush, "BufPush"), (Builtin::BufSlice, "BufSlice"), + (Builtin::BufAppendStr, "BufAppendStr"), (Builtin::BufCat, "BufCat"), (Builtin::BufEq, "BufEq"), (Builtin::BufCmp, "BufCmp"), diff --git a/crates/prism-core/src/core/cbpv.rs b/crates/prism-core/src/core/cbpv.rs index 6a783538..35bb37af 100644 --- a/crates/prism-core/src/core/cbpv.rs +++ b/crates/prism-core/src/core/cbpv.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use super::builtins::{Builtin, FloatOp}; use super::effect_shape::{classify_resume, ResumeUse}; use super::traverse::Visit; +use crate::types::Type; use prism_common::sym::Sym; use prism_syntax::ast::BinOp; use prism_syntax::names::ENTRY_POINT; @@ -148,6 +149,29 @@ pub enum Value { UnboxedRecord(Vec<(Sym, Self)>), } +impl Value { + /// The canonical source type of a scalar literal, or `None` for a value + /// with no literal encoding (variables, structures, thunks). + /// + /// Consumers pass it to the representation authority + /// (`types::scalar_plan`) rather than deciding an encoding here. Mirrors + /// `TypedValueKind::literal_scalar_type`; erasure has already dropped the + /// representation-preserving wrapper nodes, so there is no recursion. + #[must_use] + pub const fn literal_scalar_type(&self) -> Option { + match self { + Self::Int(_) => Some(Type::Int), + Self::I64(_) => Some(Type::I64), + Self::U64(_) => Some(Type::U64), + Self::Float(_) => Some(Type::Float), + Self::Bool(_) => Some(Type::Bool), + Self::Unit => Some(Type::Unit), + Self::Str(_) => Some(Type::Str), + _ => None, + } + } +} + // The numeric lane a unary negation runs in. Unary minus elaborates to a // genuine `Comp::Neg` node, never a `0 - x` desugar, for two reasons: float // negation must flip the sign bit and preserve signed zero (a real `fneg`, not @@ -513,14 +537,26 @@ pub struct Core { /// /// The field is private so the stage claim is unforgeable: a value of this type /// was produced by the pipeline, never assembled from public parts. +/// +/// ```compile_fail +/// use prism_core::core::{Core, ElaboratedCore}; +/// let _ = ElaboratedCore::new(Core { fns: Vec::new() }); +/// ``` +/// +/// A caller also cannot mutate a checked wrapper after validation: +/// +/// ```compile_fail +/// use prism_core::core::{Core, ElaboratedCore}; +/// let mut checked = ElaboratedCore::validate(Core { fns: Vec::new() }).unwrap(); +/// checked.core_mut().fns.clear(); +/// ``` #[derive(Clone, Debug)] pub struct ElaboratedCore(Core); impl ElaboratedCore { - /// Wrap the pipeline's own elaboration output. Crate-internal on purpose: - /// the stage claim is the constructor's, and only the pipeline may make it. + /// Wrap output that the checked public transition already validated. #[must_use] - pub const fn new(core: Core) -> Self { + pub(super) const fn new(core: Core) -> Self { Self(core) } @@ -529,11 +565,6 @@ impl ElaboratedCore { pub fn into_core(self) -> Core { self.0 } - - /// Mutable access for the pipeline's own late adjustments (konst injection). - pub const fn core_mut(&mut self) -> &mut Core { - &mut self.0 - } } /// Post-effect-lowering whole-program Core. The effect nodes are gone; the @@ -546,14 +577,18 @@ impl ElaboratedCore { /// `core::opt::lint`), the checked public constructor whose assurance is /// structural stage validation, lint-grade, and explicitly not typed /// verification. +/// +/// ```compile_fail +/// use prism_core::core::{Core, LoweredCore}; +/// let _ = LoweredCore::new(Core { fns: Vec::new() }); +/// ``` #[derive(Clone, Debug)] pub struct LoweredCore(Core); impl LoweredCore { - /// Wrap the pipeline's own verified lowering output. Crate-internal on - /// purpose; the public checked path is `validate_structural`. + /// Wrap output that the checked public transition already validated. #[must_use] - pub const fn new(core: Core) -> Self { + pub(super) const fn new(core: Core) -> Self { Self(core) } } @@ -710,7 +745,7 @@ mod tag_tests { // these strings, so a variant rename that also touched the tag method would // silently move every affected definition's hash; freezing the spelling here // turns that into a test failure instead. The method's own `match` is - // exhaustive, so a new variant cannot ship without a tag; this checks that tag. + // exhaustive, so a new variant cannot ship without a tag. This checks the tag. fn frozen(table: &[(T, &str)], tag: impl Fn(T) -> &'static str) { let mut seen = BTreeSet::new(); for &(variant, spelling) in table { diff --git a/crates/prism-core/src/core/fbip/balance.rs b/crates/prism-core/src/core/fbip/balance.rs index 040dfd01..ece9041f 100644 --- a/crates/prism-core/src/core/fbip/balance.rs +++ b/crates/prism-core/src/core/fbip/balance.rs @@ -6,6 +6,7 @@ use super::super::cbpv::{Comp, Core, Value}; use super::super::fv::{comp as freev, pat_vars}; #[cfg(debug_assertions)] use super::super::traverse::Visit; +use super::imbalance::{Imbalance, TokenFault}; use super::{borrow_mask, borrowed_at, borrowed_call_vars, count_val, Set, Sigs}; // Independent verifier: simulate the inserted ops as a linear token machine. Each @@ -14,8 +15,9 @@ use super::{borrow_mask, borrowed_at, borrowed_call_vars, count_val, Set, Sigs}; // reach zero before leaving scope, and the two sides of a branch must agree. A // pass that under-dups, over-drops, or unbalances a branch fails here. /// # Errors -/// Fails when refcount tokens are unbalanced. -pub fn balanced(core: &Core, sigs: &Sigs) -> Result<(), String> { +/// The token fault that broke the simulation, attributed to the declaration it +/// was found in. +pub fn balanced(core: &Core, sigs: &Sigs) -> Result<(), Imbalance> { // This runs only on effect-lowered Core (the compiled pipeline). `sim` treats // a stray `Handle`/`Do`/`Mask` as a no-op, which would silently mask an RC // imbalance in its clauses, so assert lowering really ran first rather than @@ -43,10 +45,17 @@ pub fn balanced(core: &Core, sigs: &Sigs) -> Result<(), String> { .filter(|(index, _)| borrowed_at(mask, *index)) .map(|(_, param)| *param) .collect(); - sim(&f.body, &mut env, sigs, &external).map_err(|e| format!("{}: {e}", f.name))?; + sim(&f.body, &mut env, sigs, &external) + .map_err(|fault| Imbalance::in_function(fault, f.name))?; for (v, n) in &env { if v.as_str() != "_" && *n != 0 { - return Err(format!("{}: {v} ends with {n} tokens", f.name)); + return Err(Imbalance::in_function( + TokenFault::ScopeExit { + var: *v, + tokens: *n, + }, + f.name, + )); } } } @@ -71,7 +80,7 @@ fn effect_free(c: &Comp) -> bool { s.0 } -fn use_val(v: &Value, env: &mut BTreeMap, sigs: &Sigs) -> Result<(), String> { +fn use_val(v: &Value, env: &mut BTreeMap, sigs: &Sigs) -> Result<(), TokenFault> { let mut counts = BTreeMap::new(); count_val(v, &mut counts); for (x, k) in counts { @@ -84,7 +93,7 @@ fn use_val(v: &Value, env: &mut BTreeMap, sigs: &Sigs) -> Result<(), S // never reaches them. Re-run the simulation on each thunk body: lambda params // start owned (one token), captures start borrowed (zero, so a use without a // preceding dup drives below zero and fails). Catches an under-dup'd capture. -fn verify_thunks(v: &Value, sigs: &Sigs) -> Result<(), String> { +fn verify_thunks(v: &Value, sigs: &Sigs) -> Result<(), TokenFault> { match v { Value::Thunk(c) => { let (params, body): (Set, &Comp) = match &**c { @@ -100,7 +109,10 @@ fn verify_thunks(v: &Value, sigs: &Sigs) -> Result<(), String> { sim(body, &mut env, sigs, &external)?; for (x, n) in &env { if x.as_str() != "_" && *n != 0 { - return Err(format!("thunk capture {x} ends with {n} tokens")); + return Err(TokenFault::ThunkCapture { + var: *x, + tokens: *n, + }); } } Ok(()) @@ -113,19 +125,24 @@ fn verify_thunks(v: &Value, sigs: &Sigs) -> Result<(), String> { } } -fn consume(x: Sym, k: i64, env: &mut BTreeMap) -> Result<(), String> { +fn consume(x: Sym, k: i64, env: &mut BTreeMap) -> Result<(), TokenFault> { if x.as_str() == "_" { return Ok(()); } let e = env.entry(x).or_insert(0); *e -= k; if *e < 0 { - return Err(format!("{x} consumed below zero")); + return Err(TokenFault::BelowZero { var: x }); } Ok(()) } -fn sim(c: &Comp, env: &mut BTreeMap, sigs: &Sigs, external: &Set) -> Result<(), String> { +fn sim( + c: &Comp, + env: &mut BTreeMap, + sigs: &Sigs, + external: &Set, +) -> Result<(), TokenFault> { match c { Comp::Dup(Value::Var(x)) => { *env.entry(*x).or_insert(0) += 1; @@ -133,6 +150,20 @@ fn sim(c: &Comp, env: &mut BTreeMap, sigs: &Sigs, external: &Set) -> R } Comp::Drop(Value::Var(x)) => consume(*x, 1, env), Comp::Bind(m, x, n) => { + // A bind whose right side merely renames a loaned reference extends + // the loan instead of spending a token: the binder reads the same + // cell the loan keeps live, so it starts with no token of its own + // and joins the loaned set for the rest of the chain. The inserter + // applies the identical syntactic rule, so a loaned rename never + // carries a retain for this simulation to spend. + if let Comp::Return(Value::Var(v)) = &**m { + if external.contains(v) && x.as_str() != "_" { + env.insert(*x, 0); + let mut nested_external = external.clone(); + nested_external.insert(*x); + return sim(n, env, sigs, &nested_external); + } + } sim(m, env, sigs, external)?; if x.as_str() != "_" { env.insert(*x, 1); @@ -148,7 +179,12 @@ fn sim(c: &Comp, env: &mut BTreeMap, sigs: &Sigs, external: &Set) -> R sim(e, &mut ee, sigs, external)?; merge(&et, &ee, env) } - Comp::Case(_, arms) => { + Comp::Case(scrut, arms) => { + // Matching on a loaned cell reads it without spending a token: the + // cell drops nothing in the arms, and the pattern binders are loans + // on its fields (kept live by the same owner that keeps the parent + // live), so they join the loaned set instead of shadowing it. + let loaned_scrutinee = matches!(scrut, Value::Var(v) if external.contains(v)); let mut merged: Option> = None; for (p, body) in arms { let mut ea = env.clone(); @@ -159,12 +195,16 @@ fn sim(c: &Comp, env: &mut BTreeMap, sigs: &Sigs, external: &Set) -> R } let mut arm_external = external.clone(); for var in &pv { - arm_external.remove(var); + if loaned_scrutinee { + arm_external.insert(*var); + } else { + arm_external.remove(var); + } } sim(body, &mut ea, sigs, &arm_external)?; for v in &pv { if ea.get(v).copied().unwrap_or(0) != 0 { - return Err(format!("field {v} leaks in arm")); + return Err(TokenFault::ArmLeak { field: *v }); } ea.remove(v); } @@ -232,9 +272,7 @@ fn sim(c: &Comp, env: &mut BTreeMap, sigs: &Sigs, external: &Set) -> R let spent = i64::try_from(consumed.get(&var).copied().unwrap_or(0)).unwrap_or(i64::MAX); if !external.contains(&var) && live - spent < 1 { - return Err(format!( - "borrowed call argument {var} is not live through call to {g}" - )); + return Err(TokenFault::BorrowNotLive { var, callee: *g }); } } for (i, a) in args.iter().enumerate() { @@ -269,7 +307,7 @@ fn merge( a: &BTreeMap, b: &BTreeMap, out: &mut BTreeMap, -) -> Result<(), String> { +) -> Result<(), TokenFault> { let keys: Set = a.keys().chain(b.keys()).copied().collect(); for k in keys { let (va, vb) = ( @@ -277,7 +315,11 @@ fn merge( b.get(&k).copied().unwrap_or(0), ); if va != vb { - return Err(format!("branch disagreement on {k}: {va} vs {vb}")); + return Err(TokenFault::BranchDisagreement { + var: k, + left: va, + right: vb, + }); } out.insert(k, va); } @@ -307,6 +349,45 @@ mod tests { assert_eq!(balanced(&core, &sigs), Ok(())); } + #[test] + fn borrowed_static_str_needs_no_retained_token() { + let observe = Sym::new("observe_str"); + let core = Core { + fns: vec![CoreFn { + name: Sym::new("caller_str"), + params: Vec::new(), + body: Comp::Call(observe, vec![Value::Str("static".into())]), + dict_arity: 0, + }], + }; + let sigs = iter::once((observe, vec![true])).collect(); + + assert_eq!(balanced(&core, &sigs), Ok(())); + } + + #[test] + fn borrowed_boxed_scalar_must_be_let_bound() { + let observe = Sym::new("observe_float"); + let core = Core { + fns: vec![CoreFn { + name: Sym::new("caller_float"), + params: Vec::new(), + body: Comp::Call(observe, vec![Value::Float(2.5)]), + dict_arity: 0, + }], + }; + let sigs = iter::once((observe, vec![true])).collect(); + + let error = balanced(&core, &sigs).expect_err("a boxed literal loan needs an owner"); + assert_eq!( + error.fault, + TokenFault::BorrowedArgNotBound { + callee: observe, + arg: Box::new(Value::Float(2.5)), + } + ); + } + #[test] fn borrowed_heap_temporary_must_be_let_bound() { let observe = Sym::new("observe_heap"); @@ -324,7 +405,13 @@ mod tests { let sigs = iter::once((observe, vec![true])).collect(); let error = balanced(&core, &sigs).expect_err("heap loan needs a caller-owned token"); - assert!(error.contains("not a let-bound variable"), "{error}"); + assert_eq!( + error.fault, + TokenFault::BorrowedArgNotBound { + callee: observe, + arg: Box::new(Value::Ctor("Box".into(), 0, vec![Value::Int(42)])), + } + ); } #[test] @@ -347,7 +434,13 @@ mod tests { let sigs = iter::once((observe, vec![true])).collect(); let error = balanced(&core, &sigs).expect_err("pre-call drop must end the loan"); - assert!(error.contains("borrowed call argument retained is not live")); + assert_eq!( + error.fault, + TokenFault::BorrowNotLive { + var: retained, + callee: observe, + } + ); } #[test] @@ -376,6 +469,12 @@ mod tests { .collect(); let error = balanced(&core, &sigs).expect_err("inner binder owns its own loan"); - assert!(error.contains("borrowed call argument borrowed is not live")); + assert_eq!( + error.fault, + TokenFault::BorrowNotLive { + var: borrowed, + callee: observe, + } + ); } } diff --git a/crates/prism-core/src/core/fbip/borrow.rs b/crates/prism-core/src/core/fbip/borrow.rs new file mode 100644 index 00000000..c50b714a --- /dev/null +++ b/crates/prism-core/src/core/fbip/borrow.rs @@ -0,0 +1,963 @@ +//! Borrow inference: which parameters a provably pure function only loans. +//! +//! The reference-count discipline makes borrowing any parameter of a provably +//! pure function sound (the callee dups before each consuming use and never +//! drops the loan; the caller retains ownership across the call), so this pass +//! decides profit, not safety: a parameter is inferred borrowed only when every +//! occurrence in the body is a genuine read. A read is a bare variable in a +//! scrutinee, test, or primitive operand position, or a bare variable passed to +//! a borrowed position of another call. Anything that stores the value in a +//! structure, captures it in a thunk or closure, hands it to an owned call +//! position, or reaches an effect or post-RC node disqualifies it. So does a +//! match that could recycle the parameter's cell: a loan frees nothing, so +//! when an arm destructs the value and allocates a cell the freed one could +//! service, the reuse is worth more than the saved retain and release pair +//! and the parameter stays owned. +//! +//! Elaboration rebinds every use through a value-headed let (`return r to t; +//! case t of ...`), so occurrences are tracked through aliases: a bind whose +//! head returns a bare loaned variable makes its binder carry the same loan, +//! and only a genuinely escaping occurrence, the function result, a structure +//! field, a capture, an owned argument, consumes the underlying parameter. +//! +//! The callee's body is not the only constraint: every call site must be able +//! to cover the loan with a named, retained token, so a position that any +//! caller feeds a structured temporary is forced back to owned before the +//! body walk begins. +//! +//! Nor is profit the only constraint. A loan is discharged by the frame that +//! made the call, after the call returns, and a call the backend turns into a +//! loop keeps no such frame: deferring the release past the call is exactly +//! what stops the site from being a tail call. Borrowing there would trade a +//! retain and release pair for one stack frame per iteration, which is not a +//! cost but a change of behavior, since a loop that ran in constant stack +//! begins to exhaust it on a large enough input. So a position is forced back +//! to owned when a loop-eligible call site passes it a value the calling frame +//! owns. Passing on a loan the frame itself holds stays free, because that +//! loan's owner sits further up the stack and outlives the whole loop. +//! +//! Recursion is resolved as a greatest fixpoint: every candidate starts +//! borrowed and iteration removes parameters with a consuming occurrence under +//! the current assumption, so a self-recursive read-only walk keeps its loan. +//! Declared `borrow` annotations are the source contract and are never shrunk, +//! only extended. The result is a pure function of the checked program, which +//! is why it stays out of definition identity: like a lowering tier, the +//! setting must be unobservable in program behavior. + +use std::collections::{BTreeMap, BTreeSet}; + +use prism_common::sym::Sym; +use prism_syntax::kw; + +use super::super::cbpv::{Comp, Core, CorePat, Value}; +use super::super::fv::{comp as freev, comp_without, pat_vars}; +use super::super::tailrec::{loops_as_tail_call, reassoc, trmc_shape}; +use super::super::traverse::Visit; +use super::{borrowed_at, scalar_without_cell, Set, Sigs}; + +// The loans in scope during a body walk: each name that currently carries a +// loan, mapped to the parameter whose loan it carries. Parameters map to +// themselves; a let alias maps to the parameter it renames. +type Loans = BTreeMap; + +// The names the frame holds without owning: exactly the set reference-count +// insertion calls borrowed, being a loaned parameter, a field projected out of +// a loaned scrutinee, and any let alias of one. None of them carries a release +// this frame has to place, which is the one thing a looping call site needs to +// know about its arguments. +// +// Deliberately not folded into `Loans`, which answers a different question: +// which parameter an occurrence would consume. A field of a loaned scrutinee is +// unowned, but escaping it retains the field on its own and leaves the parameter +// it came from untouched, so it must not map back to that parameter. +type Unowned = BTreeSet; + +// Positions a loop-eligible call site forces back to owned, keyed by callee. +// Unlike the poison set, which names parameters of the function being walked, +// these are decided at a call site and land on someone else's signature. +type Vetoes = BTreeMap>; + +/// Extend `declared` with inferred masks for the provably pure functions. +/// +/// `pure_fns` names the declarations whose principal body effect row solved +/// empty and closed; only those are candidates. Leading dictionary parameters +/// stay owned. Entries whose mask is all-owned are omitted, matching +/// `borrow_sigs`, so consumers keep their absent-means-owned default. +#[must_use] +pub fn infer_borrow_sigs(core: &Core, pure_fns: &Set, declared: &Sigs) -> Sigs { + let mut candidates: Sigs = core + .fns + .iter() + .filter(|f| pure_fns.contains(&f.name) && f.params.len() > f.dict_arity) + .map(|f| { + let mask = (0..f.params.len()).map(|i| i >= f.dict_arity).collect(); + (f.name, mask) + }) + .collect(); + // Which call sites the backend can loop is only visible once nested bind + // heads are flattened, so the walk reads normalized bodies. This is the + // same normalization the tail-recursion analysis and the emitter share, and + // it is a pure rewrite, so taking it once here serves every round. + let bodies: Vec = core.fns.iter().map(|f| reassoc(&f.body)).collect(); + let arity: BTreeMap = core.fns.iter().map(|f| (f.name, f.params.len())).collect(); + // A borrowed-position argument must reach reference-count insertion as a + // bare variable or a literal immediate: the caller covers the loan with a + // named, retained token it drops after the call, and a structured + // temporary has no such name. Any call site passing a structured value at + // a candidate position forces that position back to owned up front, so the + // final map can never route a program into the balance checker's + // borrowed-argument refusal. Shapes never change during the fixpoint, so + // one pre-pass suffices. + let mut shapes = CallShapes { + candidates: &mut candidates, + }; + for body in &bodies { + shapes.visit_comp(body); + } + loop { + let assumed = merged(declared, &candidates); + let mut changed = false; + let mut vetoes = Vetoes::new(); + for (f, body) in core.fns.iter().zip(&bodies) { + // Loans are read from the assumed map rather than the candidate + // one, so a declared borrow the inference never proposed still + // counts as a loan the frame may pass on for free. + let mask = assumed.get(&f.name).map(Vec::as_slice); + let in_scope: Loans = f + .params + .iter() + .enumerate() + .filter(|(i, _)| borrowed_at(mask, *i)) + .map(|(_, p)| (*p, *p)) + .collect(); + let unowned: Unowned = in_scope.keys().copied().collect(); + let mut walk = Walk { + assumed: &assumed, + arity: &arity, + frame: Some(f.name), + frame_arity: f.params.len(), + poisoned: Set::new(), + vetoes: &mut vetoes, + }; + walk.comp(body, &in_scope, &unowned, true); + let poisoned = walk.poisoned; + let Some(mask) = candidates.get(&f.name) else { + continue; + }; + if !poisoned.is_empty() { + let next: Vec = f + .params + .iter() + .enumerate() + .map(|(i, p)| mask.get(i).copied().unwrap_or(false) && !poisoned.contains(p)) + .collect(); + // Only a mask that actually lost a position counts as progress. + // Loans are read from the assumed map, which keeps a declared + // borrow in scope forever, so a consuming occurrence of one + // would otherwise report the same poison every round and the + // fixpoint would never settle. + if next != *mask { + candidates.insert(f.name, next); + changed = true; + } + } + } + // A lambda becomes a frame of its own, so its tail calls loop or grow + // the stack on exactly the same terms and it owes the same veto. + let mut lams = LamFrames { + assumed: &assumed, + arity: &arity, + vetoes: &mut vetoes, + }; + for body in &bodies { + lams.visit_comp(body); + } + // Applied after the round rather than during it, because a veto lands + // on a signature some other function is being walked against: taking it + // mid-round would make the result depend on declaration order. Every + // veto only clears a position, so the map descends and the loop ends. + for (callee, positions) in vetoes { + let Some(mask) = candidates.get_mut(&callee) else { + continue; + }; + for index in positions { + if let Some(slot) = mask.get_mut(index) { + if *slot { + *slot = false; + changed = true; + } + } + } + } + if !changed { + break; + } + } + merged(declared, &candidates) + .into_iter() + .filter(|(_, mask)| mask.iter().any(|b| *b)) + .collect() +} + +// Clears candidate positions whose call sites pass anything other than a bare +// variable or a literal immediate. The `Visit` descent reaches every call, +// including those inside thunk and closure bodies. +struct CallShapes<'a> { + candidates: &'a mut Sigs, +} + +impl Visit for CallShapes<'_> { + fn visit_comp(&mut self, c: &Comp) { + if let Comp::Call(callee, args) = c { + if let Some(mask) = self.candidates.get_mut(callee) { + for (i, arg) in args.iter().enumerate() { + if !matches!(arg, Value::Var(_)) && !scalar_without_cell(arg) { + if let Some(slot) = mask.get_mut(i) { + *slot = false; + } + } + } + } + } + self.descend_comp(c); + } +} + +// Walks every lambda as the separate frame the backend lifts it into, so a tail +// call inside one is judged against the frame it actually gets rather than the +// declaration it was written in. Only vetoes come out: a suspension owns its +// captures, so inside the body the captures are loans this frame never releases +// and the parameters are owned, which is the partition reference-count +// insertion already uses, and no loan of the enclosing frame reaches in to be +// poisoned. The lifted frame takes the captures ahead of the parameters, so its +// width is both together, read through the same free-variable query the closure +// layout is built from. +struct LamFrames<'a> { + assumed: &'a Sigs, + arity: &'a BTreeMap, + vetoes: &'a mut Vetoes, +} + +impl Visit for LamFrames<'_> { + fn visit_comp(&mut self, c: &Comp) { + if let Comp::Lam(params, body) = c { + let captures = comp_without(body, params); + let mut walk = Walk { + assumed: self.assumed, + arity: self.arity, + frame: None, + frame_arity: captures.len() + params.len(), + poisoned: Set::new(), + vetoes: self.vetoes, + }; + walk.comp(body, &Loans::new(), &captures, true); + } + self.descend_comp(c); + } +} + +// Elementwise OR of two mask maps; a missing or short entry reads as owned. +fn merged(declared: &Sigs, inferred: &Sigs) -> Sigs { + let mut out = declared.clone(); + for (name, mask) in inferred { + let entry = out.entry(*name).or_default(); + if entry.len() < mask.len() { + entry.resize(mask.len(), false); + } + for (i, b) in mask.iter().enumerate() { + entry[i] = entry[i] || *b; + } + } + out +} + +// Every loan-carrying name free in `comp` is consumed wholesale. +fn poison_free(comp: &Comp, loans: &Loans, out: &mut Set) { + let fv = freev(comp); + for (name, root) in loans { + if fv.contains(name) { + out.insert(*root); + } + } +} + +// Every loan-carrying name occurring anywhere inside `v` is consumed or +// escapes there. +fn poison_value(v: &Value, loans: &Loans, out: &mut Set) { + match v { + Value::Var(x) => { + if let Some(root) = loans.get(x) { + out.insert(*root); + } + } + // A thunk cell captures its free names, which outlives the loan. + Value::Thunk(body) => poison_free(body, loans, out), + Value::Ctor(_, _, fields) | Value::Tuple(fields) | Value::UnboxedTuple(fields) => { + for field in fields { + poison_value(field, loans, out); + } + } + Value::UnboxedRecord(fields) => { + for (_, field) in fields { + poison_value(field, loans, out); + } + } + Value::Int(_) + | Value::I64(_) + | Value::U64(_) + | Value::Float(_) + | Value::Bool(_) + | Value::Unit + | Value::Str(_) => {} + } +} + +// A value in a read position: a bare variable is a loan; any structured value +// allocates or captures, which consumes whatever candidates it contains. +fn read_value(v: &Value, loans: &Loans, out: &mut Set) { + if !matches!(v, Value::Var(_)) { + poison_value(v, loans, out); + } +} + +// One frame's body walk, which is a declaration or one lambda the backend lifts +// out of it. `poisoned` accumulates parameters of the declaration being walked; +// `vetoes` accumulates positions of whatever functions it calls, so it outlives +// the walk and is threaded in by reference. `frame` is the declaration's name, +// absent for a lambda, which has none to recurse through. +struct Walk<'a> { + assumed: &'a Sigs, + arity: &'a BTreeMap, + frame: Option, + frame_arity: usize, + poisoned: Set, + vetoes: &'a mut Vetoes, +} + +impl Walk<'_> { + // Whether a tail-position call to `callee` with `args` arguments is one the + // backend reuses this frame for rather than pushing a new one. + fn loops_here(&self, callee: Sym, args: usize) -> bool { + self.arity + .get(&callee) + .is_some_and(|callee_arity| loops_as_tail_call(args, *callee_arity, self.frame_arity)) + } + + // Force back to owned every borrowed position this looping call site hands + // a value the frame owns. The frame is about to be reused, so there is + // nowhere left to release it: the release would have to follow the call, + // which is precisely what stops the call from reusing the frame. A loan the + // frame is passing on costs nothing, since the owner that will release it + // sits further up the stack and outlives every iteration, and a scalar that + // owns no cell has no release to place at all. + fn veto_loop_args(&mut self, callee: Sym, args: &[Value], unowned: &Unowned) { + let mask = self.assumed.get(&callee).map(Vec::as_slice); + for (index, arg) in args.iter().enumerate() { + if !borrowed_at(mask, index) { + continue; + } + let free_of_release = match arg { + Value::Var(name) => unowned.contains(name), + other => scalar_without_cell(other), + }; + if !free_of_release { + self.vetoes.entry(callee).or_default().insert(index); + } + } + } + + fn comp(&mut self, comp: &Comp, loans: &Loans, unowned: &Unowned, tail: bool) { + match comp { + // A returned value leaves the function, so the caller's retained + // reference alone cannot cover it. This arm only sees tail + // positions: a bind head's `Return` is a let and is handled below. + Comp::Return(v) | Comp::Error(v) | Comp::Force(v) => { + poison_value(v, loans, &mut self.poisoned); + } + Comp::Bind(head, binder, rest) => { + // A self-call whose continuation feeds one constructor field or + // one addend is a tail modulo constructor step, which the + // backend also turns into a loop. A release deferred into that + // continuation takes the shape apart, so the site answers to the + // same rule as a bare tail call. + if tail { + if let Comp::Call(callee, args) = head.as_ref() { + if self.frame == Some(*callee) + && self.loops_here(*callee, args.len()) + && trmc_shape(rest, binder.as_str()).is_some() + { + self.veto_loop_args(*callee, args, unowned); + } + } + } + // The binder shadows anything of the same name the frame was + // already tracking, before the head can rename onto it. + let mut rest_loans = loans.clone(); + let mut rest_unowned = unowned.clone(); + rest_loans.remove(binder); + rest_unowned.remove(binder); + // A value head is a let, not a function result: naming a + // tracked variable renames it onto the binder, and a structured + // head stores whatever candidates it contains. + if let Comp::Return(v) = head.as_ref() { + match v { + Value::Var(x) => { + if let Some(root) = loans.get(x) { + rest_loans.insert(*binder, *root); + } + if unowned.contains(x) { + rest_unowned.insert(*binder); + } + } + other => poison_value(other, loans, &mut self.poisoned), + } + } else { + self.comp(head, loans, unowned, false); + } + self.comp(rest, &rest_loans, &rest_unowned, tail); + } + Comp::App(callee, args) => { + poison_free(callee, loans, &mut self.poisoned); + for arg in args { + poison_value(arg, loans, &mut self.poisoned); + } + } + Comp::If(cond, yes, no) => { + read_value(cond, loans, &mut self.poisoned); + self.comp(yes, loans, unowned, tail); + self.comp(no, loans, unowned, tail); + } + Comp::Prim(_, lhs, rhs) => { + read_value(lhs, loans, &mut self.poisoned); + read_value(rhs, loans, &mut self.poisoned); + } + Comp::FloatBuiltin(_, operand) | Comp::Neg(_, operand) => { + read_value(operand, loans, &mut self.poisoned); + } + Comp::Call(callee, args) => { + if tail && self.loops_here(*callee, args.len()) { + self.veto_loop_args(*callee, args, unowned); + } + let mask = self.assumed.get(callee).map(Vec::as_slice); + for (index, arg) in args.iter().enumerate() { + if borrowed_at(mask, index) { + read_value(arg, loans, &mut self.poisoned); + } else { + poison_value(arg, loans, &mut self.poisoned); + } + } + } + Comp::Io(_, args) | Comp::Do(_, args) | Comp::StrBuiltin(_, args) => { + for arg in args { + poison_value(arg, loans, &mut self.poisoned); + } + } + Comp::Case(scrutinee, arms) => { + read_value(scrutinee, loans, &mut self.poisoned); + let loaned_root = match scrutinee { + Value::Var(x) => loans.get(x).copied(), + _ => None, + }; + let scrutinee_unowned = matches!(scrutinee, Value::Var(x) if unowned.contains(x)); + for (pattern, body) in arms { + // A loan starves reuse: a borrowed scrutinee frees no cell + // in its arms, so the reuse pass finds no token to spend. + // When an arm could pair the freed cell with a fitting + // allocation, the freed-cell reuse is worth more than the + // saved retain and release pair, so the parameter stays + // owned. + if let (Some(root), Some(cap)) = (loaned_root, reuse_seed_arity(pattern)) { + if fitting_alloc(body, cap) { + self.poisoned.insert(root); + } + } + let mut binders = Set::new(); + pat_vars(pattern, &mut binders); + let mut arm_loans = loans.clone(); + let mut arm_unowned = unowned.clone(); + for binder in &binders { + arm_loans.remove(binder); + arm_unowned.remove(binder); + } + // Reference-count insertion projects the fields of a loaned + // scrutinee as loans themselves and retains nothing for + // them, so a field passed on carries no release either. The + // field is deliberately not mapped back to the parameter it + // came from: escaping a field retains that field on its own + // and leaves the parameter untouched. + if scrutinee_unowned { + arm_unowned.extend(binders.iter().copied()); + } + self.comp(body, &arm_loans, &arm_unowned, tail); + } + } + Comp::UnboxedProject(v, _) => read_value(v, loans, &mut self.poisoned), + // A closure capture outlives the call frame the loan is scoped to, + // and effect machinery or post-RC nodes never appear in a provably + // pure body before lowering: in both cases every candidate the node + // touches is conservatively consumed wholesale rather than reasoned + // about, so the bodies need no further walk. + Comp::Lam(_, _) + | Comp::Handle { .. } + | Comp::Mask(_, _) + | Comp::WithReuse { .. } + | Comp::Reuse(_, _) + | Comp::Dup(_) + | Comp::Drop(_) + | Comp::InitAt(_, _) + | Comp::RefNew(_) + | Comp::RefGet(_) + | Comp::RefSet(_, _) => poison_free(comp, loans, &mut self.poisoned), + } + } +} + +// The patterns whose match frees a reusable cell, mirroring `reuse_arm`: a +// destructing constructor or tuple seeds a token sized by its field count, and +// the wired nullable frees no cell so it never seeds one. The two predicates +// below must stay exactly as permissive as the reuse pass, or inference loans +// away cells the pass could have recycled. +fn reuse_seed_arity(pattern: &CorePat) -> Option { + match pattern { + CorePat::Ctor(name, _) if kw::is_or_null_ctor(name.as_str()) => None, + CorePat::Ctor(_, fields) | CorePat::Tuple(fields) => Some(fields.len()), + _ => None, + } +} + +// Whether the body holds an allocation a freed cell of `cap` slots could +// service, over the same spine `consume_alloc` walks: bind chains, branches, +// and inner reuse scopes, never thunk or handler bodies. Reaching any fitting +// allocation is enough; where the freeing drop would land depends on liveness +// the inference does not model, so this over-approximates toward owned. +fn fitting_alloc(comp: &Comp, cap: usize) -> bool { + match comp { + Comp::Return(v @ (Value::Ctor(..) | Value::Tuple(..))) => { + let arity = match v { + Value::Ctor(_, _, fields) | Value::Tuple(fields) => fields.len(), + _ => 0, + }; + arity <= cap + && !matches!(v, Value::Ctor(name, ..) if kw::is_or_null_ctor(name.as_str())) + } + Comp::Bind(m, _, n) => fitting_alloc(m, cap) || fitting_alloc(n, cap), + Comp::If(_, yes, no) => fitting_alloc(yes, cap) || fitting_alloc(no, cap), + Comp::Case(_, arms) => arms.iter().any(|(_, body)| fitting_alloc(body, cap)), + Comp::WithReuse { body, .. } => fitting_alloc(body, cap), + _ => false, + } +} + +#[cfg(test)] +mod tests { + use super::super::super::cbpv::{CoreFn, CorePat}; + use super::*; + + fn s(name: &str) -> Sym { + name.into() + } + + fn f(name: &str, params: &[&str], body: Comp) -> CoreFn { + CoreFn { + name: s(name), + params: params.iter().map(|p| s(p)).collect(), + dict_arity: 0, + body, + } + } + + fn core(fns: Vec) -> Core { + Core { fns } + } + + fn pure_set(names: &[&str]) -> Set { + names.iter().map(|n| s(n)).collect() + } + + fn mask<'a>(sigs: &'a Sigs, name: &str) -> Option<&'a Vec> { + sigs.get(&s(name)) + } + + #[test] + fn scrutinee_only_param_is_borrowed() { + let body = Comp::Case( + Value::Var(s("xs")), + vec![ + (CorePat::Ctor(s("Nil"), vec![]), Comp::Return(Value::Int(0))), + ( + CorePat::Ctor(s("Cons"), vec![None, Some(s("t"))]), + Comp::Call(s("len"), vec![Value::Var(s("t"))]), + ), + ], + ); + let sigs = infer_borrow_sigs( + &core(vec![f("len", &["xs"], body)]), + &pure_set(&["len"]), + &Sigs::new(), + ); + assert_eq!(mask(&sigs, "len"), Some(&vec![true])); + } + + #[test] + fn looping_call_that_hands_over_an_owned_value_forces_owned() { + // `driver` ends in a same-arity tail call, which the backend lowers as + // a loop reusing the frame. The first argument is a value the frame + // owns, so its release would have to follow the call, and a call with + // work after it is not a tail call: the loop would become one frame per + // iteration. The second argument is the loan the frame is passing on, + // whose owner sits further up the stack, so that position is untouched. + let reader = f( + "reader", + &["a", "b"], + Comp::Case( + Value::Var(s("a")), + vec![( + CorePat::Wild, + Comp::Case( + Value::Var(s("b")), + vec![(CorePat::Wild, Comp::Return(Value::Int(0)))], + ), + )], + ), + ); + let driver = f( + "driver", + &["n", "xs"], + Comp::Bind( + Box::new(Comp::Return(Value::Ctor( + s("Box"), + 0, + vec![Value::Var(s("n"))], + ))), + s("boxed"), + Box::new(Comp::Call( + s("reader"), + vec![Value::Var(s("boxed")), Value::Var(s("xs"))], + )), + ), + ); + let sigs = infer_borrow_sigs( + &core(vec![reader, driver]), + &pure_set(&["reader", "driver"]), + &Sigs::new(), + ); + assert_eq!(mask(&sigs, "reader"), Some(&vec![false, true])); + } + + #[test] + fn looping_call_that_hands_over_an_immediate_keeps_the_loan() { + // An immediate owns no cell, so it carries no release for the reused + // frame to place and the position keeps its loan. + let body = Comp::Case( + Value::Var(s("xs")), + vec![ + ( + CorePat::Ctor(s("Nil"), vec![]), + Comp::Case( + Value::Var(s("k")), + vec![(CorePat::Wild, Comp::Return(Value::Int(0)))], + ), + ), + ( + CorePat::Ctor(s("Cons"), vec![None, Some(s("t"))]), + Comp::Call(s("seek"), vec![Value::Int(1), Value::Var(s("t"))]), + ), + ], + ); + let sigs = infer_borrow_sigs( + &core(vec![f("seek", &["k", "xs"], body)]), + &pure_set(&["seek"]), + &Sigs::new(), + ); + assert_eq!(mask(&sigs, "seek"), Some(&vec![true, true])); + } + + #[test] + fn arm_that_could_recycle_the_cell_forces_owned() { + // A setter's shape: destruct the value, allocate one the freed cell + // could service. A loan would starve the reuse pass of its token, so + // the parameter stays owned. + let body = Comp::Case( + Value::Var(s("p")), + vec![( + CorePat::Ctor(s("P"), vec![Some(s("a")), Some(s("b"))]), + Comp::Return(Value::Ctor( + s("P"), + 0, + vec![Value::Var(s("v")), Value::Var(s("b"))], + )), + )], + ); + let sigs = infer_borrow_sigs( + &core(vec![f("with_x", &["p", "v"], body)]), + &pure_set(&["with_x"]), + &Sigs::new(), + ); + assert_eq!(mask(&sigs, "with_x"), None); + } + + #[test] + fn arm_allocation_too_wide_for_the_cell_keeps_the_loan() { + // The only allocation cannot fit in the freed one-slot cell, so no + // reuse is lost and the scrutinee-only read still earns its loan. + let body = Comp::Case( + Value::Var(s("p")), + vec![( + CorePat::Ctor(s("Wrap"), vec![Some(s("a"))]), + Comp::Return(Value::Tuple(vec![Value::Var(s("a")), Value::Var(s("a"))])), + )], + ); + let sigs = infer_borrow_sigs( + &core(vec![f("widen", &["p"], body)]), + &pure_set(&["widen"]), + &Sigs::new(), + ); + assert_eq!(mask(&sigs, "widen"), Some(&vec![true])); + } + + #[test] + fn structured_argument_at_a_call_site_forces_owned() { + let body = Comp::Case( + Value::Var(s("xs")), + vec![(CorePat::Wild, Comp::Return(Value::Int(0)))], + ); + // The body alone would earn the loan, but a caller passes a freshly + // built value directly at the position, which no retained token names. + let caller = f( + "caller", + &["n"], + Comp::Call( + s("reader"), + vec![Value::Ctor(s("Node"), 0, vec![Value::Var(s("n"))])], + ), + ); + let sigs = infer_borrow_sigs( + &core(vec![f("reader", &["xs"], body), caller]), + &pure_set(&["reader"]), + &Sigs::new(), + ); + assert_eq!(mask(&sigs, "reader"), None); + } + + #[test] + fn self_recursive_loan_survives_the_fixpoint() { + let body = Comp::If( + Value::Var(s("stop")), + Box::new(Comp::Return(Value::Int(0))), + Box::new(Comp::Call( + s("go"), + vec![Value::Var(s("stop")), Value::Var(s("xs"))], + )), + ); + let sigs = infer_borrow_sigs( + &core(vec![f("go", &["stop", "xs"], body)]), + &pure_set(&["go"]), + &Sigs::new(), + ); + assert_eq!(mask(&sigs, "go"), Some(&vec![true, true])); + } + + #[test] + fn returned_and_stored_params_stay_owned() { + let ret = f("ret", &["x"], Comp::Return(Value::Var(s("x")))); + let stored = f( + "stored", + &["x"], + Comp::Return(Value::Ctor(s("Box"), 0, vec![Value::Var(s("x"))])), + ); + let sigs = infer_borrow_sigs( + &core(vec![ret, stored]), + &pure_set(&["ret", "stored"]), + &Sigs::new(), + ); + assert_eq!(mask(&sigs, "ret"), None); + assert_eq!(mask(&sigs, "stored"), None); + } + + #[test] + fn owned_position_poison_propagates_through_the_call_graph() { + // `sink` consumes its parameter (returns it), so `relay` passing its + // own parameter to `sink` must lose the loan one iteration later. + let sink = f("sink", &["x"], Comp::Return(Value::Var(s("x")))); + let relay = f( + "relay", + &["x"], + Comp::Call(s("sink"), vec![Value::Var(s("x"))]), + ); + let sigs = infer_borrow_sigs( + &core(vec![relay, sink]), + &pure_set(&["sink", "relay"]), + &Sigs::new(), + ); + assert_eq!(mask(&sigs, "sink"), None); + assert_eq!(mask(&sigs, "relay"), None); + } + + #[test] + fn borrowed_position_call_keeps_the_loan() { + let reader = f( + "reader", + &["xs"], + Comp::Case( + Value::Var(s("xs")), + vec![(CorePat::Wild, Comp::Return(Value::Int(1)))], + ), + ); + let relay = f( + "relay", + &["xs"], + Comp::Call(s("reader"), vec![Value::Var(s("xs"))]), + ); + let sigs = infer_borrow_sigs( + &core(vec![relay, reader]), + &pure_set(&["reader", "relay"]), + &Sigs::new(), + ); + assert_eq!(mask(&sigs, "reader"), Some(&vec![true])); + assert_eq!(mask(&sigs, "relay"), Some(&vec![true])); + } + + #[test] + fn declared_masks_are_never_shrunk() { + // The body consumes `x`, but the declared annotation is the source + // contract, so the final mask keeps it borrowed. + let declared: Sigs = std::iter::once((s("keep"), vec![true])).collect(); + let keep = f("keep", &["x"], Comp::Return(Value::Var(s("x")))); + let sigs = infer_borrow_sigs(&core(vec![keep]), &pure_set(&["keep"]), &declared); + assert_eq!(mask(&sigs, "keep"), Some(&vec![true])); + } + + #[test] + fn shadowed_rebinding_does_not_poison_the_param() { + // The escaping `x` is the Bind's own binder, not the parameter. + let body = Comp::Bind( + Box::new(Comp::Case( + Value::Var(s("x")), + vec![(CorePat::Wild, Comp::Return(Value::Int(0)))], + )), + s("x"), + Box::new(Comp::Return(Value::Var(s("x")))), + ); + let sigs = infer_borrow_sigs( + &core(vec![f("shadow", &["x"], body)]), + &pure_set(&["shadow"]), + &Sigs::new(), + ); + assert_eq!(mask(&sigs, "shadow"), Some(&vec![true])); + } + + #[test] + fn let_alias_carries_the_loan_to_its_uses() { + // Elaboration names every use through a temp: `return r to t; + // case t of ...` must read exactly like `case r of ...`. + let body = Comp::Bind( + Box::new(Comp::Return(Value::Var(s("r")))), + s("t"), + Box::new(Comp::Case( + Value::Var(s("t")), + vec![(CorePat::Wild, Comp::Return(Value::Int(0)))], + )), + ); + let sigs = infer_borrow_sigs( + &core(vec![f("reader", &["r"], body)]), + &pure_set(&["reader"]), + &Sigs::new(), + ); + assert_eq!(mask(&sigs, "reader"), Some(&vec![true])); + } + + #[test] + fn returning_an_alias_escapes_the_param() { + // The alias is the function result, so the loan cannot cover it. + let body = Comp::Bind( + Box::new(Comp::Return(Value::Var(s("r")))), + s("t"), + Box::new(Comp::Return(Value::Var(s("t")))), + ); + let sigs = infer_borrow_sigs( + &core(vec![f("ident", &["r"], body)]), + &pure_set(&["ident"]), + &Sigs::new(), + ); + assert_eq!(mask(&sigs, "ident"), None); + } + + #[test] + fn thunk_capture_and_dictionaries_stay_owned() { + let capture = f( + "capture", + &["x"], + Comp::Return(Value::Thunk(Box::new(Comp::Return(Value::Var(s("x")))))), + ); + let mut with_dict = f( + "method", + &["d", "x"], + Comp::Case( + Value::Var(s("x")), + vec![(CorePat::Wild, Comp::Return(Value::Int(0)))], + ), + ); + with_dict.dict_arity = 1; + let sigs = infer_borrow_sigs( + &core(vec![capture, with_dict]), + &pure_set(&["capture", "method"]), + &Sigs::new(), + ); + assert_eq!(mask(&sigs, "capture"), None); + assert_eq!(mask(&sigs, "method"), Some(&vec![false, true])); + } + + #[test] + fn a_boxed_literal_call_site_declines_the_loan_but_a_static_str_keeps_it() { + // A `Float` literal boxes a fresh cell at codegen, so a call site + // passing one inline denies the borrowed shape; a `Str` literal names + // a static cell and covers the loan like a tagged immediate. + let float_reader = f( + "float_reader", + &["x"], + Comp::Case( + Value::Var(s("x")), + vec![(CorePat::Wild, Comp::Return(Value::Int(1)))], + ), + ); + let float_site = f( + "float_site", + &[], + Comp::Call(s("float_reader"), vec![Value::Float(2.5)]), + ); + let str_reader = f( + "str_reader", + &["x"], + Comp::Case( + Value::Var(s("x")), + vec![(CorePat::Wild, Comp::Return(Value::Int(1)))], + ), + ); + let str_site = f( + "str_site", + &[], + Comp::Call(s("str_reader"), vec![Value::Str("static".into())]), + ); + let sigs = infer_borrow_sigs( + &core(vec![float_reader, float_site, str_reader, str_site]), + &pure_set(&["float_reader", "float_site", "str_reader", "str_site"]), + &Sigs::new(), + ); + assert_eq!(mask(&sigs, "float_reader"), None); + assert_eq!(mask(&sigs, "str_reader"), Some(&vec![true])); + } + + #[test] + fn impure_functions_are_not_candidates() { + let reader = f( + "reader", + &["xs"], + Comp::Case( + Value::Var(s("xs")), + vec![(CorePat::Wild, Comp::Return(Value::Int(1)))], + ), + ); + let sigs = infer_borrow_sigs(&core(vec![reader]), &Set::new(), &Sigs::new()); + assert!(mask(&sigs, "reader").is_none()); + } +} diff --git a/crates/prism-core/src/core/fbip/check.rs b/crates/prism-core/src/core/fbip/check.rs index 9c54d71c..1c1b40cc 100644 --- a/crates/prism-core/src/core/fbip/check.rs +++ b/crates/prism-core/src/core/fbip/check.rs @@ -869,6 +869,7 @@ mod tests { params: (0..params.len()).map(|i| format!("p{i}")).collect(), ty: Type::fun(params, Type::Int), effects: Set::new(), + pure: true, } } diff --git a/crates/prism-core/src/core/fbip/imbalance.rs b/crates/prism-core/src/core/fbip/imbalance.rs new file mode 100644 index 00000000..fa4fa293 --- /dev/null +++ b/crates/prism-core/src/core/fbip/imbalance.rs @@ -0,0 +1,111 @@ +//! What the reference-count token machine rejected, and where. +//! +//! The balance check is an independent verifier: it re-simulates the inserted +//! dup/drop ops and fails when a count goes negative, when a binding leaves +//! scope holding tokens, or when two arms of a branch disagree. Every one of +//! those is an internal invariant violation, never a user diagnostic, so the +//! reason travels as data from the site that found it to the caller that +//! reports it. A test that wants to pin which invariant broke matches a +//! variant; before this it matched a substring of the sentence, which made the +//! sentence the contract and left it unsafe to reword. + +use std::fmt; + +use prism_common::sym::Sym; + +use super::super::cbpv::Value; + +/// The shapes the token machine can reject at. +#[derive(Clone, Debug, PartialEq)] +pub enum TokenFault { + /// A binding left its scope still holding tokens: the pass under-dropped. + ScopeExit { var: Sym, tokens: i64 }, + /// A closure capture left the thunk body still holding tokens. Captures + /// start borrowed, so this is the same fault seen through a thunk. + ThunkCapture { var: Sym, tokens: i64 }, + /// A use drove a count below zero: the pass under-dup'd, or dropped a value + /// that was still live. + BelowZero { var: Sym }, + /// A field extracted by a pattern was still holding tokens when its arm + /// ended. + ArmLeak { field: Sym }, + /// A borrowed argument was not live across the call that borrows it, so the + /// callee would read a cell the caller had already released. + BorrowNotLive { var: Sym, callee: Sym }, + /// Two arms of a branch left the same binding at different counts, so no + /// single count describes the join. + BranchDisagreement { var: Sym, left: i64, right: i64 }, + /// A borrowed argument was not a let-bound variable, so there is no binding + /// for the loan to be held against. + BorrowedArgNotBound { callee: Sym, arg: Box }, +} + +impl fmt::Display for TokenFault { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ScopeExit { var, tokens } => write!(f, "{var} ends with {tokens} tokens"), + Self::ThunkCapture { var, tokens } => { + write!(f, "thunk capture {var} ends with {tokens} tokens") + } + Self::BelowZero { var } => write!(f, "{var} consumed below zero"), + Self::ArmLeak { field } => write!(f, "field {field} leaks in arm"), + Self::BorrowNotLive { var, callee } => write!( + f, + "borrowed call argument {var} is not live through call to {callee}" + ), + Self::BranchDisagreement { var, left, right } => { + write!(f, "branch disagreement on {var}: {left} vs {right}") + } + Self::BorrowedArgNotBound { callee, arg } => write!( + f, + "borrowed argument to {callee} is not a let-bound variable: {arg:?}" + ), + } + } +} + +/// A token fault together with the declaration it was found in. +/// +/// The function is optional because the same simulation runs over a thunk body +/// and over a fixture handed straight to the checker, where there is no +/// enclosing declaration to name. +#[derive(Clone, Debug, PartialEq)] +pub struct Imbalance { + pub fault: TokenFault, + pub function: Option, +} + +impl Imbalance { + /// A fault with no declaration attributed to it yet. + #[must_use] + pub const fn new(fault: TokenFault) -> Self { + Self { + fault, + function: None, + } + } + + /// Attribute a fault to the declaration whose body was being simulated. + #[must_use] + pub const fn in_function(fault: TokenFault, function: Sym) -> Self { + Self { + fault, + function: Some(function), + } + } +} + +impl From for Imbalance { + fn from(fault: TokenFault) -> Self { + Self::new(fault) + } +} + +impl fmt::Display for Imbalance { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.function { + Some(function) => write!(f, "{function}: {}", self.fault), + None => write!(f, "{}", self.fault), + } + } +} diff --git a/crates/prism-core/src/core/fbip/mod.rs b/crates/prism-core/src/core/fbip/mod.rs index 2e1a24ef..84f73004 100644 --- a/crates/prism-core/src/core/fbip/mod.rs +++ b/crates/prism-core/src/core/fbip/mod.rs @@ -8,14 +8,19 @@ use prism_syntax::{ use super::cbpv::Value; use super::fv::comp as freev; +use crate::types::scalar_plan; mod balance; +mod borrow; mod check; +mod imbalance; mod rc; mod reuse; pub use balance::balanced; +pub use borrow::infer_borrow_sigs; pub use check::{check_fip, check_fip_linear, fip_annots, replayable_annots, Fips}; +pub use imbalance::{Imbalance, TokenFault}; pub use rc::insert_rc; pub use reuse::reuse; @@ -86,19 +91,18 @@ pub fn borrow_sigs(prog: &Program) -> Sigs { // A borrow-position call arg is normally a `Value::Var`: the caller retains one // ownership token through the call and drops it afterward when the loan is its // last use. Mandatory newtype erasure and scalar folding may expose a literal -// immediate directly at the call. Such a value owns no heap cell and needs no -// retained token; every other non-variable remains an invariant error rather -// than silently leaking a temporary heap value. -const fn immediate_borrow_arg(value: &Value) -> bool { - matches!( - value, - Value::Int(_) - | Value::I64(_) - | Value::U64(_) - | Value::Float(_) - | Value::Bool(_) - | Value::Unit - ) +// directly at the call, and that is fine only when the literal's encoding plan +// owns no fresh heap cell: a zero or tagged word, or the static cell a `Str` +// literal names. A literal whose plan mints a fresh cell per use needs an +// owner at a borrowed position: the typed RC pass anchors it to a binder, and +// here, like every other cell-owning value, an unanchored one is an invariant +// error rather than a silently leaking temporary. Mirrors the typed pass's +// `scalar_without_cell`, reading the same representation authority. +fn scalar_without_cell(value: &Value) -> bool { + value + .literal_scalar_type() + .and_then(|ty| scalar_plan(&ty).ok()) + .is_some_and(|plan| !plan.owns_fresh_cell()) } fn borrow_mask(name: Sym, sigs: &Sigs) -> Option<&[bool]> { @@ -111,17 +115,18 @@ fn borrowed_at(mask: Option<&[bool]>, i: usize) -> bool { mask.is_some_and(|m| m.get(i).copied().unwrap_or(false)) } -fn borrowed_call_vars(name: Sym, args: &[Value], sigs: &Sigs) -> Result { +fn borrowed_call_vars(name: Sym, args: &[Value], sigs: &Sigs) -> Result { let mask = borrow_mask(name, sigs); args.iter() .enumerate() .filter(|(index, _)| borrowed_at(mask, *index)) .filter_map(|(_, arg)| match arg { Value::Var(var) => Some(Ok(*var)), - value if immediate_borrow_arg(value) => None, - _ => Some(Err(format!( - "borrowed argument to {name} is not a let-bound variable: {arg:?}" - ))), + value if scalar_without_cell(value) => None, + _ => Some(Err(TokenFault::BorrowedArgNotBound { + callee: name, + arg: Box::new(arg.clone()), + })), }) .collect() } diff --git a/crates/prism-core/src/core/fbip/rc.rs b/crates/prism-core/src/core/fbip/rc.rs index 7745011d..2ac23650 100644 --- a/crates/prism-core/src/core/fbip/rc.rs +++ b/crates/prism-core/src/core/fbip/rc.rs @@ -6,6 +6,7 @@ use prism_syntax::names; use super::super::cbpv::{Comp, Core, CoreFn, CorePat, HandleOp, Value}; use super::super::fv::{comp as freev, pat_vars}; +use super::super::traverse::Rewrite; use super::{borrow_mask, borrowed_at, borrowed_call_vars, count_val, Set, Sigs}; #[must_use] @@ -82,6 +83,13 @@ fn rc(c: &Comp, owned: &Set, borrowed: &Set, sigs: &Sigs, fresh: &mut Fresh) -> let fm = freev(m); let mut fnn = freev(n); fnn.remove(x); + // A bind that merely renames a loaned reference extends the loan: + // the binder reads the same cell the loan already keeps live, so + // it takes no reference of its own and joins the borrowed set for + // the rest of the chain. The token simulation in `balanced` keys + // on the identical syntactic shape. + let alias = x.as_str() != "_" + && matches!(&**m, Comp::Return(Value::Var(v)) if borrowed.contains(v)); let owned_m: Set = owned.intersection(&fm).copied().collect(); let owned_n: Set = owned.intersection(&fnn).copied().collect(); let shared = by_name(owned_m.intersection(&owned_n).copied()); @@ -93,10 +101,19 @@ fn rc(c: &Comp, owned: &Set, borrowed: &Set, sigs: &Sigs, fresh: &mut Fresh) -> ); let borrowed_m: Set = borrowed.intersection(&fm).copied().collect(); let borrowed_n: Set = borrowed.intersection(&fnn).copied().collect(); - let m2 = rc(m, &owned_m, &borrowed_m, sigs, fresh); + let m2 = if alias { + (**m).clone() + } else { + rc(m, &owned_m, &borrowed_m, sigs, fresh) + }; let mut owned_n2 = owned_n; - owned_n2.insert(*x); - let n2 = rc(n, &owned_n2, &borrowed_n, sigs, fresh); + let mut borrowed_n2 = borrowed_n; + if alias { + borrowed_n2.insert(*x); + } else { + owned_n2.insert(*x); + } + let n2 = rc(n, &owned_n2, &borrowed_n2, sigs, fresh); let mut out = Comp::Bind(Box::new(m2), *x, Box::new(n2)); for v in shared { out = dup(v, out); @@ -111,12 +128,29 @@ fn rc(c: &Comp, owned: &Set, borrowed: &Set, sigs: &Sigs, fresh: &mut Fresh) -> Box::new(rc(t, owned, borrowed, sigs, fresh)), Box::new(rc(e, owned, borrowed, sigs, fresh)), ), - Comp::Case(scrut, arms) => Comp::Case( - scrut.clone(), - arms.iter() - .map(|(p, body)| (p.clone(), rc_arm(p, body, owned, borrowed, sigs, fresh))) - .collect(), - ), + Comp::Case(scrut, arms) => { + // Matching on a loaned cell reads it without taking a reference: + // no arm drops the cell (it is not owned here), and the pattern + // binders become loans on its fields, kept live by whatever keeps + // the parent live. Consuming uses of a field still dup first via + // the borrowed leaf rule below. + let loaned = matches!(scrut, Value::Var(v) if borrowed.contains(v)); + let tracked: Set = owned.union(borrowed).copied().collect(); + Comp::Case( + scrut.clone(), + arms.iter() + .map(|(p, body)| { + let unshadowed = unshadow_arm(p, body, &tracked, fresh); + let (p, body) = + unshadowed.as_ref().map_or((p, body), |(p, body)| (p, body)); + ( + p.clone(), + rc_arm(p, body, owned, borrowed, sigs, fresh, loaned), + ) + }) + .collect(), + ) + } Comp::Lam(ps, body) => { let ps_set: Set = ps.iter().copied().collect(); let caps: Set = freev(body).difference(&ps_set).copied().collect(); @@ -260,6 +294,146 @@ fn rc_thunks(c: &Comp, sigs: &Sigs, fresh: &mut Fresh) -> Comp { } } +/// Rebind pattern binders that reuse a name the match site still tracks. +/// +/// A field binder spelled like a reference the site owns or borrows hides that +/// reference for the whole arm: every occurrence in the body denotes the field, +/// and the outer reference has none left there. Free variables are names, so +/// the liveness test in [`rc_arm`] would read those field occurrences as uses +/// of the outer reference, judge it live, and emit no release for it. Nor could +/// the release be recovered inside the arm, where a `drop` of that name would +/// name the field instead. Renaming the binder restores the arm to the shape it +/// would have had without the collision. `Comp::Bind` needs no such treatment: +/// its release is emitted outside the binder's scope, where the name still +/// denotes the outer cell. +fn unshadow_arm( + p: &CorePat, + body: &Comp, + tracked: &Set, + fresh: &mut Fresh, +) -> Option<(CorePat, Comp)> { + let mut fields = Set::new(); + pat_vars(p, &mut fields); + let shadowing = by_name(fields.intersection(tracked).copied()); + if shadowing.is_empty() { + return None; + } + let mut p = p.clone(); + let mut body = body.clone(); + for from in shadowing { + let to = Sym::from(names::fresh_binder(names::FRESH_RC, fresh.bump()).as_str()); + p = rename_pat(&p, from, to); + body = RenameFree { from, to }.comp(&body, &true); + } + Some((p, body)) +} + +fn rename_pat(p: &CorePat, from: Sym, to: Sym) -> CorePat { + let rebind = |name: &Sym| if *name == from { to } else { *name }; + let rebind_fields = |fields: &Vec>| { + fields + .iter() + .map(|field| field.as_ref().map(&rebind)) + .collect() + }; + match p { + CorePat::Wild => CorePat::Wild, + CorePat::Var(name) => CorePat::Var(rebind(name)), + CorePat::Ctor(ctor, fields) => CorePat::Ctor(*ctor, rebind_fields(fields)), + CorePat::Tuple(fields) => CorePat::Tuple(rebind_fields(fields)), + } +} + +/// Rename free occurrences of one local, stopping where a binder rebinds it. +/// +/// The context is whether the renamed name is still the one this subterm's +/// occurrences denote. No capture check is needed in the other direction: the +/// replacement is an unforgeable fresh name, so nothing here can bind it. +struct RenameFree { + from: Sym, + to: Sym, +} + +impl RenameFree { + fn visible_under(&self, binders: impl IntoIterator) -> bool { + !binders.into_iter().any(|binder| binder == self.from) + } +} + +impl Rewrite for RenameFree { + type Ctx = bool; + + fn comp(&mut self, c: &Comp, visible: &bool) -> Comp { + if !*visible { + return c.clone(); + } + match c { + Comp::Bind(m, x, n) => { + let under = self.visible_under([*x]); + Comp::Bind( + Box::new(self.comp(m, visible)), + *x, + Box::new(self.comp(n, &under)), + ) + } + Comp::Lam(params, body) => { + let under = self.visible_under(params.iter().copied()); + Comp::Lam(params.clone(), Box::new(self.comp(body, &under))) + } + Comp::Case(scrut, arms) => Comp::Case( + self.value(scrut, visible), + arms.iter() + .map(|(p, body)| { + let mut fields = Set::new(); + pat_vars(p, &mut fields); + let under = self.visible_under(fields); + (p.clone(), self.comp(body, &under)) + }) + .collect(), + ), + Comp::WithReuse { token, freed, body } => { + let under = self.visible_under([*token]); + Comp::WithReuse { + token: *token, + freed: self.value(freed, visible), + body: Box::new(self.comp(body, &under)), + } + } + Comp::Handle { + body, + return_var, + return_body, + ops, + } => Comp::Handle { + body: Box::new(self.comp(body, visible)), + return_var: *return_var, + return_body: return_body.as_ref().map(|rb| { + let under = self.visible_under(return_var.iter().copied()); + Box::new(self.comp(rb, &under)) + }), + ops: ops.rebuild(|op| HandleOp { + name: op.name, + params: op.params.clone(), + resume: op.resume, + body: { + let binders = op.params.iter().copied().chain([op.resume]); + let under = self.visible_under(binders); + self.comp(&op.body, &under) + }, + }), + }, + _ => self.descend_comp(c, visible), + } + } + + fn value(&mut self, v: &Value, visible: &bool) -> Value { + match v { + Value::Var(name) if *visible && *name == self.from => Value::Var(self.to), + _ => self.descend_value(v, visible), + } + } +} + fn rc_arm( p: &CorePat, body: &Comp, @@ -267,6 +441,7 @@ fn rc_arm( borrowed: &Set, sigs: &Sigs, fresh: &mut Fresh, + loaned: bool, ) -> Comp { let fb = freev(body); let mut fields = Set::new(); @@ -274,14 +449,20 @@ fn rc_arm( let live = by_name(fields.intersection(&fb).copied()); let dead = by_name(owned.iter().filter(|v| !fb.contains(*v)).copied()); let mut owned_b: Set = owned.intersection(&fb).copied().collect(); - owned_b.extend(live.iter().copied()); - let borrowed_b: Set = borrowed.intersection(&fb).copied().collect(); + let mut borrowed_b: Set = borrowed.intersection(&fb).copied().collect(); + if loaned { + borrowed_b.extend(live.iter().copied()); + } else { + owned_b.extend(live.iter().copied()); + } let mut out = rc(body, &owned_b, &borrowed_b, sigs, fresh); for v in &dead { out = drop_(*v, out); } - for v in live.iter().rev() { - out = dup(*v, out); + if !loaned { + for v in live.iter().rev() { + out = dup(*v, out); + } } out } diff --git a/crates/prism-core/src/core/fbip/reuse.rs b/crates/prism-core/src/core/fbip/reuse.rs index 7a7628f5..fac3f34d 100644 --- a/crates/prism-core/src/core/fbip/reuse.rs +++ b/crates/prism-core/src/core/fbip/reuse.rs @@ -141,7 +141,7 @@ fn try_reuse(c: &Comp, s: Sym, tok: Sym, cap: usize) -> Option { } // Reuse credit (FP^2): a freed token feeds the first constructor allocation that -// follows the drop on every control path, not just the literal tail. Walk the +// follows the drop on every control path. Walk the // bind chain forward and rewrite the first `return Ctor` (whose arity fits the // freed cell, so prism_reuse_alloc never writes past the old shell) into an // in-place `Reuse`; the token is then spent and the continuation left alone. At a diff --git a/crates/prism-core/src/core/mod.rs b/crates/prism-core/src/core/mod.rs index 2be33ec1..ccaa81f5 100644 --- a/crates/prism-core/src/core/mod.rs +++ b/crates/prism-core/src/core/mod.rs @@ -18,6 +18,7 @@ pub mod simd; pub mod tailrec; pub mod traverse; pub mod typed; +pub mod work; pub use cbpv::{ reachable_fns, CheckedHandler, Comp, Core, CoreFn, CoreOp, CorePat, ElaboratedCore, HandleOp, @@ -44,10 +45,10 @@ pub use opt::{ pub use pretty::{pp_comp, pp_core, pp_core_pretty, pp_value}; pub use shape::{class_digests, contract_digest, instance_digest, shape_digests}; pub use typed::{ - verify as verify_typed_core, CompSig, ConstructorSig, CoreFnSig, CoreInstantiation, - CoreQuantifier, CoreType, CoreViolation, EffectLowered as TypedEffectLowered, - Elaborated as TypedElaborated, OperationSig, Owned as TypedOwned, - ReuseLowered as TypedReuseLowered, TypedBinder, TypedComp, TypedCompKind, TypedCore, - TypedCoreFn, TypedCorePhase, TypedForward, TypedHandleOp, TypedHandler, TypedPattern, - TypedValue, TypedValueKind, VerifyEnv, + audit as audit_typed_core, verify as verify_typed_core, CompSig, ConstructorSig, CoreFnSig, + CoreInstantiation, CoreQuantifier, CoreType, CoreViolation, + EffectLowered as TypedEffectLowered, Elaborated as TypedElaborated, OperationSig, + Owned as TypedOwned, ReuseLowered as TypedReuseLowered, TypedBinder, TypedComp, TypedCompKind, + TypedCore, TypedCoreFn, TypedCorePhase, TypedForward, TypedHandleOp, TypedHandler, + TypedPattern, TypedValue, TypedValueKind, UncheckedTypedCore, VerifyEnv, }; diff --git a/crates/prism-core/src/core/opt/lint.rs b/crates/prism-core/src/core/opt/lint.rs index 7a30fd4c..8d11fb23 100644 --- a/crates/prism-core/src/core/opt/lint.rs +++ b/crates/prism-core/src/core/opt/lint.rs @@ -28,12 +28,41 @@ use std::collections::BTreeSet; -use super::super::cbpv::{Comp, Core, LoweredCore, Value}; +use super::super::cbpv::{Comp, Core, CoreFn, ElaboratedCore, LoweredCore, Value}; use super::super::fv; use super::super::traverse::Visit; use super::PassStage; use prism_common::sym::Sym; +impl ElaboratedCore { + /// Validate a pre-effect-lowering program and mint its stage claim. + /// + /// This is the only public construction path. It rejects runtime nodes, + /// unbound variables, and invalid reuse scopes before returning the wrapper. + /// + /// # Errors + /// One message per structural violation, as the stage lint reports them. + pub fn validate(core: Core) -> Result> { + lint(&core, PassStage::PreLowering)?; + Ok(Self::new(core)) + } + + /// Append synthesized top-level functions and revalidate the whole program. + /// Consuming `self` prevents a caller from retaining a validated wrapper while + /// mutating its contents behind the stage claim. + /// + /// # Errors + /// One message per structural violation after the functions are appended. + pub fn with_functions( + self, + functions: impl IntoIterator, + ) -> Result> { + let mut core = self.into_core(); + core.fns.extend(functions); + Self::validate(core) + } +} + impl LoweredCore { /// Structural stage validation, lint-grade: the checked public constructor /// for a lowered program an external producer hands to the backends. @@ -47,10 +76,18 @@ impl LoweredCore { /// /// # Errors /// One message per structural violation, as the stage lint reports them. - pub fn validate_structural(core: Core) -> Result> { + pub fn validate(core: Core) -> Result> { lint(&core, PassStage::Late)?; Ok(Self::new(core)) } + + /// Backward-compatible spelling for the checked lowered-stage transition. + /// + /// # Errors + /// One message per structural violation, as the stage lint reports them. + pub fn validate_structural(core: Core) -> Result> { + Self::validate(core) + } } /// Lint `core` at pipeline `stage`, returning one message per violation. @@ -262,3 +299,39 @@ fn spends_val(token: Sym, v: &Value) -> usize { _ => 0, } } + +#[cfg(test)] +mod tests { + use super::*; + + fn program(body: Comp) -> Core { + Core { + fns: vec![CoreFn { + name: Sym::new("main"), + params: Vec::new(), + body, + dict_arity: 0, + }], + } + } + + #[test] + fn checked_stage_doors_reject_the_other_nodes() { + assert!(ElaboratedCore::validate(program(Comp::Return(Value::Int(1)))).is_ok()); + assert!(ElaboratedCore::validate(program(Comp::Dup(Value::Int(1)))).is_err()); + assert!(LoweredCore::validate(program(Comp::Do(Sym::new("read"), Vec::new()))).is_err()); + } + + #[test] + fn appending_functions_revalidates_the_stage_claim() { + let core = ElaboratedCore::validate(program(Comp::Return(Value::Int(1)))) + .expect("plain elaborated core"); + let invalid = CoreFn { + name: Sym::new("late"), + params: Vec::new(), + body: Comp::Drop(Value::Int(1)), + dict_arity: 0, + }; + assert!(core.with_functions([invalid]).is_err()); + } +} diff --git a/crates/prism-core/src/core/opt/mod.rs b/crates/prism-core/src/core/opt/mod.rs index 190f6513..5f15227a 100644 --- a/crates/prism-core/src/core/opt/mod.rs +++ b/crates/prism-core/src/core/opt/mod.rs @@ -138,7 +138,9 @@ impl CorePass { } /// Whether this pass transforms each definition independently and therefore - /// admits an SCC-local durable query boundary. + /// admits an SCC-local durable query boundary. Such passes must preserve the + /// input global-name set: regrouping keeps the original program order and + /// rejects any added or dropped definition. #[must_use] pub const fn is_scc_local(self) -> bool { matches!(self, Self::EraseNewtypes | Self::Simplify | Self::Cse) diff --git a/crates/prism-core/src/core/shape.rs b/crates/prism-core/src/core/shape.rs index 57a22600..0ab71e44 100644 --- a/crates/prism-core/src/core/shape.rs +++ b/crates/prism-core/src/core/shape.rs @@ -122,7 +122,7 @@ fn encode_data(d: &DataDecl) -> String { e.out.push_str("|data"); e.tok(&d.name); let _ = write!(e.out, "nt{}", u8::from(d.newtype)); - // Commit the parameter arity, not just the kinds: `param_kinds` is legally empty + // Commit both parameter arity and kinds: `param_kinds` is legally empty // (kinds default to Type), and without the count `data Phantom a` and // `data Phantom a b` would encode identically. let _ = write!(e.out, "K{}", d.params.len()); diff --git a/crates/prism-core/src/core/tailrec.rs b/crates/prism-core/src/core/tailrec.rs index 0a14e5c6..8b3a3e10 100644 --- a/crates/prism-core/src/core/tailrec.rs +++ b/crates/prism-core/src/core/tailrec.rs @@ -180,6 +180,23 @@ pub enum TailClass { NonTail, } +/// Whether the backend lowers a tail-position call as a loop rather than a +/// frame. +/// +/// A `musttail` call reuses the current frame, which the ABI permits only when +/// the two signatures agree: the call must be saturated and the callee must +/// take exactly as many parameters as the frame making the call. That covers a +/// saturated self-call and a same-arity mutual tail call alike, and it is the +/// test the emitter applies at its tail-call site. +/// +/// Every pass that can perturb a tail call asks here rather than restating the +/// arithmetic, so a lowering the emitter would loop is never quietly turned +/// into a stack-growing one somewhere upstream. +#[must_use] +pub const fn loops_as_tail_call(args: usize, callee_arity: usize, frame_arity: usize) -> bool { + args == callee_arity && callee_arity == frame_arity +} + // Classify every call to a member of `group` reachable within `body`'s own // evaluation, in source order. Calls hidden inside a thunk, lambda, or handler // run in a later, separate frame and do not grow THIS body's stack, so the walk diff --git a/crates/prism-core/src/core/traverse.rs b/crates/prism-core/src/core/traverse.rs index ce289447..2d8ae8ce 100644 --- a/crates/prism-core/src/core/traverse.rs +++ b/crates/prism-core/src/core/traverse.rs @@ -21,6 +21,7 @@ //! only consumer, so it stays bespoke until a second one appears. use super::cbpv::{Comp, HandleOp, Value}; +use super::work; /// Whole-term rewrite threading an immutable context extended at binders. /// @@ -41,6 +42,8 @@ pub trait Rewrite { } fn descend_comp(&mut self, c: &Comp, cx: &Self::Ctx) -> Comp { + let _frame = work::frame(); + work::rebuild(); match c { Comp::Return(v) => Comp::Return(self.value(v, cx)), Comp::Bind(a, x, b) => { @@ -107,6 +110,8 @@ pub trait Rewrite { } fn descend_value(&mut self, v: &Value, cx: &Self::Ctx) -> Value { + let _frame = work::frame(); + work::rebuild(); match v { Value::Thunk(c) => Value::Thunk(Box::new(self.comp(c, cx))), Value::Ctor(n, t, fs) => { @@ -157,6 +162,8 @@ pub trait Visit { } fn descend_comp(&mut self, c: &Comp) { + let _frame = work::frame(); + work::visit(); match c { Comp::Return(v) | Comp::Force(v) @@ -225,6 +232,8 @@ pub trait Visit { } fn descend_value(&mut self, v: &Value) { + let _frame = work::frame(); + work::visit(); match v { Value::Thunk(c) => self.visit_comp(c), Value::UnboxedRecord(fs) => { diff --git a/crates/prism-core/src/core/typed.rs b/crates/prism-core/src/core/typed.rs index cab8c5e4..3e353b1e 100644 --- a/crates/prism-core/src/core/typed.rs +++ b/crates/prism-core/src/core/typed.rs @@ -5,6 +5,7 @@ //! boundary. [`super::Core`] remains the executable representation consumed by //! passes outside the verified typed prefix. +mod authority; mod build; mod cse; pub mod effect_lower; @@ -17,7 +18,9 @@ mod simplify; pub mod specialize; mod specialize_support; pub mod verify; +pub mod violation; +pub use authority::{audit, verify, TypedCore, UncheckedTypedCore}; pub use build::{build_typed, build_verify_env, core_fn_sig, dict_type}; // The raw typed passes, exposed for the driver's ordered stage runner (which // owns verification boundaries and the SCC fixed-point cache). @@ -40,17 +43,17 @@ pub use verify::{ instantiate_constructor, instantiate_fn, instantiate_operation, instantiate_value_scheme, scheme_to_fn_sig, }; -pub use verify::{verify, ConstructorSig, CoreViolation, OperationSig, TypedCorePhase, VerifyEnv}; +pub use verify::{ConstructorSig, CoreViolation, OperationSig, TypedCorePhase, VerifyEnv}; use std::collections::BTreeSet; -use std::marker::PhantomData; +use std::fmt; use crate::types::ty::{EffRow, Label}; use crate::types::Type; use prism_common::sym::Sym; use super::{builtins::Builtin, builtins::FloatOp}; -use super::{CheckedHandler, Comp, Core, CoreFn, CoreOp, CorePat, HandleOp, IoOp, NegLane, Value}; +use super::{CheckedHandler, Comp, CoreFn, CoreOp, CorePat, HandleOp, IoOp, NegLane, Value}; // Erasure walks a whole function body as one non-tail recursion, so a deeply // nested definition can outgrow a small host stack. The same segment-growing @@ -184,6 +187,84 @@ impl CoreFnSig { } } +// Source-shaped renderings for the witness types. +// +// These exist because a failed judgment is read by a person. `Debug` on these +// types prints the constructor spelling of an internal representation +// (`Thunk(CompSig { result: Source(Fun([...], Empty, ...)), .. })`), which names +// the compiler's data structures rather than the type the program wrote, and it +// is what a verifier violation used to put in front of a user. Every rendering +// below bottoms out in `Type::show`/`EffRow::show`, the same printers the +// checker's own diagnostics use, so one type reads the same way wherever it is +// reported. + +impl fmt::Display for CoreType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Source(ty) => f.write_str(&ty.show()), + Self::Thunk(signature) => write!(f, "Thunk({signature})"), + Self::Function(signature) => write!(f, "{signature}"), + Self::Ref(ty) => write!(f, "Ref({ty})"), + Self::ReuseToken(ty) => write!(f, "Reuse({ty})"), + Self::Lowered(ty) => write!(f, "{ty}"), + } + } +} + +impl fmt::Display for LoweredType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Word => f.write_str("Word"), + Self::Eff(row) => write!(f, "Eff({})", row.show()), + Self::Queue(row) => write!(f, "Queue({})", row.show()), + Self::QueueView(row) => write!(f, "QueueView({})", row.show()), + } + } +} + +impl fmt::Display for CompSig { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} ! {}", self.result, self.effects.show()) + } +} + +impl fmt::Display for CoreQuantifier { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Type(name) | Self::Row(name) => write!(f, "{name}"), + } + } +} + +impl fmt::Display for CoreFnSig { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if !self.quantifiers.is_empty() { + write!(f, "forall")?; + for quantifier in &self.quantifiers { + write!(f, " {quantifier}")?; + } + write!(f, ". ")?; + } + f.write_str("(")?; + for (index, param) in self.params.iter().enumerate() { + if index > 0 { + f.write_str(", ")?; + } + write!(f, "{param}")?; + } + write!(f, ") -> {}", self.body) + } +} + +impl fmt::Display for CoreInstantiation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Type(ty) => f.write_str(&ty.show()), + Self::Row(row) => f.write_str(&row.show()), + } + } +} + /// A typed local binder. #[derive(Clone, Debug, PartialEq, Eq)] pub struct TypedBinder { @@ -316,6 +397,27 @@ impl TypedValue { Self { ty, kind } } + /// The binding this value reads, if it reads one. + /// + /// Representation wrappers change how a reference is typed, never which + /// reference it is, so they are transparent here. Anything else builds a + /// new value rather than reading an existing binding and has no name to + /// give. Reference counting asks this to find the owner an operation acts + /// on, and the verifier asks it to refuse an operation that acts on none. + #[must_use] + pub fn referenced_binding(&self) -> Option { + match &self.kind { + TypedValueKind::Var { name, .. } => Some(*name), + TypedValueKind::Reinterpret(inner) + | TypedValueKind::LoweredRepr { + value: inner, + proof: _, + } + | TypedValueKind::NewtypeRepr { value: inner, .. } => inner.referenced_binding(), + _ => None, + } + } + fn erase(self) -> Value { match self.kind { TypedValueKind::Var { @@ -426,6 +528,36 @@ pub enum TypedValueKind { UnboxedRecord(Vec<(Sym, TypedValue)>), } +impl TypedValueKind { + /// The canonical source type of a scalar literal, or `None` for a value + /// with no literal encoding (variables, structures, thunks). + /// + /// Seen through the representation-preserving wrapper nodes: a wrapped + /// literal keeps its scalar encoding by the wrappers' own contract, so + /// the answer is the underlying literal's type. Consumers pass it to the + /// representation authority (`types::scalar_plan`) rather than deciding + /// an encoding here. Mirrors `Value::literal_scalar_type` post-erasure. + #[must_use] + pub fn literal_scalar_type(&self) -> Option { + match self { + Self::Int(_) => Some(Type::Int), + Self::I64(_) => Some(Type::I64), + Self::U64(_) => Some(Type::U64), + Self::Float(_) => Some(Type::Float), + Self::Bool(_) => Some(Type::Bool), + Self::Unit => Some(Type::Unit), + Self::Str(_) => Some(Type::Str), + Self::Reinterpret(inner) + | Self::LoweredRepr { + value: inner, + proof: _, + } + | Self::NewtypeRepr { value: inner, .. } => inner.kind.literal_scalar_type(), + _ => None, + } + } +} + /// One typed handler operation clause. #[derive(Clone, Debug, PartialEq)] pub struct TypedHandleOp { @@ -563,7 +695,10 @@ impl TypedHandler { ) } - fn with_forwarded(mut self, mut forwarded: Vec) -> Self { + pub(in crate::core::typed) fn with_forwarded( + mut self, + mut forwarded: Vec, + ) -> Self { forwarded.sort(); forwarded.dedup(); self.forwarded = forwarded; @@ -885,72 +1020,12 @@ pub enum Owned {} #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ReuseLowered {} -/// Whole-program typed Core at phase `P`. -/// -/// The phase marker prevents routing errors while the private node builders and -/// independent verifier prevent local type/effect witness drift. -#[derive(Debug, PartialEq)] -pub struct TypedCore

{ - fns: Vec, - phase: PhantomData P>, -} - -// Manual so a phase-generic caller can clone: the derive would demand -// `P: Clone`, but the marker only ever appears under `PhantomData`. -impl

Clone for TypedCore

{ - fn clone(&self) -> Self { - Self { - fns: self.fns.clone(), - phase: PhantomData, - } - } -} - -impl

TypedCore

{ - /// Functions in deterministic program order. - #[must_use] - pub fn functions(&self) -> &[TypedCoreFn] { - &self.fns - } - - #[must_use] - pub const fn new(fns: Vec) -> Self { - Self { - fns, - phase: PhantomData, - } - } - - /// Decompose into owned functions, for the driver's SCC-local pass cache. - #[must_use] - pub fn into_functions(self) -> Vec { - self.fns - } - - /// Regroup verified functions at the same phase. [`TypedCoreFn`]s can only - /// be built inside `core`, so this can subset or reorder verified programs - /// but never forge a witness. - #[must_use] - pub const fn from_functions(fns: Vec) -> Self { - Self::new(fns) - } - - /// Consume all type/effect witnesses, yielding the existing executable - /// Core shape byte-for-byte. This is the sole semantic erasure operation at - /// the typed-prefix boundary. - #[must_use] - pub fn erase(self) -> Core { - Core { - fns: self.fns.into_iter().map(TypedCoreFn::erase).collect(), - } - } -} - #[cfg(test)] mod tests { use std::collections::BTreeMap; use crate::core::hash::hash_program; + use crate::core::Core; use super::*; @@ -966,16 +1041,19 @@ mod tests { TypedValue::new(source(ty), TypedValueKind::Int(42)) } - fn program(witness: Type) -> TypedCore { + fn erased_program(witness: Type) -> Core { let value = literal(witness.clone()); let body = TypedComp::new(pure(source(witness.clone())), TypedCompKind::Return(value)); - TypedCore::new(vec![TypedCoreFn::new( - Sym::new("main"), - Vec::new(), - body, - CoreFnSig::new(Vec::new(), Vec::new(), pure(source(witness))), - 0, - )]) + Core { + fns: vec![TypedCoreFn::new( + Sym::new("main"), + Vec::new(), + body, + CoreFnSig::new(Vec::new(), Vec::new(), pure(source(witness))), + 0, + ) + .erase()], + } } #[test] @@ -983,8 +1061,8 @@ mod tests { // Deliberately bypass verification and vary only witness data. A Bool // witness on an integer literal is invalid, while content identity must // remain a function of erased semantics alone. - let int = program(Type::Int).erase(); - let bool_witness = program(Type::Bool).erase(); + let int = erased_program(Type::Int); + let bool_witness = erased_program(Type::Bool); assert_eq!(int, bool_witness); assert_eq!( hash_program(&int, &BTreeMap::new()), diff --git a/crates/prism-core/src/core/typed/authority.rs b/crates/prism-core/src/core/typed/authority.rs new file mode 100644 index 00000000..05ce401b --- /dev/null +++ b/crates/prism-core/src/core/typed/authority.rs @@ -0,0 +1,142 @@ +//! Construction authority for whole-program typed Core. +//! +//! Passes may freely assemble unchecked witnesses, but only the independent +//! whole-program verifier can mint the authoritative phase marker consumed by +//! the next compiler transition. + +use std::marker::PhantomData; + +use super::verify::{check_functions, CoreViolation, TypedCorePhase, VerifyEnv}; +use super::TypedCoreFn; +use crate::core::Core; + +/// Whole-program typed Core awaiting independent verification at phase `P`. +/// +/// This is the honest result type for rewrites and partial SCC work: its +/// functions carry witnesses, but the complete global table and phase-local +/// judgments have not yet been checked. +#[derive(Debug, PartialEq)] +pub struct UncheckedTypedCore

{ + fns: Vec, + phase: PhantomData P>, +} + +// Manual so a phase-generic caller can clone without requiring `P: Clone`. +impl

Clone for UncheckedTypedCore

{ + fn clone(&self) -> Self { + Self::new(self.fns.clone()) + } +} + +impl

UncheckedTypedCore

{ + /// Assemble functions without claiming that their witnesses verify. + #[must_use] + pub const fn new(fns: Vec) -> Self { + Self { + fns, + phase: PhantomData, + } + } + + /// Functions in deterministic program order. + #[must_use] + pub fn functions(&self) -> &[TypedCoreFn] { + &self.fns + } + + /// Decompose an unchecked assembly for regrouping or another rewrite. + #[must_use] + pub fn into_functions(self) -> Vec { + self.fns + } +} + +/// Independently verified whole-program typed Core at phase `P`. +/// +/// There is intentionally no public `new` or `from_functions`: forgeable typed +/// nodes are valid verifier inputs, but only [`verify`] can mint this marker. +/// +/// ```compile_fail +/// use prism_core::core::{TypedCore, TypedCoreFn, TypedElaborated}; +/// let _ = TypedCore::::new(Vec::::new()); +/// ``` +/// +/// ```compile_fail +/// use prism_core::core::{TypedCore, TypedCoreFn, TypedElaborated}; +/// let _ = TypedCore::::from_functions(Vec::::new()); +/// ``` +/// +/// ```compile_fail +/// use prism_core::core::{TypedCoreFn, TypedElaborated, UncheckedTypedCore}; +/// let draft = UncheckedTypedCore::::new(Vec::::new()); +/// let _ = draft.erase(); +/// ``` +#[derive(Debug, PartialEq)] +pub struct TypedCore

{ + fns: Vec, + phase: PhantomData P>, +} + +// Manual so a phase-generic caller can clone without requiring `P: Clone`. +impl

Clone for TypedCore

{ + fn clone(&self) -> Self { + Self::from_verified(self.fns.clone()) + } +} + +impl

TypedCore

{ + const fn from_verified(fns: Vec) -> Self { + Self { + fns, + phase: PhantomData, + } + } + + /// Functions in deterministic program order. + #[must_use] + pub fn functions(&self) -> &[TypedCoreFn] { + &self.fns + } + + /// Consume the proof-bearing wrapper before transforming or regrouping it. + #[must_use] + pub fn into_unchecked(self) -> UncheckedTypedCore

{ + UncheckedTypedCore::new(self.fns) + } + + /// Consume all type/effect witnesses, yielding executable Core. + #[must_use] + pub fn erase(self) -> Core { + Core { + fns: self.fns.into_iter().map(TypedCoreFn::erase).collect(), + } + } +} + +/// Verify a complete assembly and mint its authoritative phase marker. +/// +/// `P` identifies the legal node vocabulary. The compiler transition that +/// produced the assembly remains the authority that the phase actually ran. +/// +/// # Errors +/// Every independently observed invalid scope, type, effect, handler, phase, +/// ownership, or reuse judgment. +pub fn verify( + core: UncheckedTypedCore

, + env: &VerifyEnv, +) -> Result, Vec> { + check_functions::

(core.functions(), env)?; + Ok(TypedCore::from_verified(core.into_functions())) +} + +/// Recheck an existing authoritative value without creating another minting +/// path. Primarily useful at assertions and cache boundaries. +/// +/// # Errors +/// Every independently observed invalid stored judgment. +pub fn audit( + core: &TypedCore

, + env: &VerifyEnv, +) -> Result<(), Vec> { + check_functions::

(core.functions(), env) +} diff --git a/crates/prism-core/src/core/typed/build.rs b/crates/prism-core/src/core/typed/build.rs deleted file mode 100644 index ba148a6c..00000000 --- a/crates/prism-core/src/core/typed/build.rs +++ /dev/null @@ -1,3293 +0,0 @@ -//! Typed builders at the elaboration boundary. -//! -//! The builder consumes the elaborator's executable Core as a compatibility -//! input, reconstructs witnesses from checked declaration schemes, verifies the -//! result, and erases at the typed-prefix boundary. No source inference is -//! called here. - -use std::collections::{BTreeMap, BTreeSet}; - -use prism_common::sym::Sym; -use prism_syntax::error::{Error, TypedCoreConstructionFailure, TypedCoreEnvironmentFailure}; -use prism_syntax::kw; -use prism_syntax::names::{self, IO_EFFECT}; - -use crate::core::builtins::Builtin; -use crate::core::CoreOp::{ - Add, Addf, Div, Divf, Eq, Eqf, Ge, Gef, Gt, Gtf, Le, Lef, Lt, Ltf, Mul, Mulf, Ne, Nef, Rem, - Sub, Subf, -}; -use crate::core::{CheckedHandler, Comp, Core, CoreOp, CorePat, IoOp, NegLane, Value}; -use crate::types::sig::parse_checked_signature; -use crate::types::ty::{EffRow, Kind, Label}; -use crate::types::{CtorInfo, EffOpInfo, Type}; - -use super::verify::{representation_preserving_stable, row_included}; -use super::{ - instantiate_constructor, instantiate_fn, instantiate_operation, scheme_to_fn_sig, CompSig, - ConstructorSig, CoreFnSig, CoreInstantiation, CoreQuantifier, CoreType, Elaborated, - LoweredType, OperationSig, TypedBinder, TypedComp, TypedCompKind, TypedCore, TypedCoreFn, - TypedForward, TypedHandleOp, TypedHandler, TypedPattern, TypedValue, TypedValueKind, VerifyEnv, -}; -use super::{CORE_GROW_STACK, CORE_MIN_STACK}; - -/// Translate a checked source function scheme to its Core calling convention. -/// -/// # Errors -/// A message naming the scheme, when what it peels down to is not a function -/// type and so has no calling convention. -pub fn core_fn_sig(scheme: &Type, prefix: Vec) -> Result { - let (quantifiers, body) = peel_quantifiers(scheme); - let Type::Fun(params, effects, result) = body else { - return Err(format!("expected function scheme, got {body:?}")); - }; - let mut lowered = prefix; - lowered.extend(params.iter().map(lower_value_type)); - Ok(normalize_core_sig(&CoreFnSig::new( - quantifiers, - lowered, - CompSig::new(lower_value_type(result), effects.clone()), - ))) -} - -// Inference may generalize a fresh ambient tail even when the body is pure -// (`forall e. () -> Int ! e`). Core records the effects the body actually -// performs. A row tail remains semantic when it is tied to a parameter/result -// (the usual higher-order forwarding case); a top-level-only tail is vacuous and -// is closed here together with its now-unused quantifier. -fn normalize_core_sig(sig: &CoreFnSig) -> CoreFnSig { - let escaping = escaping_effects(sig.body()); - let params = sig - .params() - .iter() - .map(|param| remove_escaping_label_contamination(param, &escaping)) - .collect(); - let sig = CoreFnSig::new(sig.quantifiers().to_vec(), params, sig.body().clone()); - let EffRow::Var(tail) = sig.body().effects().tail() else { - return sig; - }; - let tail = *tail; - let mut used = BTreeSet::new(); - for param in sig.params() { - core_row_vars(param, &mut used); - } - core_row_vars(sig.body().result(), &mut used); - for label in sig.body().effects().labels() { - for arg in &label.args { - arg.free_row_vars(&mut used); - } - } - if used.contains(&tail) { - return sig; - } - let effects = EffRow::canonical( - sig.body().effects().labels().into_iter().cloned(), - EffRow::Empty, - ); - CoreFnSig::new( - sig.quantifiers() - .iter() - .filter(|quantifier| !matches!(quantifier, CoreQuantifier::Row(name) if *name == tail)) - .cloned() - .collect(), - sig.params().to_vec(), - CompSig::new(sig.body().result().clone(), effects), - ) -} - -fn escaping_effects(signature: &CompSig) -> EffRow { - let mut labels: Vec

(core: TypedCore

) -> (TypedCore

, CseStats) { +pub fn cse

(core: UncheckedTypedCore

) -> (UncheckedTypedCore

, CseStats) { let mut eliminator = Cse { ticks: 0 }; let fns = core - .fns + .into_functions() .into_iter() .map(|function| { let body = eliminator.comp(&function.body, &Avail::new()); @@ -59,7 +59,7 @@ pub fn cse

(core: TypedCore

) -> (TypedCore

, CseStats) { }) .collect(); ( - TypedCore::new(fns), + UncheckedTypedCore::new(fns), CseStats { ticks: eliminator.ticks, }, @@ -308,8 +308,11 @@ mod tests { use crate::types::Type; use super::super::effect_lower::lower_effects; - use super::super::verify::{verify, OperationSig, VerifyEnv}; - use super::super::{CompSig, CoreFnSig, CoreType, EffectLowered, Elaborated, TypedLowering}; + use super::super::verify::{OperationSig, VerifyEnv}; + use super::super::{ + verify, CompSig, CoreFnSig, CoreType, EffectLowered, Elaborated, TypedCore, TypedLowering, + UncheckedTypedCore, + }; use super::*; fn sym(name: &str) -> Sym { @@ -339,18 +342,14 @@ mod tests { } fn run_cse(functions: Vec, env: &VerifyEnv) -> (TypedCore, u64) { - let input = TypedCore::new(functions); - if let Err(violations) = verify(&input, env) { - panic!("input fixture is invalid: {violations:#?}"); - } + let input = UncheckedTypedCore::new(functions); let (actual, stats) = cse(input); - if let Err(violations) = verify(&actual, env) { - panic!("CSE'd typed Core is invalid: {violations:#?}"); - } + let actual = verify(actual, env) + .unwrap_or_else(|violations| panic!("CSE'd typed Core is invalid: {violations:#?}")); (actual, stats.ticks()) } - fn lowered_cse_fixture() -> (TypedCore, VerifyEnv) { + fn lowered_cse_fixture() -> (UncheckedTypedCore, VerifyEnv) { let operation = sym("ask"); let effect = sym("Ask"); let mut env = VerifyEnv::new(); @@ -383,10 +382,9 @@ mod tests { ), 0, ); - let input = TypedCore::::new(vec![main]); - if let Err(violations) = verify(&input, &env) { - panic!("elaborated late-pass fixture is invalid: {violations:#?}"); - } + let input = verify(UncheckedTypedCore::::new(vec![main]), &env).unwrap_or_else( + |violations| panic!("elaborated late-pass fixture is invalid: {violations:#?}"), + ); let flags = DynFlags { effect_tier: EffectTier::FreeMonad, quiet: true, @@ -447,24 +445,18 @@ mod tests { ); let mut functions = lowered.functions().to_vec(); functions.push(cse_target); - let core = TypedCore::::new(functions); - if let Err(violations) = verify(&core, &env) { - panic!("effect-lowered late-pass fixture is invalid: {violations:#?}"); - } + let core = UncheckedTypedCore::::new(functions); (core, env) } fn run_lowered_cse( - input: TypedCore, + input: UncheckedTypedCore, env: &VerifyEnv, ) -> (TypedCore, u64) { - if let Err(violations) = verify(&input, env) { - panic!("effect-lowered CSE input is invalid: {violations:#?}"); - } let (actual, stats) = cse(input); - if let Err(violations) = verify(&actual, env) { - panic!("effect-lowered CSE output is invalid: {violations:#?}"); - } + let actual = verify(actual, env).unwrap_or_else(|violations| { + panic!("effect-lowered CSE output is invalid: {violations:#?}") + }); (actual, stats.ticks()) } diff --git a/crates/prism-core/src/core/typed/effect_lower/abi.rs b/crates/prism-core/src/core/typed/effect_lower/abi.rs index 0060425c..264173e9 100644 --- a/crates/prism-core/src/core/typed/effect_lower/abi.rs +++ b/crates/prism-core/src/core/typed/effect_lower/abi.rs @@ -663,9 +663,9 @@ pub fn qapply_fn() -> TypedCoreFn { #[cfg(test)] mod tests { - use super::super::super::{EffectLowered, Elaborated, TypedCore}; + use super::super::super::{verify, EffectLowered, Elaborated, UncheckedTypedCore}; use super::*; - use crate::core::typed::verify::verify; + use crate::core::typed::violation::Violation; use crate::core::IoOp; fn thunk(body: TypedComp, parameter: TypedBinder) -> TypedValue { @@ -861,9 +861,9 @@ mod tests { ) } - fn singleton

(body: TypedComp) -> TypedCore

{ + fn singleton

(body: TypedComp) -> UncheckedTypedCore

{ let signature = body.sig().clone(); - TypedCore::new(vec![TypedCoreFn::new( + UncheckedTypedCore::new(vec![TypedCoreFn::new( Sym::from("main"), Vec::new(), body, @@ -876,13 +876,20 @@ mod tests { fn runtime_templates_verify_under_the_phase_private_abi() { let mut env = VerifyEnv::new(); insert(&mut env); - let core = TypedCore::::new(vec![ebind_fn(), qapply_fn()]); - assert_eq!(verify(&core, &env), Ok(())); + let core = UncheckedTypedCore::::new(vec![ebind_fn(), qapply_fn()]); + assert!(verify(core, &env).is_ok()); } #[test] fn runtime_templates_erase_to_the_canonical_abi_names() { - let erased = TypedCore::::new(vec![ebind_fn(), qapply_fn()]).erase(); + let mut env = VerifyEnv::new(); + insert(&mut env); + let core = verify( + UncheckedTypedCore::::new(vec![ebind_fn(), qapply_fn()]), + &env, + ) + .expect("runtime templates verify"); + let erased = core.erase(); assert_eq!( erased .fns @@ -907,8 +914,11 @@ mod tests { pure(word()), TypedCompKind::Bind(Box::new(head), x, Box::new(tail)), ); + let mut env = VerifyEnv::new(); + insert(&mut env); let typed = singleton::(body); let (actual, stats) = super::super::super::simplify::simplify(typed).expect("simplifies"); + let actual = verify(actual, &env).expect("simplified runtime fixture verifies"); assert_eq!(stats.ticks(), 2); assert_eq!( actual.erase().fns[0].body, @@ -920,26 +930,30 @@ mod tests { fn heterogeneous_two_operation_chain_verifies() { let mut env = VerifyEnv::new(); insert(&mut env); - let core = - TypedCore::::new(vec![ebind_fn(), qapply_fn(), heterogeneous_fixture()]); - assert_eq!(verify(&core, &env), Ok(())); + let core = UncheckedTypedCore::::new(vec![ + ebind_fn(), + qapply_fn(), + heterogeneous_fixture(), + ]); + assert!(verify(core, &env).is_ok()); } #[test] fn resume_and_effectful_bounce_signatures_verify() { let mut env = VerifyEnv::new(); insert(&mut env); - let core = TypedCore::::new(remaining_constructor_fixtures()); - assert_eq!(verify(&core, &env), Ok(())); + let core = UncheckedTypedCore::::new(remaining_constructor_fixtures()); + assert!(verify(core, &env).is_ok()); } #[test] fn queue_ambient_row_cannot_be_forged_at_apply() { let mut env = VerifyEnv::new(); insert(&mut env); - let core = TypedCore::::new(vec![qapply_fn(), row_confusion_fixture()]); + let core = + UncheckedTypedCore::::new(vec![qapply_fn(), row_confusion_fixture()]); assert!( - verify(&core, &env).is_err(), + verify(core, &env).is_err(), "an IO-bearing queue must not typecheck at qApply" ); } @@ -960,7 +974,7 @@ mod tests { )), ); assert!( - verify(&singleton::(wrongly_pure), &env).is_err(), + verify(singleton::(wrongly_pure), &env).is_err(), "an IO-bearing bounce must not typecheck as Eff(Empty)" ); } @@ -971,12 +985,10 @@ mod tests { pure(word()), TypedCompKind::Return(lowered_repr(int(1), word())), ); - let errors = verify(&singleton::(body), &VerifyEnv::new()).unwrap_err(); - assert!(errors.iter().any(|error| { - error - .message() - .contains("lowered representation evidence is not legal") - })); + let errors = verify(singleton::(body), &VerifyEnv::new()).unwrap_err(); + assert!(errors + .iter() + .any(|error| matches!(error.kind(), Violation::LoweredAbiIllegal { .. }))); } #[test] @@ -988,10 +1000,10 @@ mod tests { eff(EffRow::Empty), )), ); - let errors = verify(&singleton::(body), &VerifyEnv::new()).unwrap_err(); + let errors = verify(singleton::(body), &VerifyEnv::new()).unwrap_err(); assert!(errors .iter() - .any(|error| error.message().contains("illegal lowered representation"))); + .any(|error| matches!(error.kind(), Violation::ReprConversionIllegal { .. }))); } #[test] @@ -1005,10 +1017,10 @@ mod tests { pure(word()), TypedCompKind::Return(lowered_repr(product, word())), ); - let errors = verify(&singleton::(body), &VerifyEnv::new()).unwrap_err(); + let errors = verify(singleton::(body), &VerifyEnv::new()).unwrap_err(); assert!(errors .iter() - .any(|error| error.message().contains("illegal lowered representation"))); + .any(|error| matches!(error.kind(), Violation::ReprConversionIllegal { .. }))); } #[test] @@ -1022,17 +1034,17 @@ mod tests { source(Type::Unit), )), ); - let core = TypedCore::::new(vec![TypedCoreFn::new( + let core = UncheckedTypedCore::::new(vec![TypedCoreFn::new( Sym::from("main"), vec![q], body, CoreFnSig::new(Vec::new(), vec![queue_ty], pure(source(Type::Unit))), 0, )]); - let errors = verify(&core, &VerifyEnv::new()).unwrap_err(); + let errors = verify(core, &VerifyEnv::new()).unwrap_err(); assert!(errors .iter() - .any(|error| error.message().contains("illegal lowered representation"))); + .any(|error| matches!(error.kind(), Violation::ReprConversionIllegal { .. }))); } #[test] @@ -1041,7 +1053,7 @@ mod tests { let parameter = binder("queue_word@", queue_ty.clone()); let generic_pack = lowered_repr(var("queue_word@", queue_ty.clone()), word()); let generic_body = TypedComp::new(pure(word()), TypedCompKind::Return(generic_pack)); - let generic = TypedCore::::new(vec![TypedCoreFn::new( + let generic = UncheckedTypedCore::::new(vec![TypedCoreFn::new( Sym::from("generic_queue_pack@"), vec![parameter.clone()], generic_body, @@ -1049,21 +1061,21 @@ mod tests { 0, )]); assert!( - verify(&generic, &VerifyEnv::new()).is_err(), + verify(generic, &VerifyEnv::new()).is_err(), "the general source-word evidence must not pack a runtime queue" ); let packed = pack_queue_word(var("queue_word@", queue_ty.clone())).expect("queue packs"); let restored = unpack_queue_word(packed, EffRow::Empty).expect("queue unpacks"); let body = TypedComp::new(pure(queue_ty.clone()), TypedCompKind::Return(restored)); - let typed = TypedCore::::new(vec![TypedCoreFn::new( + let typed = UncheckedTypedCore::::new(vec![TypedCoreFn::new( Sym::from("queue_roundtrip@"), vec![parameter], body, CoreFnSig::new(Vec::new(), vec![queue_ty], pure(queue(EffRow::Empty))), 0, )]); - assert_eq!(verify(&typed, &VerifyEnv::new()), Ok(())); + let typed = verify(typed, &VerifyEnv::new()).expect("sealed queue roundtrip verifies"); assert!(matches!( typed.erase().fns[0].body, crate::core::cbpv::Comp::Return(crate::core::cbpv::Value::Var(name)) @@ -1079,17 +1091,17 @@ mod tests { pure(word()), TypedCompKind::Return(lowered_repr(var("token@", token_ty.clone()), word())), ); - let core = TypedCore::::new(vec![TypedCoreFn::new( + let core = UncheckedTypedCore::::new(vec![TypedCoreFn::new( Sym::from("main"), vec![token], body, CoreFnSig::new(Vec::new(), vec![token_ty], pure(word())), 0, )]); - let errors = verify(&core, &VerifyEnv::new()).unwrap_err(); + let errors = verify(core, &VerifyEnv::new()).unwrap_err(); assert!(errors .iter() - .any(|error| error.message().contains("illegal lowered representation"))); + .any(|error| matches!(error.kind(), Violation::ReprConversionIllegal { .. }))); } #[test] @@ -1108,7 +1120,7 @@ mod tests { let bridge = try_word_bridge(var("bridge_source@", actual.clone()), expected.clone()) .expect("both function witnesses have one runtime-word representation"); let body = TypedComp::new(pure(expected.clone()), TypedCompKind::Return(bridge)); - let core = TypedCore::::new(vec![TypedCoreFn::new( + let core = UncheckedTypedCore::::new(vec![TypedCoreFn::new( Sym::from("main"), vec![parameter], body, @@ -1116,7 +1128,7 @@ mod tests { 0, )]); - assert_eq!(verify(&core, &VerifyEnv::new()), Ok(())); + let core = verify(core, &VerifyEnv::new()).expect("word bridge verifies"); assert_eq!( core.erase().fns[0].body, crate::core::cbpv::Comp::Return(crate::core::cbpv::Value::Var(Sym::from( diff --git a/crates/prism-core/src/core/typed/effect_lower/analysis.rs b/crates/prism-core/src/core/typed/effect_lower/analysis.rs index 41c1a650..35dd1408 100644 --- a/crates/prism-core/src/core/typed/effect_lower/analysis.rs +++ b/crates/prism-core/src/core/typed/effect_lower/analysis.rs @@ -1649,7 +1649,7 @@ mod judgment_tests { ); let whole = vec![function(&escaped)]; let (_, whole_plan) = planned(&whole); - // Escaping, not merely captured: the thunk is returned to a caller the + // The thunk escapes by being returned to a caller the // program does not name, so no signature describes what forcing it // performs and the region cannot reach through it. This is the // classification the confinement flip deliberately left alone. diff --git a/crates/prism-core/src/core/typed/effect_lower/arena.rs b/crates/prism-core/src/core/typed/effect_lower/arena.rs index 5a88971d..2e8f01ed 100644 --- a/crates/prism-core/src/core/typed/effect_lower/arena.rs +++ b/crates/prism-core/src/core/typed/effect_lower/arena.rs @@ -1,45 +1,32 @@ //! Typed scope-directed arena lowering: the `Elaborated -> ArenaPrepared` //! transition. //! -//! A constructor built under a `with_arena` scope becomes a performed -//! allocation plus an in-place initialization: +//! Constructors built under `with_arena` become allocations followed by in-place +//! initialization: //! //! ```text //! let cell = alloc(|fields|) in init_at(cell, Ctor(C, fields)) //! ``` //! -//! Reachability decides which code is "under an arena" as -//! `arena_only = arena_reachable \ otherwise_reachable` over the direct call -//! graph. +//! The direct call graph defines `arena_only` as +//! `arena_reachable \ otherwise_reachable`. //! -//! ## What the witnesses add +//! The rewrite introduces `alloc`, invalidating the affected rows from +//! elaboration. This phase rewrites terms and re-establishes those witnesses. //! -//! The rewrite *introduces* an effect: a function that only built a constructor -//! now performs `alloc`, so its row is no longer the one elaboration proved. -//! Re-establishing the invalidated witnesses is the whole reason this is its own -//! phase rather than a licence to admit `InitAt` in elaborated Core. -//! -//! Two propagations, and the distinction between them is the crux: +//! Terms and rows propagate differently: //! //! - **Terms** are rewritten only in `arena_only` functions, and never inside a //! thunk because a closure's layout is not `init_at`-shaped. -//! - **Rows** widen wherever the new operation became reachable, which includes -//! functions that were never rewritten. `main` never allocates, yet the thunk -//! it hands to `with_arena` now suspends a computation that does, so that -//! thunk's *witness* gains the label while `main`'s own row does not. +//! - **Rows** widen wherever the new operation becomes reachable, including +//! unchanged functions. A thunk passed to `with_arena` gains the label even +//! when its enclosing function does not. //! -//! The widening is additive and local: `Widen` adds the one label exactly where -//! a node's checking rule now derives it, and preserves every other row as -//! elaboration wrote it. `Return` is the invariant keeping this honest: it is -//! pure by rule, so it never gains the label however effectful the computation it -//! suspends, and a uniform "add the label everywhere" pass would be rejected for -//! precisely that reason. +//! `Widen` adds the label only where a node's checking rule derives it. `Return` +//! remains pure even when it suspends an effectful computation. //! -//! The frontier is the thunk passed to `with_arena`, whose declared type already -//! reads `() -> a ! {Alloc}`; widening reaches it and stops, which is why the -//! label never escapes into `main`. A program where it would escape is one whose -//! `alloc` the reachability placed outside every handler that discharges it; it -//! is rejected here rather than lowered. +//! Widening stops at the `with_arena` thunk, whose type already includes `Alloc`. +//! Programs whose `alloc` escapes all matching handlers are rejected. use std::collections::{BTreeMap, BTreeSet}; use std::{iter, ptr}; @@ -53,10 +40,11 @@ use prism_syntax::error::TypedCoreEffectLoweringFailure; use prism_syntax::names::{self, ALLOC_OP, ENTRY_POINT}; use super::super::specialize_support::Rewrite; -use super::super::verify::{verify, VerifyEnv}; +use super::super::verify::VerifyEnv; use super::super::{ - ArenaPrepared, CompSig, CoreFnSig, CoreInstantiation, CoreQuantifier, CoreType, TypedBinder, - TypedComp, TypedCompKind, TypedCore, TypedCoreFn, TypedValue, TypedValueKind, + verify, ArenaPrepared, CompSig, CoreFnSig, CoreInstantiation, CoreQuantifier, CoreType, + TypedBinder, TypedComp, TypedCompKind, TypedCore, TypedCoreFn, TypedValue, TypedValueKind, + UncheckedTypedCore, }; use super::peel; use super::walk::{each_subcomp, each_value}; @@ -151,6 +139,7 @@ pub fn prepare( alloc: &alloc, gains: &gains, fresh: Fresh::new(), + locals: BTreeMap::new(), }; let fns = fns .iter() @@ -180,9 +169,8 @@ fn finish( fns: Vec, env: &VerifyEnv, ) -> Result, TypedCoreEffectLoweringFailure> { - let out = TypedCore::::new(fns); - match verify(&out, env) { - Ok(()) => Ok(out), + match verify(UncheckedTypedCore::::new(fns), env) { + Ok(out) => Ok(out), Err(violations) => Err(TypedCoreEffectLoweringFailure::Verification { first: violations .first() @@ -604,6 +592,43 @@ impl Alloc { } } + /// Carry the introduced label from a rewritten operand into the type a + /// representation-preserving coercion converts it to. + /// + /// Elaboration inserts such a coercion wherever a value's own witness and + /// the witness its position expects differ only by a row relabelling. The + /// rewrite widens the operand, and the target must follow it in the same + /// positions or the coercion claims a purity the operand no longer has, + /// which is a laundering the verifier refuses. Only the label this pass + /// introduces moves, and only where the operand carries it; every other + /// part of the target is left exactly as elaboration wrote it. + fn widen_like(&self, target: &CoreType, operand: &CoreType) -> CoreType { + match (target, operand) { + (CoreType::Thunk(target), CoreType::Thunk(operand)) => { + CoreType::Thunk(Box::new(self.widen_sig_like(target, operand))) + } + (CoreType::Function(target), CoreType::Function(operand)) + if target.params().len() == operand.params().len() => + { + CoreType::Function(Box::new(CoreFnSig::new( + target.quantifiers().to_vec(), + target.params().to_vec(), + self.widen_sig_like(target.body(), operand.body()), + ))) + } + _ => target.clone(), + } + } + + fn widen_sig_like(&self, target: &CompSig, operand: &CompSig) -> CompSig { + let effects = if self.present(operand.effects()) { + self.widen(target.effects()) + } else { + target.effects().clone() + }; + CompSig::new(self.widen_like(target.result(), operand.result()), effects) + } + /// Whether a closure witness now abstracts the operation. fn in_function(&self, ty: &CoreType) -> bool { match ty { @@ -625,6 +650,14 @@ struct Widen<'a> { alloc: &'a Alloc, gains: &'a BTreeSet, fresh: Fresh, + /// The rewritten type of each `let`-bound name currently in scope. + /// + /// A binder holds what its computation returns, so a bound closure whose + /// body now allocates changes type at the binder. Every reference to it + /// must change with it, and a reference is visited long after the binder + /// that gave it its type, so the new type is carried down here rather than + /// discovered at the occurrence. + locals: BTreeMap, } impl Rewrite for Widen<'_> { @@ -635,12 +668,33 @@ impl Rewrite for Widen<'_> { /// out of a rewritten function and into the closure a caller passes around, /// which is how it reaches `with_arena`'s parameter without ever entering the /// row of the function that builds the thunk. + /// + /// A coercion's target follows its operand for the same reason: a let-bound + /// closure whose body allocates reaches its binder through a + /// representation-preserving relabelling, and a target left at the row + /// elaboration wrote would coerce the widened closure back to a pure one. fn value(&mut self, value: &TypedValue, cx: &Cx) -> TypedValue { let out = self.descend_value(value, cx); match &out.kind { TypedValueKind::Thunk(body) => { TypedValue::new(CoreType::Thunk(Box::new(body.sig().clone())), out.kind) } + TypedValueKind::Reinterpret(operand) => { + let ty = self.alloc.widen_like(out.ty(), operand.ty()); + TypedValue::new(ty, out.kind) + } + // A reference is witnessed by its binder, so one naming a binder + // this pass retyped follows it. Only the introduced label moves: + // the occurrence keeps whatever instantiation elaboration wrote, + // which is why the binder's type is carried into the stored one + // rather than replacing it. + TypedValueKind::Var { name, .. } => match self.locals.get(name) { + Some(bound) => { + let ty = self.alloc.widen_like(out.ty(), bound); + TypedValue::new(ty, out.kind) + } + None => out, + }, _ => out, } } @@ -659,7 +713,10 @@ impl Rewrite for Widen<'_> { rewriting: cx.rewriting && !nested_alloc_handler(comp), installer: cx.installer, }; - let out = self.descend_comp(comp, &inner); + let out = match comp.kind() { + TypedCompKind::Bind(..) => self.bind(comp, &inner), + _ => self.descend_comp(comp, &inner), + }; let out = self.retype(out); // Each alloc-handling `Handle` in an installer is one region activation: // bracket it with the runtime enter/exit hooks. @@ -791,6 +848,33 @@ impl Widen<'_> { ) } + /// Rewrite a `Bind`, binding the name to what its rewritten computation + /// returns before the body that reads it is visited. + /// + /// A binder holds exactly what its computation returns, so a binder whose + /// computation was rewritten follows it, and this is the one place that + /// happens. Correcting it after the fact instead would leave every + /// reference inside the body witnessed by a type the binder no longer has, + /// which the verifier refuses; ordering the two is the whole point of + /// handling this form here rather than in the generic descent. + fn bind(&mut self, comp: &TypedComp, cx: &Cx) -> TypedComp { + let TypedCompKind::Bind(first, binder, rest) = comp.kind() else { + unreachable!("bind called on a non-Bind computation") + }; + let first = self.comp(first, cx); + let binder = TypedBinder::new(binder.name(), first.sig().result().clone()); + let shadowed = self.locals.insert(binder.name(), binder.ty().clone()); + let rest = self.comp(rest, cx); + match shadowed { + Some(ty) => self.locals.insert(binder.name(), ty), + None => self.locals.remove(&binder.name()), + }; + TypedComp::new( + comp.sig().clone(), + TypedCompKind::Bind(Box::new(first), binder, Box::new(rest)), + ) + } + /// Re-establish one node's signature from its rewritten children. /// /// Only the introduced label moves. A node gains it exactly when its own @@ -832,7 +916,6 @@ impl Widen<'_> { sig.effects().clone() }; let result = self.result_for(&sig, &kind); - let kind = Self::rebind(kind); TypedComp::new(CompSig::new(result, effects), kind) } @@ -865,16 +948,4 @@ impl Widen<'_> { _ => sig.result().clone(), } } - - /// A bind's binder holds exactly what the bound computation returns, so a - /// binder whose computation was rewritten must follow it. - fn rebind(kind: TypedCompKind) -> TypedCompKind { - match kind { - TypedCompKind::Bind(m, x, n) => { - let x = TypedBinder::new(x.name(), m.sig().result().clone()); - TypedCompKind::Bind(m, x, n) - } - other => other, - } - } } diff --git a/crates/prism-core/src/core/typed/effect_lower/checks.rs b/crates/prism-core/src/core/typed/effect_lower/checks.rs index 0aca09a6..352771f6 100644 --- a/crates/prism-core/src/core/typed/effect_lower/checks.rs +++ b/crates/prism-core/src/core/typed/effect_lower/checks.rs @@ -27,8 +27,8 @@ pub(crate) enum ThunkRule { /// direct callers before this rail runs, which is also why a direct declaration /// may call one. /// -/// Under [`ThunkRule::PerThunk`] every declaration is walked, not just the -/// members: a declaration outside the region is exactly where a thunk left at +/// Under [`ThunkRule::PerThunk`] every declaration is walked. A declaration +/// outside the region is exactly where a thunk left at /// the direct convention can be found, and the mistake worth catching is such a /// thunk reaching code that answers with an effect cell. A thunk carries no /// type-level mark of its convention, so the convention is read back off the @@ -76,8 +76,8 @@ fn suspends_effect_cell(thunk: &TypedComp) -> bool { } /// A thunk left at the direct convention is copied verbatim into the output, so -/// it must not reach the other convention anywhere in its body, not merely in -/// its tail: a member call buried mid-body answers with an effect cell the +/// it must not reach the other convention anywhere in its body. A member call +/// buried mid-body answers with an effect cell the /// direct code around it would consume as an ordinary result. /// /// Nested thunks are not descended into. Each is a site of its own with its own diff --git a/crates/prism-core/src/core/typed/effect_lower/convention.rs b/crates/prism-core/src/core/typed/effect_lower/convention.rs new file mode 100644 index 00000000..5953fa16 --- /dev/null +++ b/crates/prism-core/src/core/typed/effect_lower/convention.rs @@ -0,0 +1,730 @@ +//! Context splitting for thunk-valued parameters. +//! +//! A named higher-order function has one symbol, so the ordinary flow analysis +//! joins every thunk passed to one parameter slot. If one call passes a direct +//! thunk and another passes an effectful thunk, that join gives both calls one +//! runtime convention and can widen an otherwise pure hot path to the whole +//! free-monad program. This pass gives statically known demand instances +//! distinct symbols before the canonical effect plan is solved. +//! +//! Clones keep the source scheme and every witness byte-for-byte. Only direct +//! call heads change, so this pass never invents a representation coercion or +//! specializes a type/effect quantifier. Unknown values and dynamic calls stay +//! on the original symbol and therefore retain the conservative fallback. + +use std::collections::{BTreeMap, BTreeSet}; + +use prism_common::sym::Sym; +use prism_syntax::names::{self, ENTRY_POINT}; + +use super::super::specialize_support::Rewrite; +use super::super::verify::VerifyEnv; +use super::super::{ + verify, ArenaPrepared, CoreType, TypedBinder, TypedComp, TypedCompKind, TypedCore, TypedCoreFn, + TypedHandleOp, TypedHandler, TypedPattern, TypedValue, TypedValueKind, UncheckedTypedCore, +}; +use super::flow::{self, Sig, ThunkFlow}; +use super::latent::Latent; +use prism_syntax::error::TypedCoreEffectLoweringFailure; + +// These are compile-resource rails, not semantic limits. Crossing either one +// leaves the already verified input untouched and lets the ordinary conservative +// lowering choose its wider tier. +const MAX_INSTANCES: usize = 256; +const MAX_INSTANCES_PER_FUNCTION: usize = 16; + +#[derive(Clone, Debug, PartialEq, Eq)] +enum Demand { + Known(Sig), + Unknown, +} + +impl Demand { + const fn pure() -> Self { + Self::Known(Sig::new()) + } + + fn join(&self, other: &Self) -> Self { + match (self, other) { + (Self::Known(left), Self::Known(right)) => { + let mut joined = left.clone(); + joined.extend(right.iter().copied()); + Self::Known(joined) + } + _ => Self::Unknown, + } + } +} + +type Loc = BTreeMap; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +struct Instance { + function: Sym, + key: Vec, +} + +struct Facts<'a> { + functions: BTreeMap, + latent: &'a Latent, + joined: &'a ThunkFlow, +} + +impl<'a> Facts<'a> { + fn new(functions: &'a [TypedCoreFn], latent: &'a Latent, joined: &'a ThunkFlow) -> Self { + Self { + functions: functions + .iter() + .map(|function| (function.name(), function)) + .collect(), + latent, + joined, + } + } + + fn joined_loc(&self, function: &TypedCoreFn) -> Loc { + function + .params() + .iter() + .enumerate() + .map(|(index, binder)| { + let demand = if thunk_slot(binder.ty()) { + self.joined + .param + .get(&function.name()) + .and_then(|slots| slots.get(index)) + .cloned() + .map_or(Demand::Unknown, Demand::Known) + } else { + Demand::pure() + }; + (binder.name(), demand) + }) + .collect() + } + + fn instance_loc(function: &TypedCoreFn, key: &[Sig]) -> Loc { + function + .params() + .iter() + .enumerate() + .map(|(index, binder)| { + let demand = if thunk_slot(binder.ty()) { + key.get(index) + .cloned() + .map_or(Demand::Unknown, Demand::Known) + } else { + Demand::pure() + }; + (binder.name(), demand) + }) + .collect() + } + + fn value_demand(&self, value: &TypedValue, loc: &Loc) -> Demand { + match super::peel(value).kind() { + TypedValueKind::Thunk(body) => Demand::Known(flow::body_sig(body, self.latent)), + TypedValueKind::Var { name, .. } => loc.get(name).cloned().unwrap_or(Demand::Unknown), + _ => Demand::Unknown, + } + } + + fn call_instance(&self, callee: Sym, args: &[TypedValue], loc: &Loc) -> Option { + // The control erasure recognizes bare `repeat_while`/`forever` spines + // and consumes them into monomorphic drivers before any tier is + // chosen. A clone would hide the spine behind a fresh symbol and keep + // the loop's control ops alive into strategy selection, so the loop + // drivers always stay on their original names. + if super::erase_control::is_loop_driver(callee) { + return None; + } + let declaration = self.functions.get(&callee)?; + let joined = self.joined.param.get(&callee)?; + if args.len() != declaration.sig().params().len() || joined.len() != args.len() { + return None; + } + let mut key = Vec::with_capacity(args.len()); + for ((argument, parameter), joined_slot) in + args.iter().zip(declaration.sig().params()).zip(joined) + { + let signature = if thunk_slot(parameter) { + let Demand::Known(signature) = self.value_demand(argument, loc) else { + return None; + }; + signature + } else { + Sig::new() + }; + // The context-sensitive demand must be a refinement of the global + // least-fixpoint join. If it is not, retain the original symbol; + // manufacturing a clone from contradictory analyses would be an + // unsound narrowing. + if !signature.is_subset(joined_slot) { + return None; + } + key.push(signature); + } + (key != *joined).then_some(Instance { + function: callee, + key, + }) + } + + fn call_result( + &self, + callee: Sym, + args: &[TypedValue], + loc: &Loc, + returns: &BTreeMap, + requested: &mut BTreeSet, + ) -> Demand { + if let Some(instance) = self.call_instance(callee, args, loc) { + requested.insert(instance.clone()); + return returns.get(&instance).cloned().unwrap_or_else(Demand::pure); + } + self.joined + .ret + .get(&callee) + .cloned() + .map_or(Demand::Unknown, Demand::Known) + } + + fn result_demand( + &self, + comp: &TypedComp, + loc: &Loc, + returns: &BTreeMap, + requested: &mut BTreeSet, + ) -> Demand { + match comp.kind() { + TypedCompKind::Return(value) => { + self.scan_value(value, loc, returns, requested); + self.value_demand(value, loc) + } + TypedCompKind::Call { callee, args, .. } => { + for argument in args { + self.scan_value(argument, loc, returns, requested); + } + self.call_result(*callee, args, loc, returns, requested) + } + TypedCompKind::Bind(first, binder, rest) => { + let first_result = self.result_demand(first, loc, returns, requested); + let mut next = loc.clone(); + next.insert( + binder.name(), + if thunk_slot(binder.ty()) { + first_result + } else { + Demand::pure() + }, + ); + self.result_demand(rest, &next, returns, requested) + } + TypedCompKind::If(condition, yes, no) => { + self.scan_value(condition, loc, returns, requested); + let yes = self.result_demand(yes, loc, returns, requested); + let no = self.result_demand(no, loc, returns, requested); + yes.join(&no) + } + TypedCompKind::Case(scrutinee, arms) => { + self.scan_value(scrutinee, loc, returns, requested); + let mut demand = Demand::pure(); + for (pattern, body) in arms { + let mut next = loc.clone(); + forget_pattern(pattern, &mut next); + demand = demand.join(&self.result_demand(body, &next, returns, requested)); + } + demand + } + TypedCompKind::Lam(params, body) => { + let mut next = loc.clone(); + forget_binders(params, &mut next); + self.result_demand(body, &next, returns, requested); + demand_for_result(comp.sig().result()) + } + TypedCompKind::App { callee, args, .. } => { + self.result_demand(callee, loc, returns, requested); + for argument in args { + self.scan_value(argument, loc, returns, requested); + } + demand_for_result(comp.sig().result()) + } + TypedCompKind::Mask(_, body) | TypedCompKind::WithReuse { body, .. } => { + self.result_demand(body, loc, returns, requested) + } + TypedCompKind::Handle { + body, + return_binder, + return_body, + ops, + } => { + self.result_demand(body, loc, returns, requested); + if let Some(return_body) = return_body { + let mut next = loc.clone(); + if let Some(binder) = return_binder { + forget_binder(binder, &mut next); + } + self.result_demand(return_body, &next, returns, requested); + } + for arm in ops.arms() { + let mut next = loc.clone(); + forget_binders(arm.params(), &mut next); + forget_binder(arm.resume(), &mut next); + self.result_demand(arm.body(), &next, returns, requested); + } + demand_for_result(comp.sig().result()) + } + _ => { + super::walk::each_value(comp, &mut |value| { + self.scan_value(value, loc, returns, requested); + }); + demand_for_result(comp.sig().result()) + } + } + } + + fn scan_value( + &self, + value: &TypedValue, + loc: &Loc, + returns: &BTreeMap, + requested: &mut BTreeSet, + ) { + match super::peel(value).kind() { + TypedValueKind::Thunk(body) => { + if let TypedCompKind::Lam(params, inner) = body.kind() { + let mut next = loc.clone(); + forget_binders(params, &mut next); + self.result_demand(inner, &next, returns, requested); + } else { + self.result_demand(body, loc, returns, requested); + } + } + TypedValueKind::Ctor { fields, .. } + | TypedValueKind::Tuple(fields) + | TypedValueKind::UnboxedTuple(fields) => { + for field in fields { + self.scan_value(field, loc, returns, requested); + } + } + TypedValueKind::UnboxedRecord(fields) => { + for (_, field) in fields { + self.scan_value(field, loc, returns, requested); + } + } + _ => {} + } + } +} + +const fn thunk_slot(ty: &CoreType) -> bool { + matches!(ty, CoreType::Thunk(_)) +} + +const fn demand_for_result(ty: &CoreType) -> Demand { + if thunk_slot(ty) { + Demand::Unknown + } else { + Demand::pure() + } +} + +fn forget_binder(binder: &TypedBinder, loc: &mut Loc) { + loc.insert( + binder.name(), + if thunk_slot(binder.ty()) { + Demand::Unknown + } else { + Demand::pure() + }, + ); +} + +fn forget_binders(binders: &[TypedBinder], loc: &mut Loc) { + for binder in binders { + forget_binder(binder, loc); + } +} + +fn forget_pattern(pattern: &TypedPattern, loc: &mut Loc) { + match pattern { + TypedPattern::Wild => {} + TypedPattern::Var(binder) => forget_binder(binder, loc), + TypedPattern::Ctor { fields, .. } | TypedPattern::Tuple(fields) => { + for binder in fields.iter().flatten() { + forget_binder(binder, loc); + } + } + } +} + +fn within_budget(instances: &BTreeSet) -> bool { + if instances.len() > MAX_INSTANCES { + return false; + } + let mut per_function = BTreeMap::::new(); + for instance in instances { + let count = per_function.entry(instance.function).or_default(); + *count += 1; + if *count > MAX_INSTANCES_PER_FUNCTION { + return false; + } + } + true +} + +// Discover every statically known demand instance and its returned-thunk +// signature before interning a clone name or changing a call head. Return +// `None` on a resource cap: the caller then returns its verified input intact. +fn discover(facts: &Facts<'_>, functions: &[TypedCoreFn]) -> Option> { + let mut returns = BTreeMap::::new(); + loop { + let mut requested = BTreeSet::new(); + for function in functions { + let loc = facts.joined_loc(function); + facts.result_demand(function.body(), &loc, &returns, &mut requested); + } + + let known: Vec = returns.keys().cloned().collect(); + let mut updates = Vec::with_capacity(known.len()); + for instance in known { + let function = facts.functions.get(&instance.function)?; + let loc = Facts::instance_loc(function, &instance.key); + let result = facts.result_demand(function.body(), &loc, &returns, &mut requested); + updates.push((instance, result)); + } + + // `requested` is rebuilt by the current traversal. Include already + // admitted keys as well so a future traversal-policy refinement cannot + // accidentally make the transactional budget forget an earlier SCC. + let all_instances: BTreeSet<_> = requested + .iter() + .cloned() + .chain(returns.keys().cloned()) + .collect(); + if !within_budget(&all_instances) { + return None; + } + let mut changed = false; + for instance in requested { + if let std::collections::btree_map::Entry::Vacant(slot) = returns.entry(instance) { + slot.insert(Demand::pure()); + changed = true; + } + } + for (instance, result) in updates { + let slot = returns.get_mut(&instance)?; + let joined = slot.join(&result); + if *slot != joined { + *slot = joined; + changed = true; + } + } + if !changed { + return Some(returns); + } + } +} + +fn assign_names( + instances: &BTreeMap, + functions: &[TypedCoreFn], +) -> BTreeMap { + let mut ordered: Vec<_> = instances.keys().cloned().collect(); + ordered.sort_by(|left, right| { + left.function + .as_str() + .cmp(right.function.as_str()) + .then_with(|| left.key.cmp(&right.key)) + }); + let mut occupied: BTreeSet = functions + .iter() + .map(|function| function.name().as_str().to_owned()) + .collect(); + let mut next = 0usize; + let mut spellings = Vec::with_capacity(ordered.len()); + for instance in &ordered { + loop { + next += 1; + let candidate = names::convention_clone(instance.function.as_str(), next); + if occupied.insert(candidate.clone()) { + spellings.push(candidate); + break; + } + } + } + // Intern only after discovery and cap checks have completed, so a declined + // attempt cannot perturb the compilation-global symbol table. + ordered + .into_iter() + .zip(spellings) + .map(|(instance, spelling)| (instance, Sym::from(&spelling))) + .collect() +} + +struct Rewriter<'a> { + facts: Facts<'a>, + returns: &'a BTreeMap, + names: &'a BTreeMap, +} + +impl Rewriter<'_> { + fn target(&self, callee: Sym, args: &[TypedValue], loc: &Loc) -> Sym { + self.facts + .call_instance(callee, args, loc) + .and_then(|instance| self.names.get(&instance).copied()) + .unwrap_or(callee) + } + + fn result(&self, comp: &TypedComp, loc: &Loc) -> Demand { + let mut ignored = BTreeSet::new(); + self.facts + .result_demand(comp, loc, self.returns, &mut ignored) + } + + fn rewritten_function(&mut self, source: &TypedCoreFn, name: Sym, loc: &Loc) -> TypedCoreFn { + TypedCoreFn::new( + name, + source.params().to_vec(), + self.comp(source.body(), loc), + source.sig().clone(), + source.dict_arity(), + ) + } +} + +impl Rewrite for Rewriter<'_> { + type Ctx = Loc; + + fn comp(&mut self, comp: &TypedComp, loc: &Loc) -> TypedComp { + match comp.kind() { + TypedCompKind::Call { + callee, + instantiation, + args, + } => TypedComp::new( + comp.sig().clone(), + TypedCompKind::Call { + callee: self.target(*callee, args, loc), + instantiation: instantiation.clone(), + args: args + .iter() + .map(|argument| self.value(argument, loc)) + .collect(), + }, + ), + TypedCompKind::Bind(first, binder, rest) => { + let first_result = self.result(first, loc); + let first = self.comp(first, loc); + let mut next = loc.clone(); + next.insert( + binder.name(), + if thunk_slot(binder.ty()) { + first_result + } else { + Demand::pure() + }, + ); + TypedComp::new( + comp.sig().clone(), + TypedCompKind::Bind( + Box::new(first), + binder.clone(), + Box::new(self.comp(rest, &next)), + ), + ) + } + TypedCompKind::Lam(params, body) => { + let mut next = loc.clone(); + forget_binders(params, &mut next); + TypedComp::new( + comp.sig().clone(), + TypedCompKind::Lam(params.clone(), Box::new(self.comp(body, &next))), + ) + } + TypedCompKind::Case(scrutinee, arms) => { + let scrutinee = self.value(scrutinee, loc); + let arms = arms + .iter() + .map(|(pattern, body)| { + let mut next = loc.clone(); + forget_pattern(pattern, &mut next); + (pattern.clone(), self.comp(body, &next)) + }) + .collect(); + TypedComp::new(comp.sig().clone(), TypedCompKind::Case(scrutinee, arms)) + } + TypedCompKind::Handle { + body, + return_binder, + return_body, + ops, + } => { + let body = Box::new(self.comp(body, loc)); + let return_body = return_body.as_ref().map(|body| { + let mut next = loc.clone(); + if let Some(binder) = return_binder { + forget_binder(binder, &mut next); + } + Box::new(self.comp(body, &next)) + }); + let arms = ops + .arms() + .iter() + .map(|arm| { + let mut next = loc.clone(); + forget_binders(arm.params(), &mut next); + forget_binder(arm.resume(), &mut next); + TypedHandleOp::new( + arm.name(), + arm.instantiation().to_vec(), + arm.params().to_vec(), + arm.resume().clone(), + self.comp(arm.body(), &next), + ) + }) + .collect(); + let handler = TypedHandler::new(arms) + .expect("verified handler operation names remain unique") + .with_forwarded(ops.forwarded().to_vec()); + TypedComp::new( + comp.sig().clone(), + TypedCompKind::Handle { + body, + return_binder: return_binder.clone(), + return_body, + ops: handler, + }, + ) + } + _ => self.descend_comp(comp, loc), + } + } +} + +/// Split statically known thunk-demand instances and return a freshly verified +/// `ArenaPrepared` program. A resource-cap decline returns `core` unchanged. +pub(super) fn split( + core: TypedCore, + env: &VerifyEnv, +) -> Result, TypedCoreEffectLoweringFailure> { + let functions = core.functions(); + let latent = super::latent::latent_map(functions); + let joined = flow::analyze(functions, &latent); + let facts = Facts::new(functions, &latent, &joined); + let Some(instances) = discover(&facts, functions) else { + return Ok(core); + }; + if instances.is_empty() { + return Ok(core); + } + let names = assign_names(&instances, functions); + let mut rewriter = Rewriter { + facts, + returns: &instances, + names: &names, + }; + let mut output: Vec = functions + .iter() + .map(|function| { + let loc = rewriter.facts.joined_loc(function); + rewriter.rewritten_function(function, function.name(), &loc) + }) + .collect(); + let ordered_instances: Vec<_> = names.keys().cloned().collect(); + for instance in ordered_instances { + let source = rewriter.facts.functions[&instance.function]; + let loc = Facts::instance_loc(source, &instance.key); + output.push(rewriter.rewritten_function(source, names[&instance], &loc)); + } + + if output + .iter() + .any(|function| function.name().as_str() == ENTRY_POINT) + { + let live = super::reachable(&output); + output.retain(|function| live.contains(&function.name())); + } + verify(UncheckedTypedCore::::new(output), env).map_err(|violations| { + TypedCoreEffectLoweringFailure::Verification { + first: violations + .first() + .map_or_else(String::new, ToString::to_string), + count: violations.len(), + } + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::typed::effect_lower::fixtures; + use crate::core::typed::effect_lower::latent::MaskOp; + + fn joined_flow(functions: &[TypedCoreFn]) -> ThunkFlow { + let mut effectful = Sig::new(); + effectful.insert(MaskOp { + id: Sym::from(fixtures::ASK_OP), + depth: 0, + }); + ThunkFlow { + ret: functions + .iter() + .map(|function| (function.name(), Sig::new())) + .collect(), + param: functions + .iter() + .map(|function| { + let slots = if function.name().as_str() == fixtures::RUN { + vec![effectful.clone()] + } else { + vec![Sig::new(); function.params().len()] + }; + (function.name(), slots) + }) + .collect(), + } + } + + #[test] + fn narrower_direct_thunk_demand_requests_a_clone() { + let functions = fixtures::capturing_program(); + let latent = Latent::new(); + let joined = joined_flow(&functions); + let facts = Facts::new(&functions, &latent, &joined); + let quiet_name = Sym::from("quiet"); + let quiet = fixtures::var(quiet_name, fixtures::action_ty()); + let loc = Loc::from([(quiet_name, Demand::pure())]); + + let instance = facts + .call_instance(Sym::from(fixtures::RUN), &[quiet], &loc) + .expect("a direct pure thunk refines the globally effectful slot"); + assert!(instance.key[0].is_empty()); + } + + #[test] + fn same_key_and_unknown_values_stay_on_the_original() { + let functions = fixtures::capturing_program(); + let latent = Latent::new(); + let joined = joined_flow(&functions); + let facts = Facts::new(&functions, &latent, &joined); + let effectful_name = Sym::from("effectful"); + let effectful = fixtures::var(effectful_name, fixtures::action_ty()); + let effectful_loc = Loc::from([( + effectful_name, + Demand::Known(joined.param[&Sym::from(fixtures::RUN)][0].clone()), + )]); + assert_eq!( + facts.call_instance(Sym::from(fixtures::RUN), &[effectful], &effectful_loc), + None, + "the joined convention needs no clone" + ); + + let unknown = fixtures::var(Sym::from("dynamic"), fixtures::action_ty()); + assert_eq!( + facts.call_instance(Sym::from(fixtures::RUN), &[unknown], &Loc::new()), + None, + "an untracked first-class thunk must retain the conservative fallback" + ); + } +} diff --git a/crates/prism-core/src/core/typed/effect_lower/decline.rs b/crates/prism-core/src/core/typed/effect_lower/decline.rs index c8d27054..2b957bfa 100644 --- a/crates/prism-core/src/core/typed/effect_lower/decline.rs +++ b/crates/prism-core/src/core/typed/effect_lower/decline.rs @@ -1,9 +1,11 @@ -//! Why a confined free-monad region was refused. +//! Why a free-monad rewrite was refused. //! //! A confined region is an optimization, so refusing one is a cost outcome the //! program survives: the whole-program lowering below it is always available. //! That makes the refusal invisible unless it is carried, which is what this -//! module is for. The reason travels as data from the site that found it to the +//! module is for. The whole-program rung has nothing below it, so the same +//! reason travels out of it as the message of an internal error, and it has to +//! name the declaration and the form or the reader is left to bisect for them. The reason travels as data from the site that found it to the //! plan artifact and the fallback warning, so neither has to re-derive it and //! neither parses it back out of a message. @@ -22,9 +24,10 @@ const HANDLER_ANSWER: &str = "handler-answer"; const HANDLER_ARMS: &str = "handler-arms"; const MEMBER_TAIL: &str = "member-tail"; const MISSING_ROW: &str = "missing-row"; +const PLAN_MISMATCH: &str = "plan-mismatch"; const UNSUPPORTED_FORM: &str = "unsupported-form"; -/// The shapes a confined attempt can refuse at. +/// The shapes a free-monad attempt can refuse at. /// /// Every refusal in the builder and in the convention-boundary check is one of /// these; there is no free-prose refusal, because a reason nobody can match on @@ -60,12 +63,18 @@ pub enum Refusal { /// A region member's tail is not `Eff`-shaped, so its caller would bind a /// value that is not a cell. MemberTail, - /// The residual row solution names no row for a member, so the monadic - /// signature for it cannot be written. + /// The residual row solution names no row for a declaration, so the + /// monadic signature for it cannot be written. MissingRow, - /// The confined builder has no rewrite for a form the declaration contains, - /// independently of the two conventions meeting. The whole-program builder - /// is the one that handles it. + /// A committed region plan and the program disagree about which + /// declarations exist: the plan names one the program does not define, or + /// the preparation the plan committed to has gone missing between building + /// the signatures and using them. + PlanMismatch, + /// The builder has no rewrite for a form the declaration contains, + /// independently of the two conventions meeting. From the confined rung + /// this widens to whole-program lowering; from whole-program lowering there + /// is nothing left to widen to. UnsupportedForm, } @@ -82,6 +91,7 @@ impl Refusal { Self::HandlerArms => HANDLER_ARMS, Self::MemberTail => MEMBER_TAIL, Self::MissingRow => MISSING_ROW, + Self::PlanMismatch => PLAN_MISMATCH, Self::UnsupportedForm => UNSUPPORTED_FORM, } } @@ -91,15 +101,16 @@ impl Refusal { #[must_use] pub const fn claim(self) -> &'static str { match self { - Self::DirectForce => "forces a computation the confined region owns", - Self::DirectHolds => "holds a computation the confined region owns", + Self::DirectForce => "forces a computation the free-monad rewrite owns", + Self::DirectHolds => "holds a computation the free-monad rewrite owns", Self::ThunkBoundary => "holds a direct thunk that reaches the other convention", - Self::WordCapture => "copies a value that reads a binder the confined region reified", + Self::WordCapture => "copies a value that reads a binder the rewrite reified", Self::HandlerAnswer => "installs a performing handler that answers with a transformer", Self::HandlerArms => "installs a handler whose clauses answer at different types", Self::MemberTail => "is a region member whose tail is not effect-shaped", - Self::MissingRow => "is a region member with no residual row", - Self::UnsupportedForm => "contains a form the confined builder cannot rewrite", + Self::MissingRow => "has no residual row to write a monadic signature from", + Self::PlanMismatch => "is named by a region plan the program does not agree with", + Self::UnsupportedForm => "contains a form the free-monad builder cannot rewrite", } } } diff --git a/crates/prism-core/src/core/typed/effect_lower/erase_control.rs b/crates/prism-core/src/core/typed/effect_lower/erase_control.rs index 101314fb..dbf5b1e5 100644 --- a/crates/prism-core/src/core/typed/effect_lower/erase_control.rs +++ b/crates/prism-core/src/core/typed/effect_lower/erase_control.rs @@ -1357,7 +1357,7 @@ impl Eraser<'_> { // `case s of SMore(ctl) => if ctl == 0 then cont else SMore(ctl) | SDone(v) // => SDone(v)`: a `continue`/`break` short-circuits the body carrying its - // disposition; a `return` propagates. `s` and `cont` share `at`; only + // disposition. A `return` propagates. `s` and `cont` share `at`. Only // `cont`'s side of the disjunction actually needs it (the propagating arm // rebuilds from `s`'s own `done` witness, which is the same `at.done`). fn step_guard_combined(&mut self, at: &StepAt, s: &TypedBinder, cont: TypedComp) -> TypedComp { @@ -1988,7 +1988,7 @@ fn nullary_thunk(m: &TypedComp) -> Option<&TypedComp> { #[cfg(test)] mod tests { - use crate::core::typed::{verify::verify, CoreFnSig, Elaborated, TypedCore, TypedCoreFn}; + use crate::core::typed::{verify, CoreFnSig, Elaborated, TypedCoreFn, UncheckedTypedCore}; use crate::types::ty::Label; use super::*; @@ -2062,7 +2062,7 @@ mod tests { ), 0, ); - if let Err(violations) = verify(&TypedCore::::new(vec![f]), &env) { + if let Err(violations) = verify(UncheckedTypedCore::::new(vec![f]), &env) { panic!("mixed-payload Step must verify: {violations:#?}"); } } @@ -2182,13 +2182,13 @@ mod tests { 0, ); let env = VerifyEnv::new(); - let input = TypedCore::::new(vec![function]); - assert_eq!(verify(&input, &env), Ok(())); + let input = verify(UncheckedTypedCore::::new(vec![function]), &env) + .expect("row-polymorphic control input verifies"); let plan = EffectPlan::analyze(input.functions()); let erased = erase_control(input.functions(), &plan); - let output = TypedCore::::new(erased.fns); - assert_eq!(verify(&output, &env), Ok(())); + let output = verify(UncheckedTypedCore::::new(erased.fns), &env) + .expect("control-erased output verifies"); assert_eq!(output.erase(), input.erase()); } diff --git a/crates/prism-core/src/core/typed/effect_lower/evidence.rs b/crates/prism-core/src/core/typed/effect_lower/evidence.rs index 0ec9f812..60040acc 100644 --- a/crates/prism-core/src/core/typed/effect_lower/evidence.rs +++ b/crates/prism-core/src/core/typed/effect_lower/evidence.rs @@ -29,8 +29,8 @@ use prism_common::fresh::Fresh; use prism_common::sym::Sym; use prism_syntax::names::{self, ENTRY_POINT, FRESH_EVIDENCE_ROW}; -use super::super::specialize_support::{free_comp_vars, free_value_vars}; -use super::super::verify::{instantiate_fn, rename_bound_core, VerifyEnv}; +use super::super::specialize_support::{free_comp_vars, free_value_vars, substitute_witnesses}; +use super::super::verify::{instantiate_fn, rename_bound_core, row_included, VerifyEnv}; use super::super::{ CompSig, CoreFnSig, CoreInstantiation, CoreQuantifier, CoreType, TypedBinder, TypedComp, TypedCompKind, TypedCoreFn, TypedHandleOp, TypedHandler, TypedValue, TypedValueKind, @@ -443,6 +443,29 @@ fn plan_fn( }) } +// Rewrite the source residual-row binder to the ambient binder the evidence +// plan appended to this callable. `plan_fn` performs the same change in the +// declaration, including parameter witnesses; the body must make that move as +// one capture-avoiding substitution too. In particular, unchanged calls can +// carry the source binder in `CoreInstantiation::Row`, and rewriting only the +// computation's outer signature leaves those witnesses stale. +fn body_in_ambient(f: &TypedCoreFn, plan: &FnPlan) -> TypedComp { + let Some(ambient) = plan.ambient else { + return f.body().clone(); + }; + let EffRow::Var(source) = f.sig().body().effects().tail() else { + return f.body().clone(); + }; + if *source == ambient { + return f.body().clone(); + } + substitute_witnesses( + f.body(), + &[CoreQuantifier::Row(*source)], + &[CoreInstantiation::Row(EffRow::Var(ambient))], + ) +} + // The type of the evidence for one op at the instantiation named by the // enclosing callable's effect row. An evidence parameter lives inside that // callable's own scheme, so `Emit(a)` becomes a monomorphic clause over the @@ -713,12 +736,14 @@ impl Retyped { // A `Var` reading a retyped local, rebuilt at its new type. #[must_use] pub fn lookup(&self, v: &TypedValue) -> Option { - let name = super::as_var(v)?; - let ty = self.0.get(&name)?; + let TypedValueKind::Var { name, .. } = v.kind() else { + return None; + }; + let ty = self.0.get(name)?; Some(TypedValue::new( ty.clone(), TypedValueKind::Var { - name, + name: *name, instantiation: Vec::new(), }, )) @@ -728,6 +753,36 @@ impl Retyped { pub fn rebuild(&self, v: &TypedValue) -> TypedValue { self.lookup(v).unwrap_or_else(|| v.clone()) } + + // Rebuild a local through source representation wrappers without erasing + // the proof those wrappers carry. A changed operand may keep an existing + // reinterpretation only when the verifier's representation judgment still + // admits its target; a newtype field is invariant and must remain exact. + fn try_rebuild(&self, v: &TypedValue) -> Option { + match v.kind() { + TypedValueKind::Reinterpret(inner) => { + reinterpret_at(self.try_rebuild(inner)?, v.ty().clone()) + } + TypedValueKind::NewtypeRepr { + constructor, + instantiation, + value, + } => { + let value2 = self.try_rebuild(value)?; + (value2.ty() == value.ty()).then(|| { + TypedValue::new( + v.ty().clone(), + TypedValueKind::NewtypeRepr { + constructor: *constructor, + instantiation: instantiation.clone(), + value: Box::new(value2), + }, + ) + }) + } + _ => Some(self.rebuild(v)), + } + } } /// Rewrite one callable's body for the evidence path. @@ -907,7 +962,9 @@ impl Threader<'_> { TypedValueKind::Unit, )] } else { - args.iter().map(|a| retyped.rebuild(a)).collect() + args.iter() + .map(|a| retyped.try_rebuild(a)) + .collect::>>()? }; Some(TypedComp::new( CompSig::new( @@ -945,11 +1002,17 @@ impl Threader<'_> { TypedCompKind::Call { callee, instantiation: instantiation.to_vec(), - args: args.iter().map(|a| retyped.rebuild(a)).collect(), + args: args + .iter() + .map(|a| retyped.try_rebuild(a)) + .collect::>>()?, }, )); }; - let mut args: Vec = args.iter().map(|a| retyped.rebuild(a)).collect(); + let mut args: Vec = args + .iter() + .map(|a| rebuild_planned_argument(retyped, a)) + .collect::>()?; let mut rows = EffRow::Empty; for param in &plan.evidence { let binder = ev.get(¶m.id)?; @@ -1112,7 +1175,9 @@ impl Threader<'_> { TypedCompKind::Bind(Box::new(bound), binder, Box::new(acc)), ); } - (acc.sig().result() == c.sig().result()).then_some(acc) + (acc.sig().result() == c.sig().result() + && threaded_row_included(acc.sig().effects(), c.sig().effects())) + .then_some(acc) } // One clause as evidence: the tail `resume(v)` stripped to `return v`, the @@ -1152,6 +1217,27 @@ impl Threader<'_> { } } +// The final guard on a rebuilt handle compares rows across two languages: the +// source row speaks user ops, while the threaded row has discharged any op +// whose evidence is in scope and rides its residual on a witness-only ambient +// row variable. Inclusion holds directly when nothing was relabeled; otherwise +// accept exactly that relabeling by rewidening the source row at the threaded +// tail, provided the tail is one the threading itself minted (the `%evr` +// namespace never reaches a source row, so nothing else can smuggle one in). +fn threaded_row_included(acc: &EffRow, src: &EffRow) -> bool { + if row_included(acc, src) { + return true; + } + match acc.tail() { + EffRow::Var(tail) if tail.as_str().starts_with(FRESH_EVIDENCE_ROW) => { + let rewidened = + EffRow::canonical(src.labels().into_iter().cloned(), EffRow::Var(*tail)); + row_included(acc, &rewidened) + } + _ => false, + } +} + // The residual row a bound clause thunk runs in. fn clause_row(binder: &TypedBinder) -> Option<&EffRow> { let CoreType::Thunk(thunk) = binder.ty() else { @@ -1173,6 +1259,31 @@ fn binder_value(b: &TypedBinder) -> TypedValue { ) } +// Retain a source reinterpretation after its operand has been threaded. Equal +// witnesses need no wrapper; otherwise the same representation proof the +// verifier checks must still hold. A convention change is not a row relabel +// and makes the evidence attempt decline instead of laundering the new value +// through the old target. +fn reinterpret_at(value: TypedValue, expected: CoreType) -> Option { + if value.ty() == &expected { + return Some(value); + } + super::super::verify::representation_preserving(value.ty(), &expected) + .then(|| TypedValue::new(expected, TypedValueKind::Reinterpret(Box::new(value)))) +} + +// Rebuild one argument of a planned call. A source wrapper whose operand the +// threading retyped has a stale target: the operand's new witness is the +// authoritative one, and the planned-parameter retarget re-aims the value at +// the only type the rewritten callee accepts. Newtype fields stay invariant, +// so only row reinterpretations are exposed this way. +fn rebuild_planned_argument(retyped: &Retyped, a: &TypedValue) -> Option { + retyped.try_rebuild(a).or_else(|| match a.kind() { + TypedValueKind::Reinterpret(inner) => rebuild_planned_argument(retyped, inner), + _ => None, + }) +} + // A planned callee may expose a narrower effect row for a thunk parameter than // the source call witness carries. The interprocedural flow plan is the proof: // it rewrites only parameters whose arriving thunk effects are known, while the @@ -1207,11 +1318,47 @@ impl Threader<'_> { loc: &Loc, retyped: &mut Retyped, ) -> Option { + // Representation wrappers are evidence, not transparent syntax. Thread + // their operand, then re-establish the proof at the original target. + // Peeling first would narrow a row-widened aggregate element back to + // its pure producer type while later uses still carry the wider row. + match v.kind() { + TypedValueKind::Reinterpret(inner) => { + let inner2 = self.thread_value_in(inner, ev, loc, retyped)?; + if inner2.ty() != inner.ty() { + // Threading retyped the operand, so the source proof's + // target is stale: re-proving there would widen the new + // witness back to the old view and hide the narrowing the + // consuming sites need. The new witness is authoritative; + // each consumer re-aims at the convention it requires. + return Some(inner2); + } + return reinterpret_at(inner2, v.ty().clone()); + } + TypedValueKind::NewtypeRepr { + constructor, + instantiation, + value, + } => { + let value2 = self.thread_value_in(value, ev, loc, retyped)?; + return (value2.ty() == value.ty()).then(|| { + TypedValue::new( + v.ty().clone(), + TypedValueKind::NewtypeRepr { + constructor: *constructor, + instantiation: instantiation.clone(), + value: Box::new(value2), + }, + ) + }); + } + _ => {} + } // A local whose type threading already changed reads at its new type. if let Some(rebuilt) = retyped.lookup(v) { return Some(rebuilt); } - match &super::peel(v).kind { + match v.kind() { TypedValueKind::Thunk(c) => match c.kind() { TypedCompKind::Lam(ps, b) => self.thread_lambda_thunk(ps, b, ev, loc, retyped), other_kind => { @@ -1444,7 +1591,14 @@ pub fn try_lower_ev( retyped.insert(p.name(), q.ty().clone()); } } - let body = threader.thread_in(f.body(), &ev, &loc, &mut retyped)?; + // The prepass coalesces the declaration's source residual row into its + // fresh ambient row. Apply that alpha-renaming to every witness in the + // body before threading: CompSigs, nested CoreTypes, and explicit row + // instantiations must agree with the rewritten declaration. + let planned_body = plan + .get(f.name()) + .map_or_else(|| f.body().clone(), |fp| body_in_ambient(f, fp)); + let body = threader.thread_in(&planned_body, &ev, &loc, &mut retyped)?; out.push(TypedCoreFn::new( f.name(), params, @@ -1520,6 +1674,83 @@ mod tests { assert_eq!(ids.ids_of(wanted.iter()), Some(vec![0, 2])); } + #[test] + fn retyped_reads_preserve_representation_wrappers() { + let callable = |effects| { + CoreType::Thunk(Box::new(CompSig::new( + CoreType::Function(Box::new(CoreFnSig::new( + Vec::new(), + vec![int()], + CompSig::new(int(), effects), + ))), + EffRow::Empty, + ))) + }; + let name = sym("f"); + let original = callable(EffRow::singleton(sym("Read"))); + let mapped = callable(EffRow::singleton(sym("Write"))); + let target = callable(EffRow::canonical( + [Label::bare(sym("Read")), Label::bare(sym("Write"))], + EffRow::Empty, + )); + let bare = TypedValue::new( + original, + TypedValueKind::Var { + name, + instantiation: Vec::new(), + }, + ); + let wrapped = TypedValue::new( + target.clone(), + TypedValueKind::Reinterpret(Box::new(bare.clone())), + ); + let mut retyped = Retyped::new(); + retyped.insert(name, mapped.clone()); + + assert_eq!( + retyped.lookup(&bare).expect("bare read retypes").ty(), + &mapped + ); + assert!( + retyped.lookup(&wrapped).is_none(), + "lookup must not flatten a representation wrapper" + ); + let rebuilt = retyped + .try_rebuild(&wrapped) + .expect("both rows are representable at the aggregate target"); + assert_eq!(rebuilt.ty(), &target); + let TypedValueKind::Reinterpret(inner) = rebuilt.kind() else { + panic!("the aggregate row target remains explicit") + }; + assert_eq!(inner.ty(), &mapped); + + let wrong_convention = CoreType::Thunk(Box::new(CompSig::new( + CoreType::Function(Box::new(CoreFnSig::new( + Vec::new(), + vec![CoreType::Source(Type::Bool)], + CompSig::new(int(), EffRow::Empty), + ))), + EffRow::Empty, + ))); + retyped.insert(name, wrong_convention); + assert!( + retyped.try_rebuild(&wrapped).is_none(), + "a wrapper cannot conceal a changed calling convention" + ); + let newtype = TypedValue::new( + target, + TypedValueKind::NewtypeRepr { + constructor: sym("FnBox"), + instantiation: Vec::new(), + value: Box::new(bare), + }, + ); + assert!( + retyped.try_rebuild(&newtype).is_none(), + "a newtype field cannot change witness under its constructor" + ); + } + #[test] fn a_call_argument_can_retarget_only_its_thunk_effect_row() { let source_tail = sym("source_row"); @@ -1944,6 +2175,24 @@ mod tests { } } + // A rebuilt handle whose clause re-performs through evidence rides the + // ambient row where its source row named the op. That relabeling, and + // only that relabeling, must pass the final handle guard. + #[test] + fn handle_guard_accepts_the_ambient_relabeling_and_nothing_else() { + let ambient = RowNames::new().next(); + let src = EffRow::singleton("Yield"); + // Discharged op, residual on the minted ambient: the eff_fuse shape. + assert!(threaded_row_included(&EffRow::Var(ambient), &src)); + // Unchanged rows still pass through plain inclusion. + assert!(threaded_row_included(&src, &src)); + // A label the source row never claimed does not launder through. + let leaked = EffRow::canonical([Label::bare(sym("IO"))], EffRow::Var(ambient)); + assert!(!threaded_row_included(&leaked, &src)); + // A tail outside the witness-only namespace is not threading's own. + assert!(!threaded_row_included(&EffRow::Var(sym("e")), &src)); + } + // A handler in the producer is gone by the time an escaping thunk is // forced. Keep effects outside `flow.ret` in the returned witness instead // of treating producer-local handlers as dynamically enclosing the force. diff --git a/crates/prism-core/src/core/typed/effect_lower/flow.rs b/crates/prism-core/src/core/typed/effect_lower/flow.rs index 2bd62019..18dc516b 100644 --- a/crates/prism-core/src/core/typed/effect_lower/flow.rs +++ b/crates/prism-core/src/core/typed/effect_lower/flow.rs @@ -14,11 +14,13 @@ use std::collections::{BTreeMap, BTreeSet}; use prism_common::sym::Sym; use super::super::{ - TypedBinder, TypedComp, TypedCompKind, TypedCoreFn, TypedValue, TypedValueKind, + CoreQuantifier, CoreType, TypedBinder, TypedComp, TypedCompKind, TypedCoreFn, TypedValue, + TypedValueKind, }; use super::latent::{latent, Latent, MaskOp}; use super::peel; use super::walk::each_value; +use crate::types::ty::EffRow; /// The op set a thunk performs when forced (mask-aware, like `latent`). pub type Sig = BTreeSet; @@ -150,25 +152,72 @@ fn buried(v: &TypedValue, loc: &Loc, lat: &Latent) -> bool { match &peel(v).kind { TypedValueKind::Ctor { fields, .. } | TypedValueKind::Tuple(fields) - | TypedValueKind::UnboxedTuple(fields) => fields - .iter() - .any(|f| !value_sig(f, loc, lat).is_empty() || buried(f, loc, lat)), - TypedValueKind::UnboxedRecord(fields) => fields - .iter() - .any(|(_, f)| !value_sig(f, loc, lat).is_empty() || buried(f, loc, lat)), + | TypedValueKind::UnboxedTuple(fields) => fields.iter().any(|f| { + declared_thunk_escape(f) || !value_sig(f, loc, lat).is_empty() || buried(f, loc, lat) + }), + TypedValueKind::UnboxedRecord(fields) => fields.iter().any(|(_, f)| { + declared_thunk_escape(f) || !value_sig(f, loc, lat).is_empty() || buried(f, loc, lat) + }), _ => false, } } +// A callback hidden in data can later be recovered only through a pattern, and +// the flow analysis intentionally does not invent a signature for pattern +// fields. Its stored witness is therefore authoritative even when the concrete +// lambda performs less: a pure thunk widened to `! {Log}` still has to be +// called at the `Log` convention after extraction. Representation wrappers are +// evidence for that widening, so inspect their targets before following their +// operands. A free open row stays opaque for the same reason: it stands for +// effects chosen elsewhere. Only a row the stored function itself quantifies +// is transparent, because each force site instantiates it in the open. +fn declared_thunk_escape(value: &TypedValue) -> bool { + effectful_thunk_type(value.ty()) + || match value.kind() { + TypedValueKind::Reinterpret(inner) + | TypedValueKind::NewtypeRepr { value: inner, .. } => declared_thunk_escape(inner), + _ => false, + } +} + +fn effectful_thunk_type(ty: &CoreType) -> bool { + let CoreType::Thunk(outer) = ty else { + return false; + }; + if row_claims_effects(outer.effects(), &[]) { + return true; + } + let CoreType::Function(function) = outer.result() else { + return false; + }; + row_claims_effects(function.body().effects(), function.quantifiers()) +} + +// Whether a stored thunk's row is a claim the flow must honor. A concrete +// label is a declared widening: the extracted thunk must be called at that +// convention even when the lambda inside performs less. A free variable or an +// existential stands for effects someone else chose, so it is the same +// unknown claim. A row variable the function itself quantifies is neither: it +// is polymorphism the caller instantiates, visible at every use site. +fn row_claims_effects(row: &EffRow, quantifiers: &[CoreQuantifier]) -> bool { + match row { + EffRow::Empty => false, + EffRow::Extend(..) | EffRow::Exist(_) => true, + EffRow::Var(v) => !quantifiers + .iter() + .any(|q| matches!(q, CoreQuantifier::Row(r) if r == v)), + } +} + fn esc(c: &TypedComp, loc: &Loc, lat: &Latent, flow: &ThunkFlow) -> bool { match c.kind() { TypedCompKind::Return(v) => buried(v, loc, lat) || in_thunk(v, loc, lat, flow), TypedCompKind::Call { args, .. } => args .iter() .any(|a| buried(a, loc, lat) || in_thunk(a, loc, lat, flow)), - TypedCompKind::App { args, .. } | TypedCompKind::Do { args, .. } => args - .iter() - .any(|a| !value_sig(a, loc, lat).is_empty() || buried(a, loc, lat)), + TypedCompKind::App { args, .. } | TypedCompKind::Do { args, .. } => args.iter().any(|a| { + declared_thunk_escape(a) || !value_sig(a, loc, lat).is_empty() || buried(a, loc, lat) + }), TypedCompKind::Bind(m, x, n) => { esc(m, loc, lat, flow) || { let mut loc2 = loc.clone(); @@ -380,3 +429,63 @@ fn visit_value( _ => {} } } + +#[cfg(test)] +mod tests { + use crate::core::typed::{CompSig, CoreFnSig}; + use crate::types::ty::EffRow; + use crate::types::Type; + + use super::*; + + fn callback(row: EffRow) -> CoreType { + CoreType::Thunk(Box::new(CompSig::new( + CoreType::Function(Box::new(CoreFnSig::new( + Vec::new(), + Vec::new(), + CompSig::new(CoreType::Source(Type::Unit), row), + ))), + EffRow::Empty, + ))) + } + + // A row variable bound by the stored function's own quantifiers: the + // caller chooses the row at each instantiation, so the thunk claims no + // effects of its own. + fn poly_callback(row: EffRow) -> CoreType { + CoreType::Thunk(Box::new(CompSig::new( + CoreType::Function(Box::new(CoreFnSig::new( + vec![CoreQuantifier::Row(Sym::new("e"))], + Vec::new(), + CompSig::new(CoreType::Source(Type::Unit), row), + ))), + EffRow::Empty, + ))) + } + + #[test] + fn stored_thunk_witnesses_make_dynamic_uses_opaque() { + assert!(!effectful_thunk_type(&callback(EffRow::Empty))); + assert!(effectful_thunk_type(&callback(EffRow::singleton("Log")))); + assert!(effectful_thunk_type(&callback(EffRow::Var(Sym::new("e"))))); + assert!(!effectful_thunk_type(&poly_callback(EffRow::Var( + Sym::new("e") + )))); + assert!(effectful_thunk_type(&CoreType::Thunk(Box::new( + CompSig::new(CoreType::Source(Type::Unit), EffRow::singleton("Log")) + )))); + + let local = TypedValue::new( + callback(EffRow::Empty), + TypedValueKind::Var { + name: Sym::new("quiet"), + instantiation: Vec::new(), + }, + ); + let widened = TypedValue::new( + callback(EffRow::singleton("Log")), + TypedValueKind::Reinterpret(Box::new(local)), + ); + assert!(declared_thunk_escape(&widened)); + } +} diff --git a/crates/prism-core/src/core/typed/effect_lower/mod.rs b/crates/prism-core/src/core/typed/effect_lower/mod.rs index d04e370c..7443dfa6 100644 --- a/crates/prism-core/src/core/typed/effect_lower/mod.rs +++ b/crates/prism-core/src/core/typed/effect_lower/mod.rs @@ -21,6 +21,7 @@ pub mod abi; pub mod analysis; pub mod arena; mod checks; +mod convention; pub mod decline; pub mod diagnostics; mod erase_control; @@ -52,10 +53,11 @@ use prism_syntax::names::ENTRY_POINT; use super::inline::calls_in; use super::specialize_support::{free_comp_vars, Rewrite}; -use super::verify::{instantiate_fn, union_rows, verify, VerifyEnv}; +use super::verify::{instantiate_fn, union_rows, VerifyEnv}; use super::{ - CoreFnSig, CoreInstantiation, CoreQuantifier, CoreType, EffectLowered, Elaborated, TypedBinder, - TypedComp, TypedCompKind, TypedCore, TypedCoreFn, TypedPattern, TypedValue, TypedValueKind, + verify, CoreFnSig, CoreInstantiation, CoreQuantifier, CoreType, EffectLowered, Elaborated, + TypedBinder, TypedComp, TypedCompKind, TypedCore, TypedCoreFn, TypedPattern, TypedValue, + TypedValueKind, UncheckedTypedCore, }; use decline::Decline; use diagnostics::DriftLog; @@ -86,8 +88,8 @@ type Attempt = Result; /// What the cascade decided. /// -/// The cascade is the single source of truth for both classification and the -/// lowering it selects, so a second classifier cannot drift from production. +/// The cascade performs classification and selects the lowering, avoiding a +/// second classifier that could drift from production. #[derive(Debug)] pub enum Decision { Lowered(Box), @@ -161,14 +163,14 @@ pub fn prepare( ) -> Result { // Dead prelude code must not flip the program into monadic mode, so only // functions reachable from main are lowered (and kept) at all. - let fns: Vec = if core.fns.iter().any(|f| f.name().as_str() == ENTRY_POINT) { - let live = reachable(&core.fns); - core.fns - .into_iter() + let fns = core.into_unchecked().into_functions(); + let fns: Vec = if fns.iter().any(|f| f.name().as_str() == ENTRY_POINT) { + let live = reachable(&fns); + fns.into_iter() .filter(|f| live.contains(&f.name())) .collect() } else { - core.fns + fns }; // Scope-directed arena lowering, before the tier branch so every tier reifies @@ -181,7 +183,10 @@ pub fn prepare( // `with_arena` is present, so the non-arena corpus stays byte-identical. let mut env = env.clone(); arena::insert_builtin_sigs(&mut env); - let fns = arena::prepare(fns, &env)?.fns; + let arena = arena::prepare(fns, &env)?; + let fns = convention::split(arena, &env)? + .into_unchecked() + .into_functions(); // Erase escape-checked local `var` state to mutable cells before strategy // selection, so a var-only program has no residual effects and classifies @@ -270,14 +275,23 @@ pub fn threaded_state_typed( return Ok(None); } let mut fresh = prism_common::fresh::Fresh::new(); - Ok(state::thread_program( + let Some(fns) = state::thread_program( &prepared.fns, &plan, &analysis, &DriftLog::new(flags.quiet), &mut fresh, - ) - .map(|fns| (TypedCore::::new(fns), env))) + ) else { + return Ok(None); + }; + verify(UncheckedTypedCore::::new(fns), &env) + .map(|core| Some((core, env))) + .map_err(|violations| TypedCoreEffectLoweringFailure::Verification { + first: violations + .first() + .map_or_else(String::new, ToString::to_string), + count: violations.len(), + }) } fn cascade( @@ -612,7 +626,9 @@ pub fn assemble_local_partial( .map_err(|msg| TypedCoreEffectLoweringFailure::Internal { msg })?; let region_functions = monadic::lower_region(fns, split.region, split.entries, analysis.ops, fresh, &rows) - .map_err(|msg| TypedCoreEffectLoweringFailure::Internal { msg })?; + .map_err(|decline| TypedCoreEffectLoweringFailure::Internal { + msg: decline.to_string(), + })?; let entry_signatures: BTreeMap = region_functions .iter() .filter(|function| split.entries.contains(&function.name())) @@ -735,7 +751,6 @@ fn attempt_monadic( ), analysis::MonadicScope::WholeProgram => { monadic::lower_whole(fns, analysis.ops, fresh, &residual) - .ok_or_else(|| Decline::program(decline::Refusal::UnsupportedForm)) } }; let mut output = match output { @@ -903,15 +918,14 @@ fn lowered( strategy: EffectStrategy, confined_decline: Option, ) -> Result { - let out = TypedCore::::new(fns); - if let Err(violations) = verify(&out, env) { - return Err(TypedCoreEffectLoweringFailure::Verification { + let out = verify(UncheckedTypedCore::::new(fns), env).map_err(|violations| { + TypedCoreEffectLoweringFailure::Verification { first: violations .first() .map_or_else(String::new, ToString::to_string), count: violations.len(), - }); - } + } + })?; Ok(Decision::Lowered(Box::new(TypedLowering { core: out, env: env.clone(), diff --git a/crates/prism-core/src/core/typed/effect_lower/monadic.rs b/crates/prism-core/src/core/typed/effect_lower/monadic.rs index 5ec95a24..1eb6ec92 100644 --- a/crates/prism-core/src/core/typed/effect_lower/monadic.rs +++ b/crates/prism-core/src/core/typed/effect_lower/monadic.rs @@ -2247,7 +2247,7 @@ impl<'a> Monadic<'a> { }) } - /// Record why a confined attempt is refused, and decline. The first + /// Record why an attempt is refused, and decline. The first /// refusal wins: it is the innermost one, and every decline above it is /// only this one unwinding. const fn refuse(&mut self, reason: Refusal, site: Site) -> Option { @@ -2259,8 +2259,8 @@ impl<'a> Monadic<'a> { /// The refusal this builder recorded, attributed to the declaration being /// lowered when it stopped. A decline with nothing recorded is a form the - /// confined builder has no rewrite for at all, which is a refusal of its - /// own kind rather than an unexplained one. + /// builder has no rewrite for at all, which is a refusal of its own kind + /// rather than an unexplained one. const fn declined(&self, function: Sym) -> Decline { let (reason, site) = match self.refusal { Some(recorded) => recorded, @@ -3014,17 +3014,26 @@ fn monadic_quantifiers(function: &TypedCoreFn, row: &EffRow) -> Vec( functions: &[TypedCoreFn], ops: &OpIds, fresh: &mut Fresh, rows: &R, -) -> Option> { +) -> Result, Decline> { let signatures: BTreeMap = functions .iter() .map(|function| { - let row = rows.row(function.name())?; - Some(( + let row = rows + .row(function.name()) + .ok_or_else(|| Decline::whole(Refusal::MissingRow, function.name()))?; + Ok(( function.name(), CoreFnSig::new( monadic_quantifiers(function, &row), @@ -3033,11 +3042,13 @@ pub fn lower_whole( ), )) }) - .collect::>()?; + .collect::>()?; let mut monadic = Monadic::new(ops, fresh, EffRow::Empty, &signatures); let mut lowered = Vec::with_capacity(functions.len()); for function in functions { - let row = rows.row(function.name())?; + let row = rows + .row(function.name()) + .ok_or_else(|| Decline::whole(Refusal::MissingRow, function.name()))?; monadic.set_row(row.clone()); monadic.quantifiers = monadic_quantifiers(function, &row); monadic.locals = function @@ -3055,7 +3066,9 @@ pub fn lower_whole( // Thunk signatures are per-declaration for the same reason: they are // keyed by binder name, and two declarations share names freely. monadic.thunk_sigs.clear(); - let body = monadic.comp(function.body())?; + let body = monadic + .comp(function.body()) + .ok_or_else(|| monadic.declined(function.name()))?; let entry = function.name().as_str() == ENTRY_POINT; let body = if entry { monadic.unwrap_entry(body, function.sig().body().result().clone()) @@ -3069,7 +3082,10 @@ pub fn lower_whole( CompSig::new(function.sig().body().result().clone(), row.clone()), ) } else { - signatures.get(&function.name())?.clone() + signatures + .get(&function.name()) + .ok_or_else(|| Decline::whole(Refusal::MissingRow, function.name()))? + .clone() }; lowered.push(TypedCoreFn::new( function.name(), @@ -3080,7 +3096,7 @@ pub fn lower_whole( )); } lowered.append(&mut monadic.generated); - Some(lowered) + Ok(lowered) } /// Lower one clean `LocalPartial` component in the whole-style convention while @@ -3090,8 +3106,10 @@ pub fn lower_whole( /// split. /// /// # Errors -/// A message when a region member has no planned row, or when its body has no -/// monadic rewrite. +/// The refusal that stopped the region: a member with no planned row, a plan +/// naming a declaration the program does not define, or a member body the +/// builder has no rewrite for. The plan is already committed by the time this +/// runs, so none of these widen; the caller reports them. pub fn lower_region( functions: &[TypedCoreFn], region: &BTreeSet, @@ -3099,36 +3117,26 @@ pub fn lower_region( ops: &OpIds, fresh: &mut Fresh, rows: &R, -) -> Result, String> { +) -> Result, Decline> { let planned_rows: BTreeMap = functions .iter() .filter(|function| region.contains(&function.name())) .map(|function| { rows.row(function.name()) .map(|row| (function.name(), row)) - .ok_or_else(|| { - format!( - "LocalPartial member `{}` has no residual-row plan", - function.name() - ) - }) + .ok_or_else(|| Decline::whole(Refusal::MissingRow, function.name())) }) .collect::>()?; if let Some(missing) = region.iter().find(|name| !planned_rows.contains_key(name)) { - return Err(format!( - "LocalPartial plan names missing declaration `{missing}`" - )); + return Err(Decline::whole(Refusal::PlanMismatch, *missing)); } let signatures: BTreeMap = functions .iter() .map(|function| { let signature = if region.contains(&function.name()) { - let row = planned_rows.get(&function.name()).ok_or_else(|| { - format!( - "LocalPartial member `{}` lost its residual-row plan", - function.name() - ) - })?; + let row = planned_rows + .get(&function.name()) + .ok_or_else(|| Decline::whole(Refusal::MissingRow, function.name()))?; CoreFnSig::new( monadic_quantifiers(function, row), function.sig().params().to_vec(), @@ -3139,19 +3147,16 @@ pub fn lower_region( }; Ok((function.name(), signature)) }) - .collect::>()?; + .collect::>()?; let mut monadic = Monadic::new(ops, fresh, EffRow::Empty, &signatures); let mut lowered = Vec::with_capacity(region.len()); for function in functions .iter() .filter(|function| region.contains(&function.name())) { - let row = planned_rows.get(&function.name()).ok_or_else(|| { - format!( - "LocalPartial member `{}` lost its prepared row", - function.name() - ) - })?; + let row = planned_rows + .get(&function.name()) + .ok_or_else(|| Decline::whole(Refusal::MissingRow, function.name()))?; monadic.set_row(row.clone()); monadic.quantifiers = monadic_quantifiers(function, row); monadic.locals = function @@ -3162,12 +3167,9 @@ pub fn lower_region( monadic.word_binders.clear(); monadic.resume_aliases.clear(); monadic.thunk_sigs.clear(); - let body = monadic.comp(function.body()).ok_or_else(|| { - format!( - "LocalPartial member `{}` failed after its region plan committed", - function.name() - ) - })?; + let body = monadic + .comp(function.body()) + .ok_or_else(|| monadic.declined(function.name()))?; let entry = entries.contains(&function.name()); let body = if entry { monadic.unwrap_entry(body, function.sig().body().result().clone()) @@ -3184,12 +3186,10 @@ pub fn lower_region( ), ) } else { - signatures.get(&function.name()).cloned().ok_or_else(|| { - format!( - "LocalPartial member `{}` lost its prepared signature", - function.name() - ) - })? + signatures + .get(&function.name()) + .cloned() + .ok_or_else(|| Decline::whole(Refusal::PlanMismatch, function.name()))? }; lowered.push(TypedCoreFn::new( function.name(), @@ -3358,12 +3358,13 @@ pub fn lower_selective( #[cfg(test)] mod tests { use super::super::super::{ - CoreFnSig, EffectLowered, Elaborated, TypedCore, TypedCoreFn, TypedHandleOp, TypedHandler, + verify, CoreFnSig, EffectLowered, Elaborated, TypedCoreFn, TypedHandleOp, TypedHandler, + UncheckedTypedCore, }; use super::super::fixtures; use super::*; use crate::core::cbpv::{Comp, CoreOp, CorePat, Value}; - use crate::core::typed::verify::{verify, VerifyEnv}; + use crate::core::typed::verify::VerifyEnv; struct MissingRows; @@ -3521,7 +3522,7 @@ mod tests { &MissingRows, ) .expect_err("a committed LocalPartial plan requires every residual row"); - assert!(error.contains("has no residual-row plan")); + assert_eq!(error, Decline::whole(Refusal::MissingRow, name)); assert_eq!(fresh.bump(), 0, "planning failures cannot consume names"); } @@ -3584,8 +3585,11 @@ mod tests { ); let mut env = VerifyEnv::new(); abi::insert(&mut env); - let core = TypedCore::::new(vec![main, abi::ebind_fn(), abi::qapply_fn()]); - assert_eq!(verify(&core, &env), Ok(())); + verify( + UncheckedTypedCore::::new(vec![main, abi::ebind_fn(), abi::qapply_fn()]), + &env, + ) + .expect("translated bind and operation verify"); let m = Sym::from(names::lowered("m", 0)); assert_eq!( @@ -3647,10 +3651,11 @@ mod tests { ); let mut env = VerifyEnv::new(); abi::insert(&mut env); - assert_eq!( - verify(&TypedCore::::new(vec![function]), &env), - Ok(()) - ); + verify( + UncheckedTypedCore::::new(vec![function]), + &env, + ) + .expect("tuple fixture verifies"); } #[test] @@ -3701,13 +3706,11 @@ mod tests { ); let mut env = VerifyEnv::new(); abi::insert(&mut env); - assert_eq!( - verify( - &TypedCore::::new(vec![consumer, invocation]), - &env, - ), - Ok(()) - ); + verify( + UncheckedTypedCore::::new(vec![consumer, invocation]), + &env, + ) + .expect("retagged region call verifies"); } #[test] @@ -3744,10 +3747,8 @@ mod tests { ); let mut env = VerifyEnv::new(); abi::insert(&mut env); - assert_eq!( - verify(&TypedCore::::new(vec![main]), &env), - Ok(()) - ); + verify(UncheckedTypedCore::::new(vec![main]), &env) + .expect("dynamic application verifies"); assert_eq!( body.erase(), Comp::App( @@ -3802,10 +3803,11 @@ mod tests { .expect("whole-program convention closes direct calls"); let mut env = VerifyEnv::new(); abi::insert(&mut env); - assert_eq!( - verify(&TypedCore::::new(lowered.clone()), &env), - Ok(()) - ); + verify( + UncheckedTypedCore::::new(lowered.clone()), + &env, + ) + .expect("whole-program direct calls verify"); assert_eq!( lowered .into_iter() @@ -3874,10 +3876,8 @@ mod tests { ); let mut env = VerifyEnv::new(); abi::insert(&mut env); - assert_eq!( - verify(&TypedCore::::new(vec![main]), &env), - Ok(()) - ); + verify(UncheckedTypedCore::::new(vec![main]), &env) + .expect("lifted primitive verifies"); let p = Sym::from(names::lowered("p", 0)); assert_eq!( body.erase(), @@ -4013,16 +4013,16 @@ mod tests { CoreFnSig::new(Vec::new(), Vec::new(), source_body.sig().clone()), 0, ); - let source = TypedCore::::new(vec![main]); + let source = UncheckedTypedCore::::new(vec![main]); let mut fresh = Fresh::new(); - let mut lowered = lower_whole(&source.fns, &ops, &mut fresh, &EffRow::Empty) + let mut lowered = lower_whole(source.functions(), &ops, &mut fresh, &EffRow::Empty) .expect("open handler translates"); lowered.push(abi::ebind_fn()); lowered.push(abi::qapply_fn()); let mut env = VerifyEnv::new(); abi::insert(&mut env); - let typed = TypedCore::::new(lowered); - assert_eq!(verify(&typed, &env), Ok(())); + let typed = verify(UncheckedTypedCore::::new(lowered), &env) + .expect("open handler output verifies"); crate::core::residual_effects(&typed.erase()).expect("no raw effects survive"); } @@ -4120,16 +4120,16 @@ mod tests { CoreFnSig::new(Vec::new(), Vec::new(), handled.sig().clone()), 0, ); - let source = TypedCore::::new(vec![main]); + let source = UncheckedTypedCore::::new(vec![main]); let mut fresh = Fresh::new(); - let mut lowered = lower_whole(&source.fns, &ops, &mut fresh, &EffRow::Empty) + let mut lowered = lower_whole(source.functions(), &ops, &mut fresh, &EffRow::Empty) .expect("routed resume application translates"); lowered.push(abi::ebind_fn()); lowered.push(abi::qapply_fn()); let mut env = VerifyEnv::new(); abi::insert(&mut env); - let typed = TypedCore::::new(lowered); - assert_eq!(verify(&typed, &env), Ok(())); + let typed = verify(UncheckedTypedCore::::new(lowered), &env) + .expect("routed resume output verifies"); crate::core::residual_effects(&typed.erase()).expect("no raw effects survive"); } @@ -4160,16 +4160,16 @@ mod tests { CoreFnSig::new(Vec::new(), Vec::new(), masked.sig().clone()), 0, ); - let source = TypedCore::::new(vec![main]); + let source = UncheckedTypedCore::::new(vec![main]); let mut fresh = Fresh::new(); - let mut lowered = lower_whole(&source.fns, &ops, &mut fresh, &EffRow::Empty) + let mut lowered = lower_whole(source.functions(), &ops, &mut fresh, &EffRow::Empty) .expect("mask driver translates"); lowered.push(abi::ebind_fn()); lowered.push(abi::qapply_fn()); let mut env = VerifyEnv::new(); abi::insert(&mut env); - let typed = TypedCore::::new(lowered); - assert_eq!(verify(&typed, &env), Ok(())); + let typed = verify(UncheckedTypedCore::::new(lowered), &env) + .expect("mask driver output verifies"); crate::core::residual_effects(&typed.erase()).expect("no raw effects survive"); } @@ -4230,15 +4230,15 @@ mod tests { CoreFnSig::new(Vec::new(), Vec::new(), handled.sig().clone()), 0, ); - let source = TypedCore::::new(vec![main]); - let effects = super::super::EffectPlan::analyze(&source.fns); + let source = UncheckedTypedCore::::new(vec![main]); + let effects = super::super::EffectPlan::analyze(source.functions()); let latent = effects.latent(); - let plan = super::super::analysis::plan(&source.fns, &effects, false); + let plan = super::super::analysis::plan(source.functions(), &effects, false); assert_eq!(plan.scope, MonadicScope::Selective); let mut fresh = Fresh::new(); let mut lowered = lower_selective( - &source.fns, + source.functions(), &ops, &mut fresh, &EffRow::Empty, @@ -4254,8 +4254,8 @@ mod tests { lowered.push(abi::qapply_fn()); let mut env = VerifyEnv::new(); abi::insert(&mut env); - let typed = TypedCore::::new(lowered); - assert_eq!(verify(&typed, &env), Ok(())); + let typed = verify(UncheckedTypedCore::::new(lowered), &env) + .expect("selective closed-handler output verifies"); crate::core::residual_effects(&typed.erase()).expect("no raw effects survive"); } @@ -4336,13 +4336,13 @@ mod tests { CoreFnSig::new(Vec::new(), Vec::new(), handled.sig().clone()), 0, ); - let source = TypedCore::::new(vec![main]); - let effects = super::super::EffectPlan::analyze(&source.fns); + let source = UncheckedTypedCore::::new(vec![main]); + let effects = super::super::EffectPlan::analyze(source.functions()); let latent = effects.latent(); - let plan = super::super::analysis::plan(&source.fns, &effects, false); + let plan = super::super::analysis::plan(source.functions(), &effects, false); let mut fresh = Fresh::new(); let mut lowered = lower_selective( - &source.fns, + source.functions(), &ops, &mut fresh, &EffRow::Empty, @@ -4358,8 +4358,8 @@ mod tests { lowered.push(abi::qapply_fn()); let mut env = VerifyEnv::new(); abi::insert(&mut env); - let typed = TypedCore::::new(lowered); - assert_eq!(verify(&typed, &env), Ok(())); + let typed = verify(UncheckedTypedCore::::new(lowered), &env) + .expect("native-region output verifies"); crate::core::residual_effects(&typed.erase()).expect("no raw effects survive"); } @@ -4450,10 +4450,8 @@ mod tests { lowered.push(abi::qapply_fn()); let mut env = VerifyEnv::new(); abi::insert(&mut env); - assert_eq!( - verify(&TypedCore::::new(lowered), &env), - Ok(()) - ); + verify(UncheckedTypedCore::::new(lowered), &env) + .expect("generic captured-handler output verifies"); } #[test] @@ -4461,9 +4459,9 @@ mod tests { let ops = OpIds::assign(&BTreeSet::from([Sym::from(fixtures::ASK_OP)])) .expect("one operation has an id"); let functions = fixtures::capturing_program(); - let source = TypedCore::::new(functions); - let effects = super::super::EffectPlan::analyze(&source.fns); - let plan = super::super::analysis::plan(&source.fns, &effects, false); + let source = UncheckedTypedCore::::new(functions); + let effects = super::super::EffectPlan::analyze(source.functions()); + let plan = super::super::analysis::plan(source.functions(), &effects, false); assert_eq!(plan.scope, MonadicScope::Selective); assert!( !plan.members.contains(&Sym::from(ENTRY_POINT)), @@ -4472,7 +4470,7 @@ mod tests { let mut fresh = Fresh::new(); let mut lowered = lower_selective( - &source.fns, + source.functions(), &ops, &mut fresh, &EffRow::Empty, @@ -4488,8 +4486,8 @@ mod tests { lowered.push(abi::qapply_fn()); let mut env = VerifyEnv::new(); abi::insert(&mut env); - let typed = TypedCore::::new(lowered); - assert_eq!(verify(&typed, &env), Ok(())); + let typed = verify(UncheckedTypedCore::::new(lowered), &env) + .expect("confined-region output verifies"); crate::core::residual_effects(&typed.erase()).expect("no raw effects survive"); } @@ -4505,15 +4503,15 @@ mod tests { Sym::from(fixtures::LEAK_OP), ])) .expect("both operations have ids"); - let source = TypedCore::::new(fixtures::island_program()); - let effects = super::super::EffectPlan::analyze(&source.fns); - let plan = super::super::analysis::plan(&source.fns, &effects, false); + let source = UncheckedTypedCore::::new(fixtures::island_program()); + let effects = super::super::EffectPlan::analyze(source.functions()); + let plan = super::super::analysis::plan(source.functions(), &effects, false); assert_eq!(plan.scope, MonadicScope::Selective); assert!(plan.members.contains(&Sym::from(fixtures::RUN))); let mut fresh = Fresh::new(); let mut lowered = lower_selective( - &source.fns, + source.functions(), &ops, &mut fresh, &EffRow::Empty, @@ -4529,8 +4527,8 @@ mod tests { lowered.push(abi::qapply_fn()); let mut env = VerifyEnv::new(); abi::insert(&mut env); - let typed = TypedCore::::new(lowered); - assert_eq!(verify(&typed, &env), Ok(())); + let typed = verify(UncheckedTypedCore::::new(lowered), &env) + .expect("island-handler output verifies"); crate::core::residual_effects(&typed.erase()).expect("no raw effects survive"); } @@ -4598,9 +4596,9 @@ mod tests { let ops = OpIds::assign(&BTreeSet::from([Sym::from(fixtures::ASK_OP)])) .expect("one operation has an id"); let functions = fixtures::capturing_program(); - let source = TypedCore::::new(functions); - let effects = super::super::EffectPlan::analyze(&source.fns); - let mut plan = super::super::analysis::plan(&source.fns, &effects, false); + let source = UncheckedTypedCore::::new(functions); + let effects = super::super::EffectPlan::analyze(source.functions()); + let mut plan = super::super::analysis::plan(source.functions(), &effects, false); // Hand-narrow the region to drop the forwarder. Nothing in the planner // produces this shape; the point is that if anything ever did, the // builder refuses to emit direct code that forces a monadic thunk @@ -4610,7 +4608,7 @@ mod tests { let mut fresh = Fresh::new(); let refusal = lower_selective( - &source.fns, + source.functions(), &ops, &mut fresh, &EffRow::Empty, @@ -4655,11 +4653,11 @@ mod tests { Sym::from(fixtures::LEAK_OP), ])) .expect("both operations have ids"); - let source = TypedCore::::new(functions); - let effects = super::super::EffectPlan::analyze(&source.fns); + let source = UncheckedTypedCore::::new(functions); + let effects = super::super::EffectPlan::analyze(source.functions()); let mut fresh = Fresh::new(); lower_selective( - &source.fns, + source.functions(), &ops, &mut fresh, &EffRow::Empty, @@ -4726,12 +4724,12 @@ mod tests { fn a_member_with_no_residual_row_declines_before_minting_names() { let ops = OpIds::assign(&BTreeSet::from([Sym::from(fixtures::ASK_OP)])) .expect("one operation has an id"); - let source = TypedCore::::new(fixtures::capturing_program()); - let effects = super::super::EffectPlan::analyze(&source.fns); - let plan = super::super::analysis::plan(&source.fns, &effects, false); + let source = UncheckedTypedCore::::new(fixtures::capturing_program()); + let effects = super::super::EffectPlan::analyze(source.functions()); + let plan = super::super::analysis::plan(source.functions(), &effects, false); let mut fresh = Fresh::new(); let refusal = lower_selective( - &source.fns, + source.functions(), &ops, &mut fresh, &MissingRows, @@ -4771,15 +4769,15 @@ mod tests { Vec::new(), fixtures::call(fixtures::RUN, vec![quiet], fixtures::asking()), )); - let source = TypedCore::::new(functions); - let effects = super::super::EffectPlan::analyze(&source.fns); - let plan = super::super::analysis::plan(&source.fns, &effects, false); + let source = UncheckedTypedCore::::new(functions); + let effects = super::super::EffectPlan::analyze(source.functions()); + let plan = super::super::analysis::plan(source.functions(), &effects, false); assert_eq!( plan.monadic_params.get(&Sym::from(fixtures::RUN)), Some(&BTreeSet::from([0])), "the performing call site is what makes the slot monadic", ); - let refusal = refusal_of(source.fns, &plan); + let refusal = refusal_of(source.into_functions(), &plan); assert_eq!( refusal, Decline::whole(Refusal::ThunkBoundary, Sym::from(fixtures::HELPER)), diff --git a/crates/prism-core/src/core/typed/effect_lower/plan.rs b/crates/prism-core/src/core/typed/effect_lower/plan.rs index 8fe466ac..2a94d951 100644 --- a/crates/prism-core/src/core/typed/effect_lower/plan.rs +++ b/crates/prism-core/src/core/typed/effect_lower/plan.rs @@ -1,45 +1,27 @@ -//! The canonical effect plan: the one authority on which operations a piece of -//! code can perform, and on the facts that force the free monad. +//! Reachable operations and the conditions that require free-monad lowering. //! -//! Every question of the form "which ops can this run" is answered from here, -//! computed once from [`ThunkFlow`] and the typed rows, so no pass carries a -//! private reachability walk that can drift from the one the cascade acts on. A -//! plan is a fact about one program tree, not about a compilation: the erasures -//! read a plan for the tree they receive and the cascade reads one for the -//! prepared tree. Those are the same computation applied to two trees, which is -//! the thing that was missing; two *implementations* is the defect. +//! [`EffectPlan`] centralizes operation reachability for the cascade and erasure +//! passes. Each pass computes it for the tree it receives. //! -//! Reachability is the least fixpoint of three contributions: +//! Reachability is the least fixpoint of: //! //! * the operations a body names directly (its `do`s, handler arms, and masks, //! including inside thunk literals), //! * the reachable set of every function it calls by name, and //! * the signatures that flowed into its thunk-valued parameters. //! -//! The third is what a by-name call graph cannot see. A thunk arriving as a -//! parameter and forced is not a named call, so a fixpoint over calls alone -//! reports that such a function performs nothing at all, and every guard reading -//! it waves the function through. That is not a precision loss, it is a wrong -//! answer: it let a multishot handler's `var` block collapse to one shared cell. +//! The third contribution covers forced thunk parameters, which do not appear as +//! named call-graph edges. //! -//! One residue is named rather than hidden. A thunk buried in a constructor or -//! tuple and extracted later is tracked by neither the call graph nor -//! `ThunkFlow`; [`EffectPlan::opaque_thunks`] reports exactly that condition, -//! and the region planner, which must not confine a region whose forcings it -//! cannot bound, reads it. +//! [`EffectPlan::opaque_thunks`] records thunks hidden in constructors or tuples, +//! whose forcing sites cannot be bounded by the call graph or [`ThunkFlow`]. //! -//! Capturing an effectful computation in a thunk is likewise recorded with its -//! precision attached rather than as one flag: [`EffectPlan::tracked_captures`] -//! are the captures whose forcing a signature already describes, and -//! [`EffectPlan::opaque_captures`] the rest. Both capture, so everything that -//! acts on the fact reads their union, [`EffectPlan::thunk_effects`]. +//! [`EffectPlan::tracked_captures`] contains captures described by a signature; +//! [`EffectPlan::opaque_captures`] contains the rest. Their union is available as +//! [`EffectPlan::thunk_effects`]. //! -//! Being a fact about one tree, it is a fact about that tree's effects, and the -//! erasures exist to remove those: on the tree they receive, every local `var` -//! block is itself an effectful thunk handed to a runner, so the flag is true -//! of precisely the state about to be rewritten away. The erasures therefore -//! read their own reachable set and not the flag, which would otherwise decline -//! a rewrite because the rewrite has not happened yet. +//! Erasure passes use the reachable set from their input tree because their work +//! removes the effectful state represented there. use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Write; diff --git a/crates/prism-core/src/core/typed/effect_lower/trampoline.rs b/crates/prism-core/src/core/typed/effect_lower/trampoline.rs index 99b7dedb..d84cb25c 100644 --- a/crates/prism-core/src/core/typed/effect_lower/trampoline.rs +++ b/crates/prism-core/src/core/typed/effect_lower/trampoline.rs @@ -596,8 +596,8 @@ pub fn trampolinize(functions: &[TypedCoreFn], fresh: &mut Fresh) -> Option::new(rewritten); - assert_eq!(verify(&typed, &env), Ok(())); + let typed = verify(UncheckedTypedCore::::new(rewritten), &env) + .expect("trampolined program verifies"); let erased = typed.erase(); assert!(matches!( @@ -765,10 +765,8 @@ mod tests { rewritten.push(prism_drive_fn()); let mut env = VerifyEnv::new(); abi::insert(&mut env); - assert_eq!( - verify(&TypedCore::::new(rewritten), &env), - Ok(()) - ); + verify(UncheckedTypedCore::::new(rewritten), &env) + .expect("residual-row trampoline verifies"); } #[test] @@ -804,8 +802,8 @@ mod tests { rewritten.push(prism_drive_fn()); let mut env = VerifyEnv::new(); abi::insert(&mut env); - let typed = TypedCore::::new(rewritten); - assert_eq!(verify(&typed, &env), Ok(())); + let typed = verify(UncheckedTypedCore::::new(rewritten), &env) + .expect("runtime-loop trampoline verifies"); let erased = typed.erase(); let driven = Sym::from(names::lowered("drv", 0)); assert!(matches!( diff --git a/crates/prism-core/src/core/typed/fuse.rs b/crates/prism-core/src/core/typed/fuse.rs index 8abb3e8d..5410fe79 100644 --- a/crates/prism-core/src/core/typed/fuse.rs +++ b/crates/prism-core/src/core/typed/fuse.rs @@ -40,7 +40,7 @@ use super::specialize_support::{ use super::verify::{substitute_core_type, union_rows}; use super::{ CompSig, CoreFnSig, CoreInstantiation, CoreType, TypedBinder, TypedComp, TypedCompKind, - TypedCore, TypedCoreFn, TypedPattern, TypedValue, TypedValueKind, + TypedCore, TypedCoreFn, TypedPattern, TypedValue, TypedValueKind, UncheckedTypedCore, }; // A seed whose symbolic driving takes more than this many reduction steps aborts @@ -147,10 +147,10 @@ struct Cx { /// configuration is left untouched (degrade to not fusing, never a partial /// rewrite). #[must_use] -pub fn fuse

(core: TypedCore

) -> (TypedCore

, FuseStats) { +pub fn fuse

(core: TypedCore

) -> (UncheckedTypedCore

, FuseStats) { + let source_functions = core.into_unchecked().into_functions(); let mut cx = Cx { - fns: core - .fns + fns: source_functions .iter() .map(|function| (function.name, function.clone())) .collect(), @@ -164,8 +164,7 @@ pub fn fuse

(core: TypedCore

) -> (TypedCore

, FuseStats) { // shared, so names are deterministic. When a body actually fused, its // now-dead upstream pipeline is removed by dead-let elimination, so the // fused loop stands alone instead of running beside a discarded allocation. - let mut fns: Vec = core - .fns + let mut fns: Vec = source_functions .into_iter() .map(|function| { let before = cx.joins; @@ -184,7 +183,7 @@ pub fn fuse

(core: TypedCore

) -> (TypedCore

, FuseStats) { .collect(); let ticks = u64::from(cx.joins); fns.append(&mut cx.emitted); - (TypedCore::new(fns), FuseStats { ticks }) + (UncheckedTypedCore::new(fns), FuseStats { ticks }) } // A value looked through any representation-only wrapper: those erase away @@ -2117,8 +2116,8 @@ mod tests { use crate::core::CoreOp; use crate::types::Type; - use super::super::verify::{verify, ConstructorSig, VerifyEnv}; - use super::super::Elaborated; + use super::super::verify::{ConstructorSig, VerifyEnv}; + use super::super::{verify, Elaborated}; use super::*; fn sym(name: &str) -> Sym { @@ -2461,14 +2460,11 @@ mod tests { functions: Vec, env: &VerifyEnv, ) -> (TypedCore, u64) { - let input = TypedCore::new(functions); - if let Err(violations) = verify(&input, env) { - panic!("input fixture is invalid: {violations:#?}"); - } + let input = verify(UncheckedTypedCore::::new(functions), env) + .unwrap_or_else(|violations| panic!("input fixture is invalid: {violations:#?}")); let (actual, stats) = fuse(input); - if let Err(violations) = verify(&actual, env) { - panic!("fused typed Core is invalid: {violations:#?}"); - } + let actual = verify(actual, env) + .unwrap_or_else(|violations| panic!("fused typed Core is invalid: {violations:#?}")); (actual, stats.ticks()) } diff --git a/crates/prism-core/src/core/typed/inline.rs b/crates/prism-core/src/core/typed/inline.rs index 3b4ac77e..ad3b7608 100644 --- a/crates/prism-core/src/core/typed/inline.rs +++ b/crates/prism-core/src/core/typed/inline.rs @@ -26,7 +26,7 @@ use super::verify::{ }; use super::{ CompSig, CoreInstantiation, TypedBinder, TypedComp, TypedCompKind, TypedCore, TypedCoreFn, - TypedValue, TypedValueKind, + TypedValue, TypedValueKind, UncheckedTypedCore, }; /// Rewrite counts for typed inlining. @@ -44,14 +44,18 @@ impl InlineStats { /// Inline single-call-site non-recursive functions, preserving every witness. #[must_use] -pub fn inline

(core: TypedCore

) -> (TypedCore

, InlineStats) { - let names: BTreeSet = core.fns.iter().map(|function| function.name).collect(); +pub fn inline

(core: TypedCore

) -> (UncheckedTypedCore

, InlineStats) { + let names: BTreeSet = core + .functions() + .iter() + .map(|function| function.name) + .collect(); // Per-function call-site count (Call heads) and whether it is ever used // first-class (as a value), across all bodies. let mut call_count: BTreeMap = BTreeMap::new(); let mut first_class: BTreeSet = BTreeSet::new(); - for function in &core.fns { + for function in core.functions() { for head in calls_in(&function.body) { *call_count.entry(head).or_default() += 1; } @@ -75,12 +79,12 @@ pub fn inline

(core: TypedCore

) -> (TypedCore

, InlineStats) { }) .collect(); if inlinable.is_empty() { - return (core, InlineStats::default()); + return (core.into_unchecked(), InlineStats::default()); } + let source_functions = core.into_unchecked().into_functions(); let mut inliner = Inliner { - fns: core - .fns + fns: source_functions .iter() .map(|function| (function.name, function.clone())) .collect(), @@ -88,8 +92,7 @@ pub fn inline

(core: TypedCore

) -> (TypedCore

, InlineStats) { ticks: 0, counter: 0, }; - let fns = core - .fns + let fns = source_functions .iter() .map(|function| { TypedCoreFn::new( @@ -102,7 +105,7 @@ pub fn inline

(core: TypedCore

) -> (TypedCore

, InlineStats) { }) .collect(); ( - TypedCore::new(fns), + UncheckedTypedCore::new(fns), InlineStats { ticks: inliner.ticks, }, @@ -113,7 +116,7 @@ pub fn inline

(core: TypedCore

) -> (TypedCore

, InlineStats) { // not terminate and would reshape the spines native codegen expects. fn recursive_set

(core: &TypedCore

, names: &BTreeSet) -> BTreeSet { let mut edges: BTreeMap> = BTreeMap::new(); - for function in &core.fns { + for function in core.functions() { let heads = calls_in(&function.body); edges.insert( function.name, @@ -432,9 +435,9 @@ mod tests { use crate::types::Type; use super::super::effect_lower::lower_effects; - use super::super::verify::{verify, OperationSig, VerifyEnv}; + use super::super::verify::{OperationSig, VerifyEnv}; use super::super::{ - CoreFnSig, CoreQuantifier, CoreType, EffectLowered, Elaborated, TypedLowering, + verify, CoreFnSig, CoreQuantifier, CoreType, EffectLowered, Elaborated, TypedLowering, }; use super::*; @@ -476,14 +479,11 @@ mod tests { } fn run_inline(functions: Vec, env: &VerifyEnv) -> (TypedCore, u64) { - let input = TypedCore::new(functions); - if let Err(violations) = verify(&input, env) { - panic!("input fixture is invalid: {violations:#?}"); - } + let input = verify(UncheckedTypedCore::::new(functions), env) + .unwrap_or_else(|violations| panic!("input fixture is invalid: {violations:#?}")); let (actual, stats) = inline(input); - if let Err(violations) = verify(&actual, env) { - panic!("inlined typed Core is invalid: {violations:#?}"); - } + let actual = verify(actual, env) + .unwrap_or_else(|violations| panic!("inlined typed Core is invalid: {violations:#?}")); (actual, stats.ticks()) } @@ -565,10 +565,13 @@ mod tests { ), 0, ); - let input = TypedCore::::new(vec![increment, main]); - if let Err(violations) = verify(&input, &env) { - panic!("elaborated late-pass fixture is invalid: {violations:#?}"); - } + let input = verify( + UncheckedTypedCore::::new(vec![increment, main]), + &env, + ) + .unwrap_or_else(|violations| { + panic!("elaborated late-pass fixture is invalid: {violations:#?}") + }); let flags = DynFlags { effect_tier: EffectTier::FreeMonad, quiet: true, @@ -586,9 +589,6 @@ mod tests { assert_eq!(strategy, EffectStrategy::SelectiveFreeMonad); assert!(ctors.contains_key("EPure")); assert!(ctors.contains_key("EOp")); - if let Err(violations) = verify(&lowered, &env) { - panic!("effect-lowered late-pass fixture is invalid: {violations:#?}"); - } let lowered_main = lowered .functions() .iter() @@ -633,13 +633,10 @@ mod tests { input: TypedCore, env: &VerifyEnv, ) -> (TypedCore, u64) { - if let Err(violations) = verify(&input, env) { - panic!("effect-lowered Inline input is invalid: {violations:#?}"); - } let (actual, stats) = inline(input); - if let Err(violations) = verify(&actual, env) { - panic!("effect-lowered Inline output is invalid: {violations:#?}"); - } + let actual = verify(actual, env).unwrap_or_else(|violations| { + panic!("effect-lowered Inline output is invalid: {violations:#?}") + }); (actual, stats.ticks()) } diff --git a/crates/prism-core/src/core/typed/newtypes.rs b/crates/prism-core/src/core/typed/newtypes.rs index 332cecbc..584c61f2 100644 --- a/crates/prism-core/src/core/typed/newtypes.rs +++ b/crates/prism-core/src/core/typed/newtypes.rs @@ -11,8 +11,8 @@ use crate::types::ty::EffRow; use prism_common::sym::Sym; use super::{ - instantiate_constructor, CompSig, TypedBinder, TypedComp, TypedCompKind, TypedCore, - TypedCoreFn, TypedHandler, TypedPattern, TypedValue, TypedValueKind, VerifyEnv, + instantiate_constructor, CompSig, TypedBinder, TypedComp, TypedCompKind, TypedCoreFn, + TypedHandler, TypedPattern, TypedValue, TypedValueKind, UncheckedTypedCore, VerifyEnv, }; /// Rewrite counts for typed newtype erasure. @@ -35,10 +35,10 @@ impl NewtypeEraseStats { /// independent verifier remains responsible for rejecting an invalid input. #[must_use] pub fn erase_newtypes

( - core: TypedCore

, + core: UncheckedTypedCore

, constructors: &BTreeSet, env: &VerifyEnv, -) -> (TypedCore

, NewtypeEraseStats) { +) -> (UncheckedTypedCore

, NewtypeEraseStats) { if constructors.is_empty() { return (core, NewtypeEraseStats::default()); } @@ -48,12 +48,12 @@ pub fn erase_newtypes

( ticks: 0, }; let functions = core - .fns + .into_functions() .into_iter() .map(|function| pass.function(function)) .collect(); ( - TypedCore::new(functions), + UncheckedTypedCore::new(functions), NewtypeEraseStats { ticks: pass.ticks }, ) } @@ -347,11 +347,12 @@ mod tests { use crate::types::Type; use super::*; + use crate::core::typed::violation::Violation; use crate::core::typed::{ - verify, ConstructorSig, CoreFnSig, CoreType, Elaborated, TypedCoreFn, + verify, ConstructorSig, CoreFnSig, CoreType, Elaborated, TypedCoreFn, UncheckedTypedCore, }; - fn fixture(mark_newtype: bool) -> (TypedCore, VerifyEnv, BTreeSet) { + fn fixture(mark_newtype: bool) -> (UncheckedTypedCore, VerifyEnv, BTreeSet) { let constructor = Sym::new("UserId"); let newtype = CoreType::Source(Type::Con(Sym::new("Id"), Vec::new())); let field = CoreType::Source(Type::Int); @@ -390,7 +391,7 @@ mod tests { )], ), ); - let typed = TypedCore::new(vec![TypedCoreFn::new( + let typed = UncheckedTypedCore::new(vec![TypedCoreFn::new( Sym::new("main"), Vec::new(), body, @@ -411,14 +412,15 @@ mod tests { #[test] fn typed_erasure_removes_the_constructor_box_and_the_irrefutable_match() { let (typed, env, constructors) = fixture(true); - verify(&typed, &env).expect("fixture is valid before newtype erasure"); + let checked = verify(typed.clone(), &env).expect("fixture is valid before newtype erasure"); assert!( - pp_core(&typed.clone().erase()).contains("UserId"), + pp_core(&checked.erase()).contains("UserId"), "the fixture must start with the newtype constructor present" ); let (rewritten, stats) = erase_newtypes(typed, &constructors, &env); - verify(&rewritten, &env).expect("newtype witnesses verify after the typed pass"); + let rewritten = + verify(rewritten, &env).expect("newtype witnesses verify after the typed pass"); let erased = rewritten.erase(); // Both the constructor box and the irrefutable constructor match are @@ -434,15 +436,14 @@ mod tests { #[test] fn verifier_rejects_one_field_data_forged_as_newtype_evidence() { let (typed, env, constructors) = fixture(false); - verify(&typed, &env).expect("ordinary one-field constructor is valid before coercion"); + verify(typed.clone(), &env) + .expect("ordinary one-field constructor is valid before coercion"); let (forged, _) = erase_newtypes(typed, &constructors, &env); - let violations = verify(&forged, &env) + let violations = verify(forged, &env) .expect_err("ordinary one-field data must not prove a newtype coercion"); - assert!(violations.iter().any(|violation| { - violation - .message() - .contains("representation coercion names non-newtype constructor") - })); + assert!(violations + .iter() + .any(|violation| matches!(violation.kind(), Violation::NotANewtype { .. }))); } } diff --git a/crates/prism-core/src/core/typed/rc.rs b/crates/prism-core/src/core/typed/rc.rs deleted file mode 100644 index f75f73e6..00000000 --- a/crates/prism-core/src/core/typed/rc.rs +++ /dev/null @@ -1,1582 +0,0 @@ -//! Reference-count insertion for witness-carrying Core. -//! -//! This is the typed counterpart of [`super::super::fbip::insert_rc`]. It keeps -//! the same ownership partition, free-variable decisions, borrow masks, and -//! name-stable insertion order while retaining the witness for every inserted -//! `dup` and `drop` operand. - -use std::collections::{BTreeMap, BTreeSet}; - -use crate::core::fbip::Sigs; -use crate::types::ty::EffRow; -use crate::types::Type; -use prism_common::fresh::Fresh; -use prism_common::sym::Sym; -use prism_syntax::names; - -use super::specialize_support::{free_comp_vars, BoundStack}; -use super::{ - CompSig, CoreType, EffectLowered, Owned, TypedBinder, TypedComp, TypedCompKind, TypedCore, - TypedCoreFn, TypedPattern, TypedValue, TypedValueKind, -}; - -type Set = BTreeSet; -type Scope = BTreeMap; - -/// Insert precise reference-count operations without erasing type witnesses. -#[must_use] -pub fn insert_rc(core: TypedCore, sigs: &Sigs) -> TypedCore { - let mut scope = reference_scope(&core); - let mut fresh = Fresh::new(); - let fns = core - .fns - .into_iter() - .map(|function| { - let mask = sigs.get(&function.name).map(Vec::as_slice); - let owned: Set = function - .params - .iter() - .enumerate() - .filter(|(index, _)| !borrowed_at(mask, *index)) - .map(|(_, binder)| binder.name) - .collect(); - let borrowed: Set = function - .params - .iter() - .enumerate() - .filter(|(index, _)| borrowed_at(mask, *index)) - .map(|(_, binder)| binder.name) - .collect(); - let undo = bind_scope(&mut scope, &function.params); - let body = rc( - &function.body, - &owned, - &borrowed, - sigs, - &mut scope, - &mut fresh, - ); - unbind_scope(&mut scope, undo); - TypedCoreFn::new( - function.name, - function.params, - body, - function.sig, - function.dict_arity, - ) - }) - .collect(); - TypedCore::new(fns) -} - -fn borrowed_at(mask: Option<&[bool]>, index: usize) -> bool { - mask.is_some_and(|entries| entries.get(index).copied().unwrap_or(false)) -} - -// `Sym` orders by intern id, which is intentionally unrelated to the stable -// emitted order. RC operations are therefore sorted by their textual names. -fn by_name(syms: impl IntoIterator) -> Vec { - let mut names: Vec = syms.into_iter().collect(); - names.sort_by(|lhs, rhs| lhs.as_str().cmp(rhs.as_str())); - names -} - -fn binder_value(binder: &TypedBinder) -> TypedValue { - TypedValue::new( - binder.ty.clone(), - TypedValueKind::Var { - name: binder.name, - instantiation: Vec::new(), - }, - ) -} - -// The scope is one shared map mutated in place: cloning it per binder made -// deep bind chains quadratic in the number of globals plus locals. Each entry -// records the value it displaced so a reverse replay restores the enclosing -// scope exactly, including a shadowed global or outer local of the same name. -type ScopeUndo = Vec<(Sym, Option)>; - -fn bind_scope(scope: &mut Scope, binders: &[TypedBinder]) -> ScopeUndo { - binders - .iter() - .map(|binder| (binder.name, scope.insert(binder.name, binder_value(binder)))) - .collect() -} - -fn unbind_scope(scope: &mut Scope, undo: ScopeUndo) { - for (name, displaced) in undo.into_iter().rev() { - match displaced { - Some(value) => { - scope.insert(name, value); - } - None => { - scope.remove(&name); - } - } - } -} - -// Unlike the effect-lowering cascade, RC has no downgrade: it runs once on the -// committed lowered tree and emits the dup/drop operations codegen relies on -// for memory safety. A missing scope entry means the RC pass cannot know the -// operand's runtime representation, and a guessed representation would emit a -// mistyped dup/drop (a leak or a use-after-free), strictly worse than a loud -// failure. So this invariant deliberately stays a hard check rather than a -// silent decline; it is unreachable on verified input, which the typed -// verifier guarantees before RC ever runs. -fn operand(scope: &Scope, name: Sym) -> TypedValue { - scope - .get(&name) - .unwrap_or_else(|| panic!("verified RC operand {name} is out of scope")) - .clone() -} - -const fn pure_unit() -> CompSig { - CompSig::new(CoreType::Source(Type::Unit), EffRow::Empty) -} - -fn seq(op: TypedComp, continuation: TypedComp) -> TypedComp { - TypedComp::new( - continuation.sig.clone(), - TypedCompKind::Bind( - Box::new(op), - TypedBinder::rc_sequence(), - Box::new(continuation), - ), - ) -} - -fn dup(name: Sym, continuation: TypedComp, scope: &Scope) -> TypedComp { - seq( - TypedComp::new(pure_unit(), TypedCompKind::Dup(operand(scope, name))), - continuation, - ) -} - -fn drop_(name: Sym, continuation: TypedComp, scope: &Scope) -> TypedComp { - seq( - TypedComp::new(pure_unit(), TypedCompKind::Drop(operand(scope, name))), - continuation, - ) -} - -fn erased_var(value: &TypedValue) -> Option { - match &value.kind { - TypedValueKind::Var { name, .. } => Some(*name), - TypedValueKind::Reinterpret(inner) - | TypedValueKind::LoweredRepr { - value: inner, - proof: _, - } - | TypedValueKind::NewtypeRepr { value: inner, .. } => erased_var(inner), - _ => None, - } -} - -fn borrowed_call_vars(comp: &TypedComp, sigs: &Sigs) -> Set { - let TypedCompKind::Call { callee, args, .. } = &comp.kind else { - return Set::new(); - }; - let mask = sigs.get(callee).map(Vec::as_slice); - args.iter() - .enumerate() - .filter(|(index, _)| borrowed_at(mask, *index)) - // The retained set names the caller's own variables a borrowed - // position leaves owned. A borrowed position holding a non-variable - // (a literal or a freshly built value) has no prior owner to retain, - // so it is correctly absent from the set: skip it rather than crash. - // On verified input every borrowed argument is an erased variable, so - // this filter drops nothing there. - .filter_map(|(_, arg)| erased_var(arg)) - .collect() -} - -fn defer_call_drops( - call: TypedComp, - deferred: &Set, - scope: &Scope, - fresh: &mut Fresh, -) -> TypedComp { - let result = TypedBinder::new( - Sym::from(names::fresh_binder(names::FRESH_RC, fresh.bump())), - call.sig.result.clone(), - ); - let returned = TypedValue::new( - result.ty.clone(), - TypedValueKind::Var { - name: result.name, - instantiation: Vec::new(), - }, - ); - let mut post = TypedComp::new( - CompSig::new(result.ty.clone(), EffRow::Empty), - TypedCompKind::Return(returned), - ); - for name in by_name(deferred.iter().copied()) { - post = drop_(name, post, scope); - } - TypedComp::new( - call.sig.clone(), - TypedCompKind::Bind(Box::new(call), result, Box::new(post)), - ) -} - -#[allow(clippy::too_many_lines)] -fn rc( - comp: &TypedComp, - owned: &Set, - borrowed: &Set, - sigs: &Sigs, - scope: &mut Scope, - fresh: &mut Fresh, -) -> TypedComp { - match &comp.kind { - TypedCompKind::Bind(..) => rc_bind_spine(comp, owned, borrowed, sigs, scope, fresh), - TypedCompKind::If(condition, yes, no) => TypedComp::new( - comp.sig.clone(), - TypedCompKind::If( - condition.clone(), - Box::new(rc(yes, owned, borrowed, sigs, scope, fresh)), - Box::new(rc(no, owned, borrowed, sigs, scope, fresh)), - ), - ), - TypedCompKind::Case(scrutinee, arms) => TypedComp::new( - comp.sig.clone(), - TypedCompKind::Case( - scrutinee.clone(), - arms.iter() - .map(|(pattern, body)| { - ( - pattern.clone(), - rc_arm(pattern, body, owned, borrowed, sigs, scope, fresh), - ) - }) - .collect(), - ), - ), - TypedCompKind::Lam(params, body) => { - let params_set: Set = params.iter().map(|binder| binder.name).collect(); - let captures: Set = free_comp_vars(body) - .difference(¶ms_set) - .copied() - .collect(); - let undo = bind_scope(scope, params); - let body = rc(body, ¶ms_set, &captures, sigs, scope, fresh); - unbind_scope(scope, undo); - TypedComp::new( - comp.sig.clone(), - TypedCompKind::Lam(params.clone(), Box::new(body)), - ) - } - TypedCompKind::Mask(effects, body) => TypedComp::new( - comp.sig.clone(), - TypedCompKind::Mask( - effects.clone(), - Box::new(rc(body, owned, borrowed, sigs, scope, fresh)), - ), - ), - // Effect lowering eliminates every `Handle` before RC runs: the - // `EffectLowered` marker means handlers have already been rewritten into - // evidence threading, state passing, or the free-monad driver. A handler - // surviving to RC is a structural IR-invariant violation with no correct - // reference-count treatment (there is no runtime handler to count - // against), so it is a genuine compiler bug, not a case to handle. This - // is deliberately a hard invariant, unlike the tier cascade's silent - // declines: RC is post-commit and has no downgrade. - TypedCompKind::Handle { .. } => { - unreachable!("effect lowering removes every Handle before reference counting") - } - _ => { - let mut counts = BTreeMap::new(); - leaf_counts(comp, &mut counts, sigs); - let borrowed_call = borrowed_call_vars(comp, sigs); - let deferred: Set = owned.intersection(&borrowed_call).copied().collect(); - let mut out = rc_thunks(comp, sigs, scope, fresh); - if !deferred.is_empty() { - out = defer_call_drops(out, &deferred, scope, fresh); - } - for name in by_name(owned.iter().copied()) { - let count = counts.get(&name).copied().unwrap_or(0); - if deferred.contains(&name) { - for _ in 0..count { - out = dup(name, out, scope); - } - } else { - match count { - 0 => out = drop_(name, out, scope), - count => { - for _ in 1..count { - out = dup(name, out, scope); - } - } - } - } - } - for name in by_name(borrowed.iter().copied()) { - for _ in 0..counts.get(&name).copied().unwrap_or(0) { - out = dup(name, out, scope); - } - } - out - } - } -} - -/// One right-spine `Bind` level and the free-variable facts its ownership -/// partition needs. -struct SpineStep<'a> { - sig: &'a CompSig, - first: &'a TypedComp, - binder: &'a TypedBinder, - first_free: Set, - /// How many suffix components reference the binder's name while it is in - /// scope; the forward pass restores this count once the level is done. - prev_count: u32, -} - -/// A rewritten spine level, ready to be reassembled from the tail outward. -struct SpineLevel<'a> { - sig: &'a CompSig, - binder: &'a TypedBinder, - first: TypedComp, - shared_ops: Vec, - dead_ops: Vec, -} - -/// Rewrite a right-leaning `Bind` chain in one pass over its levels. -/// -/// A per-level recursion would recompute `free_comp_vars` on both subtrees at -/// every step, which is quadratic in the chain length. This walk derives the -/// same facts bottom-up: a backward pass over the spine accumulates a count of -/// how many suffix components reference each name, and the forward pass peels -/// one component's contribution back off per level, leaving exactly the -/// membership the recursive formulation computed from scratch. Counting -/// components (not occurrences) suffices because every decision below is a -/// set-membership test. The ownership partition, operand resolution point, -/// and dup/drop wrap order are unchanged, so the emitted tree is identical. -#[allow(clippy::too_many_lines)] -fn rc_bind_spine( - comp: &TypedComp, - owned: &Set, - borrowed: &Set, - sigs: &Sigs, - scope: &mut Scope, - fresh: &mut Fresh, -) -> TypedComp { - let mut steps = Vec::new(); - let mut cursor = comp; - while let TypedCompKind::Bind(first, binder, rest) = &cursor.kind { - steps.push(SpineStep { - sig: &cursor.sig, - first, - binder, - first_free: free_comp_vars(first), - prev_count: 0, - }); - cursor = rest; - } - let tail = cursor; - - // Backward pass: `live` maps each name to the number of remaining spine - // components (suffix firsts plus the tail) in which it occurs free. A - // binder's occurrences are bound over its rest, so its count is saved and - // withdrawn before the defining component's own free set is added back - // (where the same name may legitimately reference an outer binding). - let mut live: BTreeMap = free_comp_vars(tail) - .into_iter() - .map(|name| (name, 1)) - .collect(); - for step in steps.iter_mut().rev() { - step.prev_count = live.remove(&step.binder.name).unwrap_or(0); - for name in &step.first_free { - *live.entry(*name).or_insert(0) += 1; - } - } - - // Forward pass: at each level, removing the defining component's - // contribution leaves `live` keyed by exactly the free variables of the - // chain rest with the binder excluded, the `rest_free` of the recursive - // formulation. Ownership then splits as before: names live on both sides - // are dupped, names live on neither are dropped, and the binder joins the - // owned set for the rest of the chain. - let mut owned = owned.clone(); - let mut borrowed = borrowed.clone(); - let mut undo: ScopeUndo = Vec::with_capacity(steps.len()); - let mut levels: Vec> = Vec::with_capacity(steps.len()); - for step in &steps { - for name in &step.first_free { - if let Some(count) = live.get_mut(name) { - if *count > 1 { - *count -= 1; - } else { - live.remove(name); - } - } - } - let first_owned: Set = owned - .iter() - .filter(|name| step.first_free.contains(*name)) - .copied() - .collect(); - let mut rest_owned: Set = owned - .iter() - .filter(|name| live.contains_key(*name)) - .copied() - .collect(); - let shared = by_name( - first_owned - .iter() - .filter(|name| rest_owned.contains(*name)) - .copied(), - ); - let dead = by_name( - owned - .iter() - .filter(|name| !step.first_free.contains(*name) && !live.contains_key(*name)) - .copied(), - ); - let first_borrowed: Set = borrowed - .iter() - .filter(|name| step.first_free.contains(*name)) - .copied() - .collect(); - let rest_borrowed: Set = borrowed - .iter() - .filter(|name| live.contains_key(*name)) - .copied() - .collect(); - // Dup/drop operands resolve against the scope enclosing this level, - // before the binder is visible, exactly as the wraps are emitted. - let shared_ops: Vec = shared.iter().map(|name| operand(scope, *name)).collect(); - let dead_ops: Vec = dead.iter().map(|name| operand(scope, *name)).collect(); - let first = rc( - step.first, - &first_owned, - &first_borrowed, - sigs, - scope, - fresh, - ); - undo.push(( - step.binder.name, - scope.insert(step.binder.name, binder_value(step.binder)), - )); - rest_owned.insert(step.binder.name); - owned = rest_owned; - borrowed = rest_borrowed; - if step.prev_count > 0 { - live.insert(step.binder.name, step.prev_count); - } - levels.push(SpineLevel { - sig: step.sig, - binder: step.binder, - first, - shared_ops, - dead_ops, - }); - } - let mut out = rc(tail, &owned, &borrowed, sigs, scope, fresh); - unbind_scope(scope, undo); - - // Reassemble from the tail outward; per level the dups wrap the bind and - // the drops wrap the dups, each in ascending name order. - for level in levels.into_iter().rev() { - out = TypedComp::new( - level.sig.clone(), - TypedCompKind::Bind(Box::new(level.first), level.binder.clone(), Box::new(out)), - ); - for value in level.shared_ops { - out = seq(TypedComp::new(pure_unit(), TypedCompKind::Dup(value)), out); - } - for value in level.dead_ops { - out = seq(TypedComp::new(pure_unit(), TypedCompKind::Drop(value)), out); - } - } - out -} - -// A thunk cell owns its captures. The suspended body therefore treats captures -// as borrowed while lambda parameters remain owned. -fn rc_value(value: &TypedValue, sigs: &Sigs, scope: &mut Scope, fresh: &mut Fresh) -> TypedValue { - let kind = match &value.kind { - TypedValueKind::Thunk(body) => TypedValueKind::Thunk(Box::new(rc( - body, - &Set::new(), - &free_comp_vars(body), - sigs, - scope, - fresh, - ))), - TypedValueKind::Ctor { - name, - tag, - instantiation, - fields, - } => TypedValueKind::Ctor { - name: *name, - tag: *tag, - instantiation: instantiation.clone(), - fields: fields - .iter() - .map(|field| rc_value(field, sigs, scope, fresh)) - .collect(), - }, - TypedValueKind::Tuple(fields) => TypedValueKind::Tuple( - fields - .iter() - .map(|field| rc_value(field, sigs, scope, fresh)) - .collect(), - ), - TypedValueKind::UnboxedTuple(fields) => TypedValueKind::UnboxedTuple( - fields - .iter() - .map(|field| rc_value(field, sigs, scope, fresh)) - .collect(), - ), - TypedValueKind::UnboxedRecord(fields) => TypedValueKind::UnboxedRecord( - fields - .iter() - .map(|(name, field)| (*name, rc_value(field, sigs, scope, fresh))) - .collect(), - ), - TypedValueKind::Reinterpret(inner) => { - TypedValueKind::Reinterpret(Box::new(rc_value(inner, sigs, scope, fresh))) - } - TypedValueKind::LoweredRepr { value, proof } => TypedValueKind::LoweredRepr { - value: Box::new(rc_value(value, sigs, scope, fresh)), - proof: proof.clone(), - }, - TypedValueKind::NewtypeRepr { - constructor, - instantiation, - value, - } => TypedValueKind::NewtypeRepr { - constructor: *constructor, - instantiation: instantiation.clone(), - value: Box::new(rc_value(value, sigs, scope, fresh)), - }, - _ => return value.clone(), - }; - TypedValue::new(value.ty.clone(), kind) -} - -fn rc_thunks(comp: &TypedComp, sigs: &Sigs, scope: &mut Scope, fresh: &mut Fresh) -> TypedComp { - let kind = match &comp.kind { - TypedCompKind::Return(result) => { - TypedCompKind::Return(rc_value(result, sigs, scope, fresh)) - } - TypedCompKind::Force(thunk) => TypedCompKind::Force(rc_value(thunk, sigs, scope, fresh)), - TypedCompKind::Error(error) => TypedCompKind::Error(rc_value(error, sigs, scope, fresh)), - TypedCompKind::Io(op, args) => TypedCompKind::Io( - *op, - args.iter() - .map(|arg| rc_value(arg, sigs, scope, fresh)) - .collect(), - ), - TypedCompKind::FloatBuiltin(op, arg) => { - TypedCompKind::FloatBuiltin(*op, rc_value(arg, sigs, scope, fresh)) - } - TypedCompKind::Neg(lane, arg) => { - TypedCompKind::Neg(*lane, rc_value(arg, sigs, scope, fresh)) - } - TypedCompKind::Prim(op, lhs, rhs) => TypedCompKind::Prim( - *op, - rc_value(lhs, sigs, scope, fresh), - rc_value(rhs, sigs, scope, fresh), - ), - TypedCompKind::Call { - callee, - instantiation, - args, - } => TypedCompKind::Call { - callee: *callee, - instantiation: instantiation.clone(), - args: args - .iter() - .map(|arg| rc_value(arg, sigs, scope, fresh)) - .collect(), - }, - TypedCompKind::Do { - operation, - instantiation, - args, - } => TypedCompKind::Do { - operation: *operation, - instantiation: instantiation.clone(), - args: args - .iter() - .map(|arg| rc_value(arg, sigs, scope, fresh)) - .collect(), - }, - TypedCompKind::StrBuiltin { - op, - instantiation, - args, - } => TypedCompKind::StrBuiltin { - op: *op, - instantiation: instantiation.clone(), - args: args - .iter() - .map(|arg| rc_value(arg, sigs, scope, fresh)) - .collect(), - }, - TypedCompKind::App { - callee, - instantiation, - args, - } => TypedCompKind::App { - callee: Box::new(rc_thunks(callee, sigs, scope, fresh)), - instantiation: instantiation.clone(), - args: args - .iter() - .map(|arg| rc_value(arg, sigs, scope, fresh)) - .collect(), - }, - TypedCompKind::RefNew(initial) => { - TypedCompKind::RefNew(rc_value(initial, sigs, scope, fresh)) - } - TypedCompKind::RefGet(cell) => TypedCompKind::RefGet(rc_value(cell, sigs, scope, fresh)), - TypedCompKind::RefSet(cell, new_value) => TypedCompKind::RefSet( - rc_value(cell, sigs, scope, fresh), - rc_value(new_value, sigs, scope, fresh), - ), - TypedCompKind::InitAt(cell, ctor) => TypedCompKind::InitAt( - rc_value(cell, sigs, scope, fresh), - rc_value(ctor, sigs, scope, fresh), - ), - _ => return comp.clone(), - }; - TypedComp::new(comp.sig.clone(), kind) -} - -fn pattern_binders(pattern: &TypedPattern) -> Vec { - match pattern { - TypedPattern::Wild => Vec::new(), - TypedPattern::Var(binder) => vec![binder.clone()], - TypedPattern::Ctor { fields, .. } | TypedPattern::Tuple(fields) => { - fields.iter().flatten().cloned().collect() - } - } -} - -fn rc_arm( - pattern: &TypedPattern, - body: &TypedComp, - owned: &Set, - borrowed: &Set, - sigs: &Sigs, - scope: &mut Scope, - fresh: &mut Fresh, -) -> TypedComp { - let body_free = free_comp_vars(body); - let binders = pattern_binders(pattern); - let fields: Set = binders.iter().map(|binder| binder.name).collect(); - let live = by_name(fields.intersection(&body_free).copied()); - let dead = by_name( - owned - .iter() - .filter(|name| !body_free.contains(*name)) - .copied(), - ); - let mut body_owned: Set = owned.intersection(&body_free).copied().collect(); - body_owned.extend(live.iter().copied()); - let body_borrowed: Set = borrowed.intersection(&body_free).copied().collect(); - // The wraps resolve against the arm scope (fields visible), so the arm's - // binders stay installed until after they are emitted. - let undo = bind_scope(scope, &binders); - let mut out = rc(body, &body_owned, &body_borrowed, sigs, scope, fresh); - for name in &dead { - out = drop_(*name, out, scope); - } - for name in live.iter().rev() { - out = dup(*name, out, scope); - } - unbind_scope(scope, undo); - out -} - -fn count_value(value: &TypedValue, counts: &mut BTreeMap) { - match &value.kind { - TypedValueKind::Var { name, .. } => *counts.entry(*name).or_default() += 1, - TypedValueKind::Ctor { fields, .. } - | TypedValueKind::Tuple(fields) - | TypedValueKind::UnboxedTuple(fields) => { - for field in fields { - count_value(field, counts); - } - } - TypedValueKind::UnboxedRecord(fields) => { - for (_, field) in fields { - count_value(field, counts); - } - } - TypedValueKind::Thunk(body) => { - for name in free_comp_vars(body) { - *counts.entry(name).or_default() += 1; - } - } - TypedValueKind::Reinterpret(inner) - | TypedValueKind::LoweredRepr { - value: inner, - proof: _, - } - | TypedValueKind::NewtypeRepr { value: inner, .. } => count_value(inner, counts), - TypedValueKind::Int(_) - | TypedValueKind::I64(_) - | TypedValueKind::U64(_) - | TypedValueKind::Float(_) - | TypedValueKind::Bool(_) - | TypedValueKind::Unit - | TypedValueKind::Str(_) => {} - } -} - -fn leaf_counts(comp: &TypedComp, counts: &mut BTreeMap, sigs: &Sigs) { - match &comp.kind { - TypedCompKind::Return(value) - | TypedCompKind::Force(value) - | TypedCompKind::Error(value) - | TypedCompKind::FloatBuiltin(_, value) - | TypedCompKind::Neg(_, value) - | TypedCompKind::RefNew(value) - | TypedCompKind::RefGet(value) => count_value(value, counts), - TypedCompKind::RefSet(cell, value) | TypedCompKind::InitAt(cell, value) => { - count_value(cell, counts); - count_value(value, counts); - } - TypedCompKind::App { callee, args, .. } => { - for name in free_comp_vars(callee) { - *counts.entry(name).or_default() += 1; - } - for arg in args { - count_value(arg, counts); - } - } - TypedCompKind::Prim(_, lhs, rhs) => { - count_value(lhs, counts); - count_value(rhs, counts); - } - TypedCompKind::Call { callee, args, .. } => { - let mask = sigs.get(callee).map(Vec::as_slice); - for (index, arg) in args.iter().enumerate() { - if !borrowed_at(mask, index) { - count_value(arg, counts); - } - } - } - TypedCompKind::Do { args, .. } - | TypedCompKind::StrBuiltin { args, .. } - | TypedCompKind::Io(_, args) => { - for arg in args { - count_value(arg, counts); - } - } - TypedCompKind::Bind(_, _, _) - | TypedCompKind::Lam(_, _) - | TypedCompKind::If(_, _, _) - | TypedCompKind::Case(_, _) - | TypedCompKind::Handle { .. } - | TypedCompKind::Mask(_, _) - | TypedCompKind::UnboxedProject(_, _) - | TypedCompKind::Dup(_) - | TypedCompKind::Drop(_) - | TypedCompKind::WithReuse { .. } - | TypedCompKind::Reuse(_, _) => {} - } -} - -// A polymorphic global may occur at several instances. RC operations inspect -// only its runtime word, so the first verified, lexically unshadowed occurrence -// is a sufficient operand witness for every inserted operation on that symbol. -// The declared-signature fallback is used only when no value occurrence exists. -fn reference_scope(core: &TypedCore) -> Scope { - let globals: Set = core.fns.iter().map(|function| function.name).collect(); - let mut scope = Scope::new(); - for function in &core.fns { - let mut bound = BoundStack::new(); - bound.push_all(function.params.iter().map(|binder| binder.name)); - collect_global_refs_comp(&function.body, &globals, &mut bound, &mut scope); - } - for function in &core.fns { - scope.entry(function.name).or_insert_with(|| { - TypedValue::new( - CoreType::Function(Box::new(function.sig.clone())), - TypedValueKind::Var { - name: function.name, - instantiation: Vec::new(), - }, - ) - }); - } - scope -} - -fn collect_global_refs_value( - value: &TypedValue, - globals: &Set, - bound: &mut BoundStack, - scope: &mut Scope, -) { - match &value.kind { - TypedValueKind::Var { name, .. } => { - if globals.contains(name) && !bound.contains(*name) { - scope.entry(*name).or_insert_with(|| value.clone()); - } - } - TypedValueKind::Reinterpret(inner) - | TypedValueKind::LoweredRepr { - value: inner, - proof: _, - } - | TypedValueKind::NewtypeRepr { value: inner, .. } => { - collect_global_refs_value(inner, globals, bound, scope); - } - TypedValueKind::Thunk(body) => collect_global_refs_comp(body, globals, bound, scope), - TypedValueKind::Ctor { fields, .. } - | TypedValueKind::Tuple(fields) - | TypedValueKind::UnboxedTuple(fields) => { - for field in fields { - collect_global_refs_value(field, globals, bound, scope); - } - } - TypedValueKind::UnboxedRecord(fields) => { - for (_, field) in fields { - collect_global_refs_value(field, globals, bound, scope); - } - } - TypedValueKind::Int(_) - | TypedValueKind::I64(_) - | TypedValueKind::U64(_) - | TypedValueKind::Float(_) - | TypedValueKind::Bool(_) - | TypedValueKind::Unit - | TypedValueKind::Str(_) => {} - } -} - -#[allow(clippy::too_many_lines)] -fn collect_global_refs_comp( - comp: &TypedComp, - globals: &Set, - bound: &mut BoundStack, - scope: &mut Scope, -) { - match &comp.kind { - TypedCompKind::Return(value) - | TypedCompKind::Force(value) - | TypedCompKind::Error(value) - | TypedCompKind::FloatBuiltin(_, value) - | TypedCompKind::Neg(_, value) - | TypedCompKind::UnboxedProject(value, _) - | TypedCompKind::Dup(value) - | TypedCompKind::Drop(value) - | TypedCompKind::RefNew(value) - | TypedCompKind::RefGet(value) - | TypedCompKind::Reuse(_, value) => { - collect_global_refs_value(value, globals, bound, scope); - } - TypedCompKind::Prim(_, lhs, rhs) - | TypedCompKind::RefSet(lhs, rhs) - | TypedCompKind::InitAt(lhs, rhs) => { - collect_global_refs_value(lhs, globals, bound, scope); - collect_global_refs_value(rhs, globals, bound, scope); - } - TypedCompKind::Bind(first, binder, rest) => { - collect_global_refs_comp(first, globals, bound, scope); - let mark = bound.mark(); - bound.push(binder.name); - collect_global_refs_comp(rest, globals, bound, scope); - bound.pop_to(mark); - } - TypedCompKind::Lam(params, body) => { - let mark = bound.mark(); - bound.push_all(params.iter().map(|binder| binder.name)); - collect_global_refs_comp(body, globals, bound, scope); - bound.pop_to(mark); - } - TypedCompKind::Mask(_, body) => { - collect_global_refs_comp(body, globals, bound, scope); - } - TypedCompKind::App { callee, args, .. } => { - collect_global_refs_comp(callee, globals, bound, scope); - for arg in args { - collect_global_refs_value(arg, globals, bound, scope); - } - } - TypedCompKind::If(condition, yes, no) => { - collect_global_refs_value(condition, globals, bound, scope); - collect_global_refs_comp(yes, globals, bound, scope); - collect_global_refs_comp(no, globals, bound, scope); - } - TypedCompKind::Call { args, .. } - | TypedCompKind::Io(_, args) - | TypedCompKind::Do { args, .. } - | TypedCompKind::StrBuiltin { args, .. } => { - for arg in args { - collect_global_refs_value(arg, globals, bound, scope); - } - } - TypedCompKind::Case(scrutinee, arms) => { - collect_global_refs_value(scrutinee, globals, bound, scope); - for (pattern, body) in arms { - let mark = bound.mark(); - bound.push_all(pattern_binders(pattern).iter().map(|binder| binder.name)); - collect_global_refs_comp(body, globals, bound, scope); - bound.pop_to(mark); - } - } - TypedCompKind::Handle { - body, - return_binder, - return_body, - ops, - } => { - collect_global_refs_comp(body, globals, bound, scope); - if let Some(return_body) = return_body { - let mark = bound.mark(); - bound.push_all(return_binder.iter().map(|binder| binder.name)); - collect_global_refs_comp(return_body, globals, bound, scope); - bound.pop_to(mark); - } - for arm in &ops.arms { - let mark = bound.mark(); - bound.push_all(arm.params.iter().map(|binder| binder.name)); - bound.push(arm.resume.name); - collect_global_refs_comp(&arm.body, globals, bound, scope); - bound.pop_to(mark); - } - } - TypedCompKind::WithReuse { token, freed, body } => { - collect_global_refs_value(freed, globals, bound, scope); - let mark = bound.mark(); - bound.push(token.name); - collect_global_refs_comp(body, globals, bound, scope); - bound.pop_to(mark); - } - } -} - -#[cfg(test)] -mod tests { - use crate::core::{Comp, Value}; - use crate::types::ty::Label; - use crate::types::Type; - use prism_syntax::names::ALLOC_OP; - - use super::super::specialize_support::count_free_comp_var_visits; - use super::super::verify::{verify, OperationSig, VerifyEnv}; - use super::super::{ - CoreFnSig, CoreInstantiation, CoreQuantifier, LoweredType, TypedHandler, TypedValueKind, - }; - use super::*; - - fn sym(name: &str) -> Sym { - Sym::new(name) - } - - fn source(ty: Type) -> CoreType { - CoreType::Source(ty) - } - - fn pure(result: CoreType) -> CompSig { - CompSig::new(result, EffRow::Empty) - } - - fn var(name: &str, ty: CoreType) -> TypedValue { - TypedValue::new( - ty, - TypedValueKind::Var { - name: sym(name), - instantiation: Vec::new(), - }, - ) - } - - fn ret(value: TypedValue) -> TypedComp { - TypedComp::new(pure(value.ty.clone()), TypedCompKind::Return(value)) - } - - fn function(name: &str, params: Vec, body: TypedComp) -> TypedCoreFn { - let signature = CoreFnSig::new( - Vec::new(), - params.iter().map(|binder| binder.ty.clone()).collect(), - body.sig.clone(), - ); - TypedCoreFn::new(sym(name), params, body, signature, 0) - } - - fn head_dup<'a>(comp: &'a Comp, name: &str) -> &'a Comp { - let Comp::Bind(op, binder, rest) = comp else { - panic!("expected a leading dup, found {comp:?}"); - }; - assert_eq!(binder.as_str(), "_"); - assert!(matches!( - &**op, - Comp::Dup(Value::Var(actual)) if *actual == sym(name) - )); - rest - } - - fn head_drop<'a>(comp: &'a Comp, name: &str) -> &'a Comp { - let Comp::Bind(op, binder, rest) = comp else { - panic!("expected a leading drop, found {comp:?}"); - }; - assert_eq!(binder.as_str(), "_"); - assert!(matches!( - &**op, - Comp::Drop(Value::Var(actual)) if *actual == sym(name) - )); - rest - } - - fn run_and_verify( - input: &TypedCore, - sigs: &Sigs, - env: &VerifyEnv, - ) -> TypedCore { - if let Err(violations) = verify(input, env) { - panic!("input fixture is invalid: {violations:#?}"); - } - let actual = insert_rc(input.clone(), sigs); - if let Err(violations) = verify(&actual, env) { - panic!("owned typed Core is invalid: {violations:#?}"); - } - actual - } - - // `EffectLowered` promises that no source handler remains. If a compiler - // bug forges that phase marker, RC must fail before emitting a transform - // whose ownership treatment would be unsound. - #[test] - #[should_panic(expected = "effect lowering removes every Handle before reference counting")] - fn surviving_handle_fails_closed_before_rc_insertion() { - let unit = source(Type::Unit); - let body = ret(TypedValue::new(unit.clone(), TypedValueKind::Unit)); - let handled = TypedComp::new( - pure(unit), - TypedCompKind::Handle { - body: Box::new(body), - return_binder: None, - return_body: None, - ops: TypedHandler::new(Vec::new()).unwrap(), - }, - ); - let input: TypedCore = - TypedCore::new(vec![function("main", Vec::new(), handled)]); - let _ = insert_rc(input, &Sigs::new()); - } - - #[test] - fn borrow_masks_preserve_the_calling_convention() { - let int = source(Type::Int); - let parameter = TypedBinder::new(sym("borrowed"), int.clone()); - let body = ret(var("borrowed", int)); - let observe = function("observe", vec![parameter], body); - let retained = TypedBinder::new(sym("retained"), source(Type::Int)); - let call = TypedComp::new( - pure(source(Type::Int)), - TypedCompKind::Call { - callee: sym("observe"), - instantiation: Vec::new(), - args: vec![var("retained", source(Type::Int))], - }, - ); - let caller = function("caller", vec![retained], call); - let input = TypedCore::new(vec![observe, caller]); - let sigs = std::iter::once((sym("observe"), vec![true])).collect(); - let actual = run_and_verify(&input, &sigs, &VerifyEnv::new()).erase(); - let observe_rest = head_dup(&actual.fns[0].body, "borrowed"); - assert!(matches!( - observe_rest, - Comp::Return(Value::Var(name)) if *name == sym("borrowed") - )); - let Comp::Bind(call, result, post) = &actual.fns[1].body else { - panic!("borrowed tail call must retain its argument through the call"); - }; - assert!(matches!( - &**call, - Comp::Call(name, args) - if *name == sym("observe") - && matches!(args.as_slice(), [Value::Var(arg)] if *arg == sym("retained")) - )); - assert_eq!(result.as_str(), "%rc0"); - let returned = head_drop(post, "retained"); - assert!(matches!( - returned, - Comp::Return(Value::Var(name)) if name == result - )); - } - - #[test] - fn an_owned_and_borrowed_alias_keeps_a_loan_token_through_the_call() { - let int = source(Type::Int); - let owned = TypedBinder::new(sym("owned"), int.clone()); - let loan = TypedBinder::new(sym("loan"), int.clone()); - let callee = function( - "consume_and_borrow", - vec![owned, loan], - ret(var("owned", int.clone())), - ); - let shared = TypedBinder::new(sym("shared"), int.clone()); - let call = TypedComp::new( - pure(int.clone()), - TypedCompKind::Call { - callee: sym("consume_and_borrow"), - instantiation: Vec::new(), - args: vec![var("shared", int.clone()), var("shared", int)], - }, - ); - let invoking_function = function("caller", vec![shared], call); - let input = TypedCore::new(vec![callee, invoking_function]); - let sigs = std::iter::once((sym("consume_and_borrow"), vec![false, true])).collect(); - let actual = run_and_verify(&input, &sigs, &VerifyEnv::new()).erase(); - - let after_loan = head_dup(&actual.fns[1].body, "shared"); - let Comp::Bind(call, result, post) = after_loan else { - panic!("aliased call must defer loan cleanup"); - }; - assert!(matches!( - &**call, - Comp::Call(name, args) - if *name == sym("consume_and_borrow") - && matches!( - args.as_slice(), - [Value::Var(lhs), Value::Var(rhs)] - if *lhs == sym("shared") && *rhs == sym("shared") - ) - )); - assert_eq!(result.as_str(), "%rc0"); - let returned = head_drop(post, "shared"); - assert!(matches!( - returned, - Comp::Return(Value::Var(name)) if name == result - )); - } - - #[test] - fn thunk_captures_are_borrowed_inside_the_suspension() { - let int = source(Type::Int); - let capture = TypedBinder::new(sym("capture"), int.clone()); - let thunk = TypedValue::new( - CoreType::Thunk(Box::new(pure(int.clone()))), - TypedValueKind::Thunk(Box::new(ret(var("capture", int)))), - ); - let input = TypedCore::new(vec![function("main", vec![capture], ret(thunk))]); - let actual = run_and_verify(&input, &Sigs::new(), &VerifyEnv::new()).erase(); - - // The capture is threaded through to the suspension's result. Perceus may - // insert a balancing `Dup` before the return; its placement tracks this - // hand-built fixture's process-global `Sym` supply (adding a builtin or - // prelude effect moves it), not real elaboration, so peel any leading `Dup` - // binds and assert the tail returns `capture` untouched: no rename, no drop - // of the captured value. `run_and_verify` above already proved the RC is - // balanced, and real programs are covered by the parity and snapshot - // corpora, which are byte-identical across this change. - let Comp::Return(Value::Thunk(closure)) = &actual.fns[0].body else { - panic!("expected a returned thunk"); - }; - let mut tail = &**closure; - while let Comp::Bind(bound, _, rest) = tail { - assert!( - matches!(&**bound, Comp::Dup(_)), - "only a balancing Dup may precede the return, got {bound:?}" - ); - tail = rest; - } - assert!(matches!( - tail, - Comp::Return(Value::Var(name)) if *name == sym("capture") - )); - } - - #[test] - fn rc_sequence_binders_do_not_shadow_a_lowered_word_discard() { - let int = source(Type::Int); - let capture = TypedBinder::new(sym("capture"), int.clone()); - let word = CoreType::Lowered(LoweredType::Word); - let discarded = TypedBinder::new(sym("_"), word.clone()); - let lambda_sig = CoreFnSig::new(Vec::new(), vec![word], pure(int.clone())); - let lambda = TypedComp::new( - pure(CoreType::Function(Box::new(lambda_sig))), - TypedCompKind::Lam(vec![discarded], Box::new(ret(var("capture", int)))), - ); - let thunk = TypedValue::new( - CoreType::Thunk(Box::new(lambda.sig.clone())), - TypedValueKind::Thunk(Box::new(lambda)), - ); - let input = TypedCore::new(vec![function("main", vec![capture], ret(thunk))]); - let actual = run_and_verify(&input, &Sigs::new(), &VerifyEnv::new()); - - let TypedCompKind::Return(thunk) = &actual.fns[0].body.kind else { - panic!("expected returned thunk"); - }; - let TypedValueKind::Thunk(lambda) = &thunk.kind else { - panic!("expected retained thunk body"); - }; - let TypedCompKind::Lam(_, body) = &lambda.kind else { - panic!("expected retained lambda body"); - }; - let TypedCompKind::Bind(_, first_sequence, rest) = &body.kind else { - panic!("expected the capture dup to be sequenced"); - }; - assert_eq!(first_sequence.name().as_str(), names::RC_SEQUENCE_BINDER); - assert_eq!(first_sequence.erase_name().as_str(), "_"); - let TypedCompKind::Bind(_, second_sequence, _) = &rest.kind else { - panic!("expected the discarded parameter drop to be sequenced"); - }; - assert_eq!(second_sequence.name().as_str(), names::RC_SEQUENCE_BINDER); - assert_eq!(second_sequence.erase_name().as_str(), "_"); - } - - #[test] - fn unboxed_products_rewrite_the_thunks_they_contain() { - let int = source(Type::Int); - let source_function = Type::Fun(Vec::new(), EffRow::Empty, Box::new(Type::Int)); - let captured_thunk = |capture: &str| { - let closure_sig = CoreFnSig::new(Vec::new(), Vec::new(), pure(int.clone())); - let closure = TypedComp::new( - pure(CoreType::Function(Box::new(closure_sig))), - TypedCompKind::Lam(Vec::new(), Box::new(ret(var(capture, int.clone())))), - ); - TypedValue::new( - CoreType::Thunk(Box::new(closure.sig.clone())), - TypedValueKind::Thunk(Box::new(closure)), - ) - }; - - let tuple_capture = TypedBinder::new(sym("tuple_capture"), int.clone()); - let tuple = TypedValue::new( - source(Type::UnboxedTuple(vec![source_function.clone()])), - TypedValueKind::UnboxedTuple(vec![captured_thunk("tuple_capture")]), - ); - let tuple_function = function("tuple", vec![tuple_capture], ret(tuple)); - - let field_name = sym("run"); - let record_capture = TypedBinder::new(sym("record_capture"), int.clone()); - let record = TypedValue::new( - source(Type::UnboxedRecord(vec![(field_name, source_function)])), - TypedValueKind::UnboxedRecord(vec![(field_name, captured_thunk("record_capture"))]), - ); - let record_function = function("record", vec![record_capture], ret(record)); - let input = TypedCore::new(vec![tuple_function, record_function]); - let actual = run_and_verify(&input, &Sigs::new(), &VerifyEnv::new()).erase(); - - let Comp::Return(Value::UnboxedTuple(tuple_fields)) = &actual.fns[0].body else { - panic!("expected unboxed tuple return"); - }; - let Value::Thunk(tuple_closure) = &tuple_fields[0] else { - panic!("expected tuple thunk"); - }; - let Comp::Lam(_, tuple_body) = &**tuple_closure else { - panic!("expected tuple closure"); - }; - let tuple_rest = head_dup(tuple_body, "tuple_capture"); - assert!(matches!( - tuple_rest, - Comp::Return(Value::Var(name)) if *name == sym("tuple_capture") - )); - - let Comp::Return(Value::UnboxedRecord(record_fields)) = &actual.fns[1].body else { - panic!("expected unboxed record return"); - }; - let Value::Thunk(record_closure) = &record_fields[0].1 else { - panic!("expected record thunk"); - }; - let Comp::Lam(_, record_body) = &**record_closure else { - panic!("expected record closure"); - }; - let record_rest = head_dup(record_body, "record_capture"); - assert!(matches!( - record_rest, - Comp::Return(Value::Var(name)) if *name == sym("record_capture") - )); - } - - #[test] - fn branches_and_refs_balance_on_every_path() { - let int = source(Type::Int); - let condition = TypedBinder::new(sym("condition"), source(Type::Bool)); - let cell_ty = CoreType::Ref(Box::new(int.clone())); - let cell = TypedBinder::new(sym("cell"), cell_ty.clone()); - let get = || { - TypedComp::new( - pure(int.clone()), - TypedCompKind::RefGet(var("cell", cell_ty.clone())), - ) - }; - let body = TypedComp::new( - pure(int.clone()), - TypedCompKind::If( - var("condition", source(Type::Bool)), - Box::new(get()), - Box::new(get()), - ), - ); - let input = TypedCore::new(vec![function("main", vec![condition, cell], body)]); - let actual = run_and_verify(&input, &Sigs::new(), &VerifyEnv::new()).erase(); - - // Each arm must independently balance: the unused boolean is dropped on - // both paths, and the cell is consumed by its read. - let Comp::If(_, yes, no) = &actual.fns[0].body else { - panic!("expected the branch structure to survive RC insertion"); - }; - for branch in [&**yes, &**no] { - let after_drop = head_drop(branch, "condition"); - assert!(matches!( - after_drop, - Comp::RefGet(Value::Var(name)) if *name == sym("cell") - )); - } - } - - #[test] - fn pattern_arms_duplicate_live_fields_before_dropping_the_scrutinee() { - let int = source(Type::Int); - let tuple_ty = source(Type::Tuple(vec![Type::Int])); - let scrutinee = TypedBinder::new(sym("scrutinee"), tuple_ty.clone()); - let field = TypedBinder::new(sym("field"), int.clone()); - let body = TypedComp::new( - pure(int.clone()), - TypedCompKind::Case( - var("scrutinee", tuple_ty), - vec![( - TypedPattern::Tuple(vec![Some(field)]), - ret(var("field", int)), - )], - ), - ); - let input = TypedCore::new(vec![function("main", vec![scrutinee], body)]); - let actual = run_and_verify(&input, &Sigs::new(), &VerifyEnv::new()).erase(); - let Comp::Case(_, arms) = &actual.fns[0].body else { - panic!("expected case after RC insertion"); - }; - let field_rest = head_dup(&arms[0].1, "field"); - let scrutinee_rest = head_drop(field_rest, "scrutinee"); - assert!(matches!( - scrutinee_rest, - Comp::Return(Value::Var(name)) if *name == sym("field") - )); - } - - #[test] - fn init_at_consumes_the_cell_and_every_constructor_field() { - let int = source(Type::Int); - let tuple = source(Type::Tuple(vec![Type::Int, Type::Int])); - let cell = TypedBinder::new(sym("cell"), int.clone()); - let field = TypedBinder::new(sym("field"), int.clone()); - let ctor = TypedValue::new( - tuple.clone(), - TypedValueKind::Tuple(vec![var("field", int.clone()), var("field", int.clone())]), - ); - let body = TypedComp::new( - pure(tuple), - TypedCompKind::InitAt(var("cell", int.clone()), ctor), - ); - let input = TypedCore::new(vec![function("main", vec![cell, field], body)]); - let mut env = VerifyEnv::new(); - env.insert_operation( - sym(ALLOC_OP), - OperationSig::new( - Vec::new(), - vec![int.clone()], - int, - Label::bare(sym("Arena")), - ), - ); - let actual = run_and_verify(&input, &Sigs::new(), &env).erase(); - let after_dup = head_dup(&actual.fns[0].body, "field"); - assert!(matches!( - after_dup, - Comp::InitAt(Value::Var(cell), Value::Tuple(fields)) - if *cell == sym("cell") - && matches!( - fields.as_slice(), - [Value::Var(lhs), Value::Var(rhs)] - if *lhs == sym("field") && *rhs == sym("field") - ) - )); - } - - #[test] - fn polymorphic_global_closures_share_one_verified_rc_operand_instance() { - let id = sym("id"); - let parameter_type = sym("a"); - let generic = source(Type::Var(parameter_type)); - let parameter = TypedBinder::new(sym("value"), generic.clone()); - let id_body = ret(var("value", generic.clone())); - let id_sig = CoreFnSig::new( - vec![CoreQuantifier::Type(parameter_type)], - vec![generic.clone()], - pure(generic), - ); - let id_function = TypedCoreFn::new(id, vec![parameter], id_body, id_sig, 0); - - let capture = |name: &str, ty: Type| { - let instance = CoreFnSig::new( - Vec::new(), - vec![source(ty.clone())], - pure(source(ty.clone())), - ); - let global = TypedValue::new( - CoreType::Function(Box::new(instance)), - TypedValueKind::Var { - name: id, - instantiation: vec![CoreInstantiation::Type(ty)], - }, - ); - let closure_sig = CoreFnSig::new(Vec::new(), Vec::new(), pure(global.ty.clone())); - let closure = TypedComp::new( - pure(CoreType::Function(Box::new(closure_sig))), - TypedCompKind::Lam(Vec::new(), Box::new(ret(global))), - ); - function(name, Vec::new(), closure) - }; - let input = TypedCore::new(vec![ - id_function, - capture("int_capture", Type::Int), - capture("bool_capture", Type::Bool), - ]); - let actual = run_and_verify(&input, &Sigs::new(), &VerifyEnv::new()); - let int_instance = CoreType::Function(Box::new(CoreFnSig::new( - Vec::new(), - vec![source(Type::Int)], - pure(source(Type::Int)), - ))); - - for function in &actual.fns[1..] { - let TypedCompKind::Lam(_, body) = &function.body.kind else { - panic!("expected captured global closure"); - }; - let TypedCompKind::Bind(dup, _, _) = &body.kind else { - panic!("expected a capture dup"); - }; - let TypedCompKind::Dup(operand) = &dup.kind else { - panic!("expected a typed dup operand"); - }; - assert_eq!(operand.ty, int_instance); - } - } - - #[test] - fn a_shadowing_local_cannot_poison_a_later_global_capture_witness() { - let global_name = sym("f"); - let int = source(Type::Int); - let unit = source(Type::Unit); - let global_sig = CoreFnSig::new(Vec::new(), vec![unit.clone()], pure(unit.clone())); - - let poison_param = TypedBinder::new(global_name, int.clone()); - let poison = function( - "poison", - vec![poison_param], - ret(TypedValue::new( - int, - TypedValueKind::Var { - name: global_name, - instantiation: Vec::new(), - }, - )), - ); - let global_param = TypedBinder::new(sym("arg"), unit); - let global = TypedCoreFn::new( - global_name, - vec![global_param.clone()], - ret(binder_value(&global_param)), - global_sig.clone(), - 0, - ); - let global_value = TypedValue::new( - CoreType::Function(Box::new(global_sig.clone())), - TypedValueKind::Var { - name: global_name, - instantiation: Vec::new(), - }, - ); - let capture_sig = CoreFnSig::new(Vec::new(), Vec::new(), pure(global_value.ty.clone())); - let capture = function( - "capture", - Vec::new(), - TypedComp::new( - pure(CoreType::Function(Box::new(capture_sig))), - TypedCompKind::Lam(Vec::new(), Box::new(ret(global_value))), - ), - ); - let input = TypedCore::new(vec![poison, global, capture]); - let actual = run_and_verify(&input, &Sigs::new(), &VerifyEnv::new()); - let TypedCompKind::Lam(_, body) = &actual.fns[2].body.kind else { - panic!("expected global-capturing closure"); - }; - let TypedCompKind::Bind(dup, _, _) = &body.kind else { - panic!("expected capture dup"); - }; - let TypedCompKind::Dup(operand) = &dup.kind else { - panic!("expected typed dup operand"); - }; - assert_eq!( - operand.ty, - CoreType::Function(Box::new(global_sig)), - "the earlier local f:Int must not replace the global f witness" - ); - } - - #[test] - fn insertion_order_is_name_stable() { - let int = source(Type::Int); - let zulu = TypedBinder::new(sym("zulu"), int.clone()); - let alpha = TypedBinder::new(sym("alpha"), int); - let unit = TypedValue::new(source(Type::Unit), TypedValueKind::Unit); - let input = TypedCore::new(vec![function("main", vec![zulu, alpha], ret(unit))]); - let actual = run_and_verify(&input, &Sigs::new(), &VerifyEnv::new()).erase(); - let rendered = crate::core::pp_core(&actual); - let alpha_at = rendered.find("drop alpha").expect("alpha drop"); - let zulu_at = rendered.find("drop zulu").expect("zulu drop"); - assert!( - zulu_at < alpha_at, - "name-sorted insertion wraps the later name outermost" - ); - } - - #[test] - fn bind_spine_free_variable_work_scales_linearly() { - fn fixture(bindings: usize) -> TypedCore { - let unit = source(Type::Unit); - let returned_unit = || ret(TypedValue::new(unit.clone(), TypedValueKind::Unit)); - let mut body = returned_unit(); - for index in (0..bindings).rev() { - body = TypedComp::new( - pure(unit.clone()), - TypedCompKind::Bind( - Box::new(returned_unit()), - TypedBinder::new(sym(&format!("spine_{index}")), unit.clone()), - Box::new(body), - ), - ); - } - TypedCore::new(vec![function("main", Vec::new(), body)]) - } - - fn visits(bindings: usize) -> usize { - let input = fixture(bindings); - verify(&input, &VerifyEnv::new()).expect("bind-spine fixture must be valid"); - let (owned, visits) = count_free_comp_var_visits(|| insert_rc(input, &Sigs::new())); - verify(&owned, &VerifyEnv::new()).expect("RC output must remain valid"); - visits - } - - const SMALL: usize = 128; - const LARGE: usize = 256; - let small = visits(SMALL); - let large = visits(LARGE); - - assert!( - large <= small * 2 + 2, - "doubling a bind spine must approximately double free-variable work: \ - {SMALL} bindings visited {small} nodes, {LARGE} visited {large}" - ); - assert!( - large <= LARGE * 4, - "free-variable work must stay linear in bind-spine length: \ - {LARGE} bindings visited {large} nodes" - ); - } -} diff --git a/crates/prism-core/src/core/typed/rc/census.rs b/crates/prism-core/src/core/typed/rc/census.rs new file mode 100644 index 00000000..59c72c4c --- /dev/null +++ b/crates/prism-core/src/core/typed/rc/census.rs @@ -0,0 +1,140 @@ +//! Counting the references one leaf holds, and naming each one. +//! +//! The census is the single walk the ownership rules read. Its per-name length +//! is the count they compare against the reference the site owns, and its +//! entries are the terms they emit operations against, so the decision to insert +//! an operation and the justification for it cannot drift apart. + +use std::collections::BTreeMap; + +use crate::core::fbip::Sigs; +use prism_common::sym::Sym; + +use super::super::specialize_support::free_comp_var_witnesses; +use super::super::{TypedComp, TypedCompKind, TypedValue, TypedValueKind}; +use super::{borrowed_at, Set}; + +/// Every occurrence of a name in one leaf, in traversal order. +/// +/// The length is the count the ownership rules read, and the entries are the +/// witnesses those rules emit against, so the decision to insert an operation +/// and the term that justifies it come out of one walk and cannot disagree. +pub(super) type Census = BTreeMap>; + +/// The occurrences of `name` this leaf holds, empty when it holds none. +pub(super) fn occurrences(census: &Census, name: Sym) -> &[TypedValue] { + census.get(&name).map_or(&[], Vec::as_slice) +} + +pub(super) fn borrowed_call_vars(comp: &TypedComp, sigs: &Sigs) -> Set { + let TypedCompKind::Call { callee, args, .. } = &comp.kind else { + return Set::new(); + }; + let mask = sigs.get(callee).map(Vec::as_slice); + args.iter() + .enumerate() + .filter(|(index, _)| borrowed_at(mask, *index)) + // The retained set names the caller's own variables a borrowed + // position leaves owned. By the time this census runs, anchoring has + // rebound every cell-owning non-variable at a borrowed position to a + // fresh binder, so the only non-variables left are scalars the backend + // represents without a heap cell; those own nothing to retain and are + // correctly absent from the set. + .filter_map(|(_, arg)| arg.referenced_binding()) + .collect() +} + +fn census_value(value: &TypedValue, census: &mut Census) { + match &value.kind { + TypedValueKind::Var { name, .. } => census.entry(*name).or_default().push(value.clone()), + TypedValueKind::Ctor { fields, .. } + | TypedValueKind::Tuple(fields) + | TypedValueKind::UnboxedTuple(fields) => { + for field in fields { + census_value(field, census); + } + } + TypedValueKind::UnboxedRecord(fields) => { + for (_, field) in fields { + census_value(field, census); + } + } + // A thunk cell captures one reference per distinct free name however many + // times the suspended body reads it, so the census takes one witness per + // name here rather than one per occurrence. + TypedValueKind::Thunk(body) => { + for (name, witness) in free_comp_var_witnesses(body) { + census.entry(name).or_default().push(witness); + } + } + TypedValueKind::Reinterpret(inner) + | TypedValueKind::LoweredRepr { + value: inner, + proof: _, + } + | TypedValueKind::NewtypeRepr { value: inner, .. } => census_value(inner, census), + TypedValueKind::Int(_) + | TypedValueKind::I64(_) + | TypedValueKind::U64(_) + | TypedValueKind::Float(_) + | TypedValueKind::Bool(_) + | TypedValueKind::Unit + | TypedValueKind::Str(_) => {} + } +} + +pub(super) fn leaf_census(comp: &TypedComp, census: &mut Census, sigs: &Sigs) { + match &comp.kind { + TypedCompKind::Return(value) + | TypedCompKind::Force(value) + | TypedCompKind::Error(value) + | TypedCompKind::FloatBuiltin(_, value) + | TypedCompKind::Neg(_, value) + | TypedCompKind::RefNew(value) + | TypedCompKind::RefGet(value) => census_value(value, census), + TypedCompKind::RefSet(cell, value) | TypedCompKind::InitAt(cell, value) => { + census_value(cell, census); + census_value(value, census); + } + TypedCompKind::App { callee, args, .. } => { + // Same rule as a thunk: the closure holds one reference per captured + // name, not one per read. + for (name, witness) in free_comp_var_witnesses(callee) { + census.entry(name).or_default().push(witness); + } + for arg in args { + census_value(arg, census); + } + } + TypedCompKind::Prim(_, lhs, rhs) => { + census_value(lhs, census); + census_value(rhs, census); + } + TypedCompKind::Call { callee, args, .. } => { + let mask = sigs.get(callee).map(Vec::as_slice); + for (index, arg) in args.iter().enumerate() { + if !borrowed_at(mask, index) { + census_value(arg, census); + } + } + } + TypedCompKind::Do { args, .. } + | TypedCompKind::StrBuiltin { args, .. } + | TypedCompKind::Io(_, args) => { + for arg in args { + census_value(arg, census); + } + } + TypedCompKind::Bind(_, _, _) + | TypedCompKind::Lam(_, _) + | TypedCompKind::If(_, _, _) + | TypedCompKind::Case(_, _) + | TypedCompKind::Handle { .. } + | TypedCompKind::Mask(_, _) + | TypedCompKind::UnboxedProject(_, _) + | TypedCompKind::Dup(_) + | TypedCompKind::Drop(_) + | TypedCompKind::WithReuse { .. } + | TypedCompKind::Reuse(_, _) => {} + } +} diff --git a/crates/prism-core/src/core/typed/rc/mod.rs b/crates/prism-core/src/core/typed/rc/mod.rs new file mode 100644 index 00000000..e0cb7604 --- /dev/null +++ b/crates/prism-core/src/core/typed/rc/mod.rs @@ -0,0 +1,432 @@ +//! Reference-count insertion for witness-carrying Core. +//! +//! This is the typed counterpart of [`super::super::fbip::insert_rc`]. It keeps +//! the same ownership partition, free-variable decisions, borrow masks, and +//! name-stable insertion order while retaining the witness for every inserted +//! `dup` and `drop` operand. + +mod census; +mod ops; +mod scope; +mod spine; +#[cfg(test)] +mod tests; +mod thunks; + +use std::collections::{BTreeMap, BTreeSet}; + +use crate::core::fbip::Sigs; +use crate::types::scalar_plan; +use crate::types::ty::EffRow; +use prism_common::fresh::Fresh; +use prism_common::sym::Sym; +use prism_syntax::names; + +use super::specialize_support::{binder_occurrence, free_comp_vars, substitute_terms}; +use super::{ + CompSig, EffectLowered, Owned, TypedBinder, TypedComp, TypedCompKind, TypedCore, TypedCoreFn, + TypedPattern, TypedValue, TypedValueKind, UncheckedTypedCore, +}; +use census::{borrowed_call_vars, leaf_census, occurrences, Census}; +use ops::{defer_call_drops, drop_, dup, dup_each}; +use scope::{bind_scope, operand, unbind_scope, Scope}; +use spine::rc_bind_spine; +use thunks::rc_thunks; + +type Set = BTreeSet; + +/// Insert precise reference-count operations without erasing type witnesses. +#[must_use] +pub fn insert_rc(core: TypedCore, sigs: &Sigs) -> UncheckedTypedCore { + let mut scope = Scope::new(); + let mut fresh = Fresh::new(); + let fns = core + .into_unchecked() + .into_functions() + .into_iter() + .map(|function| { + let mask = sigs.get(&function.name).map(Vec::as_slice); + let owned: Set = function + .params + .iter() + .enumerate() + .filter(|(index, _)| !borrowed_at(mask, *index)) + .map(|(_, binder)| binder.name) + .collect(); + let borrowed: Set = function + .params + .iter() + .enumerate() + .filter(|(index, _)| borrowed_at(mask, *index)) + .map(|(_, binder)| binder.name) + .collect(); + let undo = bind_scope(&mut scope, &function.params); + let body = rc( + &function.body, + &owned, + &borrowed, + sigs, + &mut scope, + &mut fresh, + ); + unbind_scope(&mut scope, undo); + TypedCoreFn::new( + function.name, + function.params, + body, + function.sig, + function.dict_arity, + ) + }) + .collect(); + UncheckedTypedCore::new(fns) +} + +fn borrowed_at(mask: Option<&[bool]>, index: usize) -> bool { + mask.is_some_and(|entries| entries.get(index).copied().unwrap_or(false)) +} + +// `Sym` orders by intern id, which is intentionally unrelated to the stable +// emitted order. RC operations are therefore sorted by their textual names. +fn by_name(syms: impl IntoIterator) -> Vec { + let mut names: Vec = syms.into_iter().collect(); + names.sort_by(|lhs, rhs| lhs.as_str().cmp(rhs.as_str())); + names +} + +fn rc( + comp: &TypedComp, + owned: &Set, + borrowed: &Set, + sigs: &Sigs, + scope: &mut Scope, + fresh: &mut Fresh, +) -> TypedComp { + match &comp.kind { + TypedCompKind::Bind(..) => rc_bind_spine(comp, owned, borrowed, sigs, scope, fresh), + TypedCompKind::If(condition, yes, no) => TypedComp::new( + comp.sig.clone(), + TypedCompKind::If( + condition.clone(), + Box::new(rc(yes, owned, borrowed, sigs, scope, fresh)), + Box::new(rc(no, owned, borrowed, sigs, scope, fresh)), + ), + ), + TypedCompKind::Case(scrutinee, arms) => { + // Matching on a loaned cell reads it without taking a reference: no + // arm drops the cell (it is not owned here), and the pattern binders + // become loans on its fields, kept live by whatever keeps the parent + // live. Consuming uses of a field still retain first via the + // borrowed leaf rule below. Wrappers are transparent through + // `referenced_binding`, matching what erasure leaves behind. + let loaned = scrutinee + .referenced_binding() + .is_some_and(|name| borrowed.contains(&name)); + let tracked: Set = owned.union(borrowed).copied().collect(); + TypedComp::new( + comp.sig.clone(), + TypedCompKind::Case( + scrutinee.clone(), + arms.iter() + .map(|(pattern, body)| { + let unshadowed = unshadow_arm(pattern, body, &tracked, fresh); + let (pattern, body) = unshadowed + .as_ref() + .map_or((pattern, body), |(pattern, body)| (pattern, body)); + ( + pattern.clone(), + rc_arm(pattern, body, owned, borrowed, sigs, scope, fresh, loaned), + ) + }) + .collect(), + ), + ) + } + TypedCompKind::Lam(params, body) => { + let params_set: Set = params.iter().map(|binder| binder.name).collect(); + let captures: Set = free_comp_vars(body) + .difference(¶ms_set) + .copied() + .collect(); + let undo = bind_scope(scope, params); + let body = rc(body, ¶ms_set, &captures, sigs, scope, fresh); + unbind_scope(scope, undo); + TypedComp::new( + comp.sig.clone(), + TypedCompKind::Lam(params.clone(), Box::new(body)), + ) + } + TypedCompKind::Mask(effects, body) => TypedComp::new( + comp.sig.clone(), + TypedCompKind::Mask( + effects.clone(), + Box::new(rc(body, owned, borrowed, sigs, scope, fresh)), + ), + ), + // Effect lowering eliminates every `Handle` before RC runs: the + // `EffectLowered` marker means handlers have already been rewritten into + // evidence threading, state passing, or the free-monad driver. A handler + // surviving to RC is a structural IR-invariant violation with no correct + // reference-count treatment (there is no runtime handler to count + // against), so it is a genuine compiler bug, not a case to handle. This + // is deliberately a hard invariant, unlike the tier cascade's silent + // declines: RC is post-commit and has no downgrade. + TypedCompKind::Handle { .. } => { + unreachable!("effect lowering removes every Handle before reference counting") + } + _ => { + // The optimizer may leave a cell-owning value directly in a + // borrowed argument; anchor each one to a fresh binder first so + // the ordinary ownership rules below see a variable whose last + // use is the loan and defer its release past the call. + if let Some(anchored) = rebind_borrowed_temporaries(comp, sigs, fresh) { + return rc(&anchored, owned, borrowed, sigs, scope, fresh); + } + let mut census = Census::new(); + leaf_census(comp, &mut census, sigs); + let borrowed_call = borrowed_call_vars(comp, sigs); + let deferred: Set = owned.intersection(&borrowed_call).copied().collect(); + let mut out = rc_thunks(comp, sigs, scope, fresh); + if !deferred.is_empty() { + out = defer_call_drops(out, &deferred, scope, fresh); + } + for name in by_name(owned.iter().copied()) { + let seen = occurrences(&census, name); + if deferred.contains(&name) { + // The call borrows the name, so nothing here consumes the + // reference the site owns: every occurrence needs its own. + out = dup_each(seen, out); + } else if let Some((consumed, duplicated)) = seen.split_first() { + // The first occurrence spends the owned reference; the rest + // each need one, and `consumed` is only a witness that the + // site had a use to spend it on. + let _ = consumed; + out = dup_each(duplicated, out); + } else { + out = drop_(name, out, scope); + } + } + for name in by_name(borrowed.iter().copied()) { + out = dup_each(occurrences(&census, name), out); + } + out + } + } +} + +// A borrowed position may hold a value as it stands only when the loan has +// something to borrow without taking ownership: a variable names a reference +// the caller retains, and a scalar literal whose encoding plan owns no fresh +// heap cell (a zero or tagged word, or the static cell a `Str` literal names) +// has nothing to own. Every other value materializes a fresh cell at codegen: +// wide numeric literals box per use, and a constructor, tuple, or thunk +// allocates. The borrow convention says the callee will not consume that cell, +// so without an owner it leaks; such a value must be anchored to a binder the +// caller can release. Mirrors `fbip::scalar_without_cell`, which makes the +// erased checker refuse whatever this pass failed to anchor. +fn anchored_borrow_arg(value: &TypedValue) -> bool { + value.referenced_binding().is_some() || scalar_without_cell(&value.kind) +} + +fn scalar_without_cell(kind: &TypedValueKind) -> bool { + kind.literal_scalar_type() + .and_then(|ty| scalar_plan(&ty).ok()) + .is_some_and(|plan| !plan.owns_fresh_cell()) +} + +// Rewrite a call so every borrowed position holds a value the loan can anchor +// to. Borrow masks are committed before the typed optimizer runs, so +// simplification may inline a cell-owning value (a boxed scalar literal, a +// freshly built structure) directly into a borrowed argument. Re-anchoring it +// as `bind %rc = return v in call .. %rc ..` hands the ordinary machinery an +// owned binder whose only use is the loan, which defers exactly one release to +// just after the call. +fn rebind_borrowed_temporaries( + comp: &TypedComp, + sigs: &Sigs, + fresh: &mut Fresh, +) -> Option { + let TypedCompKind::Call { + callee, + instantiation, + args, + } = &comp.kind + else { + return None; + }; + let mask = sigs.get(callee).map(Vec::as_slice); + let loose = |(index, argument): (usize, &TypedValue)| -> bool { + borrowed_at(mask, index) && !anchored_borrow_arg(argument) + }; + if !args.iter().enumerate().any(loose) { + return None; + } + let mut anchors: Vec<(TypedBinder, TypedValue)> = Vec::new(); + let args = args + .iter() + .enumerate() + .map(|(index, argument)| { + if !loose((index, argument)) { + return argument.clone(); + } + let binder = TypedBinder::new( + Sym::from(names::fresh_binder(names::FRESH_RC, fresh.bump())), + argument.ty.clone(), + ); + let anchored = TypedValue::new( + binder.ty().clone(), + TypedValueKind::Var { + name: binder.name(), + instantiation: Vec::new(), + }, + ); + anchors.push((binder, argument.clone())); + anchored + }) + .collect(); + let mut out = TypedComp::new( + comp.sig.clone(), + TypedCompKind::Call { + callee: *callee, + instantiation: instantiation.clone(), + args, + }, + ); + for (binder, value) in anchors.into_iter().rev() { + let returned = TypedComp::new( + CompSig::new(value.ty.clone(), EffRow::Empty), + TypedCompKind::Return(value), + ); + out = TypedComp::new( + comp.sig.clone(), + TypedCompKind::Bind(Box::new(returned), binder, Box::new(out)), + ); + } + Some(out) +} + +fn pattern_binders(pattern: &TypedPattern) -> Vec { + match pattern { + TypedPattern::Wild => Vec::new(), + TypedPattern::Var(binder) => vec![binder.clone()], + TypedPattern::Ctor { fields, .. } | TypedPattern::Tuple(fields) => { + fields.iter().flatten().cloned().collect() + } + } +} + +/// Rebind pattern binders that reuse a name the match site still tracks. +/// +/// A field binder spelled like a reference the site owns or borrows hides that +/// reference for the whole arm: every occurrence in the body denotes the field, +/// and the outer reference has none left there. Free variables are names, so +/// the liveness test in [`rc_arm`] would read those field occurrences as uses +/// of the outer reference, judge it live, and emit no release for it, leaking +/// its cell and everything the cell holds. Nor could the release be recovered +/// inside the arm, where the outer name no longer denotes the outer cell. +/// +/// Renaming the binder restores the arm to the shape it would have had without +/// the collision, and the ordinary dead-name rule then releases the outer +/// reference exactly once. Fresh names are unforgeable, so the rename cannot +/// collide in turn. +fn unshadow_arm( + pattern: &TypedPattern, + body: &TypedComp, + tracked: &Set, + fresh: &mut Fresh, +) -> Option<(TypedPattern, TypedComp)> { + let mut renames = BTreeMap::new(); + for binder in pattern_binders(pattern) { + if tracked.contains(&binder.name) { + let mut rebound = binder.clone(); + rebound.name = Sym::from(names::fresh_binder(names::FRESH_RC, fresh.bump())); + renames.insert(binder.name, rebound); + } + } + if renames.is_empty() { + return None; + } + let substitution: BTreeMap = renames + .iter() + .map(|(shadowed, rebound)| (*shadowed, binder_occurrence(rebound))) + .collect(); + let body = substitute_terms(body, &substitution, fresh.counter(), names::FRESH_RC); + Some((rename_binders(pattern, &renames), body)) +} + +fn rename_binders(pattern: &TypedPattern, renames: &BTreeMap) -> TypedPattern { + let rebind = |binder: &TypedBinder| { + renames + .get(&binder.name) + .cloned() + .unwrap_or_else(|| binder.clone()) + }; + let rebind_fields = |fields: &Vec>| { + fields + .iter() + .map(|field| field.as_ref().map(&rebind)) + .collect() + }; + match pattern { + TypedPattern::Wild => TypedPattern::Wild, + TypedPattern::Var(binder) => TypedPattern::Var(rebind(binder)), + TypedPattern::Ctor { + name, + instantiation, + fields, + } => TypedPattern::Ctor { + name: *name, + instantiation: instantiation.clone(), + fields: rebind_fields(fields), + }, + TypedPattern::Tuple(fields) => TypedPattern::Tuple(rebind_fields(fields)), + } +} + +#[allow(clippy::too_many_arguments)] +fn rc_arm( + pattern: &TypedPattern, + body: &TypedComp, + owned: &Set, + borrowed: &Set, + sigs: &Sigs, + scope: &mut Scope, + fresh: &mut Fresh, + loaned: bool, +) -> TypedComp { + let body_free = free_comp_vars(body); + let binders = pattern_binders(pattern); + let fields: Set = binders.iter().map(|binder| binder.name).collect(); + let live = by_name(fields.intersection(&body_free).copied()); + let dead = by_name( + owned + .iter() + .filter(|name| !body_free.contains(*name)) + .copied(), + ); + let mut body_owned: Set = owned.intersection(&body_free).copied().collect(); + let mut body_borrowed: Set = borrowed.intersection(&body_free).copied().collect(); + if loaned { + body_borrowed.extend(live.iter().copied()); + } else { + body_owned.extend(live.iter().copied()); + } + // The wraps resolve against the arm scope (fields visible), so the arm's + // binders stay installed until after they are emitted. + let undo = bind_scope(scope, &binders); + let mut out = rc(body, &body_owned, &body_borrowed, sigs, scope, fresh); + for name in &dead { + out = drop_(*name, out, scope); + } + // A live field of an owned scrutinee is retained as it is projected out, + // before the body that reads it exists, so the binder the pattern + // introduced is the witness rather than any occurrence of it. A loaned + // scrutinee's fields are loans themselves and retain nothing here. + if !loaned { + for name in live.iter().rev() { + out = dup(operand(scope, *name), out); + } + } + unbind_scope(scope, undo); + out +} diff --git a/crates/prism-core/src/core/typed/rc/ops.rs b/crates/prism-core/src/core/typed/rc/ops.rs new file mode 100644 index 00000000..690f378f --- /dev/null +++ b/crates/prism-core/src/core/typed/rc/ops.rs @@ -0,0 +1,89 @@ +//! Emitting one reference-count operation. +//! +//! Each of these takes the witness the ownership rules chose and wraps it around +//! a continuation. Nothing here decides whether an operation is needed; that is +//! the caller's job, and keeping the decision and the emission apart is what +//! lets the witness travel from the walk that found it to the term that carries +//! it without a lookup in between. + +use crate::types::ty::EffRow; +use crate::types::Type; +use prism_common::fresh::Fresh; +use prism_common::sym::Sym; +use prism_syntax::names; + +use super::super::{ + CompSig, CoreType, TypedBinder, TypedComp, TypedCompKind, TypedValue, TypedValueKind, +}; +use super::scope::{operand, Scope}; +use super::{by_name, Set}; + +pub(super) const fn pure_unit() -> CompSig { + CompSig::new(CoreType::Source(Type::Unit), EffRow::Empty) +} + +pub(super) fn seq(op: TypedComp, continuation: TypedComp) -> TypedComp { + TypedComp::new( + continuation.sig.clone(), + TypedCompKind::Bind( + Box::new(op), + TypedBinder::rc_sequence(), + Box::new(continuation), + ), + ) +} + +// The witness is the occurrence that justified the retain, not a lookup by +// name: a polymorphic global occurs at several instantiations and they are not +// interchangeable to a consumer that has to say which one a later release +// discharges. +pub(super) fn dup(witness: TypedValue, continuation: TypedComp) -> TypedComp { + seq( + TypedComp::new(pure_unit(), TypedCompKind::Dup(witness)), + continuation, + ) +} + +/// One retain per occurrence, each against the occurrence that needs it. +pub(super) fn dup_each(witnesses: &[TypedValue], continuation: TypedComp) -> TypedComp { + witnesses + .iter() + .fold(continuation, |out, witness| dup(witness.clone(), out)) +} + +pub(super) fn drop_(name: Sym, continuation: TypedComp, scope: &Scope) -> TypedComp { + seq( + TypedComp::new(pure_unit(), TypedCompKind::Drop(operand(scope, name))), + continuation, + ) +} + +pub(super) fn defer_call_drops( + call: TypedComp, + deferred: &Set, + scope: &Scope, + fresh: &mut Fresh, +) -> TypedComp { + let result = TypedBinder::new( + Sym::from(names::fresh_binder(names::FRESH_RC, fresh.bump())), + call.sig.result.clone(), + ); + let returned = TypedValue::new( + result.ty.clone(), + TypedValueKind::Var { + name: result.name, + instantiation: Vec::new(), + }, + ); + let mut post = TypedComp::new( + CompSig::new(result.ty.clone(), EffRow::Empty), + TypedCompKind::Return(returned), + ); + for name in by_name(deferred.iter().copied()) { + post = drop_(name, post, scope); + } + TypedComp::new( + call.sig.clone(), + TypedCompKind::Bind(Box::new(call), result, Box::new(post)), + ) +} diff --git a/crates/prism-core/src/core/typed/rc/scope.rs b/crates/prism-core/src/core/typed/rc/scope.rs new file mode 100644 index 00000000..ccebed3e --- /dev/null +++ b/crates/prism-core/src/core/typed/rc/scope.rs @@ -0,0 +1,68 @@ +//! The scope a release resolves against. +//! +//! Every other reference-count operation names an occurrence in the subtree that +//! justified it. A release is the exception: it discharges a name the site owns +//! and does not use, so no occurrence of it exists there to point at, and the +//! binder that introduced it has to answer instead. This is the map that keeps +//! those binders reachable. + +use std::collections::BTreeMap; + +use prism_common::sym::Sym; + +use super::super::specialize_support::binder_occurrence; +use super::super::{TypedBinder, TypedValue}; + +pub(super) type Scope = BTreeMap; + +// The scope is one shared map mutated in place: cloning it per binder made +// deep bind chains quadratic in the number of globals plus locals. Each entry +// records the value it displaced so a reverse replay restores the enclosing +// scope exactly, including a shadowed global or outer local of the same name. +pub(super) type ScopeUndo = Vec<(Sym, Option)>; + +pub(super) fn bind_scope(scope: &mut Scope, binders: &[TypedBinder]) -> ScopeUndo { + binders + .iter() + .map(|binder| { + ( + binder.name, + scope.insert(binder.name, binder_occurrence(binder)), + ) + }) + .collect() +} + +pub(super) fn unbind_scope(scope: &mut Scope, undo: ScopeUndo) { + for (name, displaced) in undo.into_iter().rev() { + match displaced { + Some(value) => { + scope.insert(name, value); + } + None => { + scope.remove(&name); + } + } + } +} + +// The scope answers for binders only. A `drop` discharges a name the site owns +// and does not use, so no occurrence of it exists there to point at, and the +// binder that introduced it is the witness. Every other operation takes its +// witness from an occurrence in the subtree that justified it, so nothing +// reaches here that a lexical binder cannot answer. +// +// Unlike the effect-lowering cascade, RC has no downgrade: it runs once on the +// committed lowered tree and emits the dup/drop operations codegen relies on +// for memory safety. A missing scope entry means the RC pass cannot know the +// operand's runtime representation, and a guessed representation would emit a +// mistyped dup/drop (a leak or a use-after-free), strictly worse than a loud +// failure. So this invariant deliberately stays a hard check rather than a +// silent decline; it is unreachable on verified input, which the typed +// verifier guarantees before RC ever runs. +pub(super) fn operand(scope: &Scope, name: Sym) -> TypedValue { + scope + .get(&name) + .unwrap_or_else(|| panic!("verified RC operand {name} is out of scope")) + .clone() +} diff --git a/crates/prism-core/src/core/typed/rc/spine.rs b/crates/prism-core/src/core/typed/rc/spine.rs new file mode 100644 index 00000000..e314d9d5 --- /dev/null +++ b/crates/prism-core/src/core/typed/rc/spine.rs @@ -0,0 +1,221 @@ +//! Rewriting a right-leaning `Bind` chain in one pass over its levels. + +use std::collections::BTreeMap; + +use crate::core::fbip::Sigs; +use prism_common::fresh::Fresh; +use prism_common::sym::Sym; + +use super::super::specialize_support::{ + binder_occurrence, free_comp_var_witnesses, free_comp_vars, +}; +use super::super::{CompSig, TypedBinder, TypedComp, TypedCompKind, TypedValue}; +use super::ops::{pure_unit, seq}; +use super::scope::{operand, unbind_scope, Scope, ScopeUndo}; +use super::{by_name, rc, Set}; + +/// One right-spine `Bind` level and the free-variable facts its ownership +/// partition needs. +struct SpineStep<'a> { + sig: &'a CompSig, + first: &'a TypedComp, + binder: &'a TypedBinder, + /// The free names of `first`, each with the occurrence that produced it. + /// Membership answers the ownership tests, and the value answers which + /// occurrence a retain emitted at this level is retaining. + first_refs: BTreeMap, + /// How many suffix components reference the binder's name while it is in + /// scope; the forward pass restores this count once the level is done. + prev_count: u32, +} + +/// A rewritten spine level, ready to be reassembled from the tail outward. +struct SpineLevel<'a> { + sig: &'a CompSig, + binder: &'a TypedBinder, + first: TypedComp, + shared_ops: Vec, + dead_ops: Vec, +} + +/// Rewrite a right-leaning `Bind` chain in one pass over its levels. +/// +/// A per-level recursion would recompute `free_comp_vars` on both subtrees at +/// every step, which is quadratic in the chain length. This walk derives the +/// same facts bottom-up: a backward pass over the spine accumulates a count of +/// how many suffix components reference each name, and the forward pass peels +/// one component's contribution back off per level, leaving exactly the +/// membership the recursive formulation computed from scratch. Counting +/// components (not occurrences) suffices because every decision below is a +/// set-membership test. The ownership partition, operand resolution point, +/// and dup/drop wrap order are unchanged, so the emitted tree is identical. +// The renamed reference a level's first component reads, when that is all it +// does. Wrappers are transparent through `referenced_binding`, matching what +// erasure leaves behind. +fn alias_source(comp: &TypedComp) -> Option { + match &comp.kind { + TypedCompKind::Return(value) => value.referenced_binding(), + _ => None, + } +} + +pub(super) fn rc_bind_spine( + comp: &TypedComp, + owned: &Set, + borrowed: &Set, + sigs: &Sigs, + scope: &mut Scope, + fresh: &mut Fresh, +) -> TypedComp { + let mut steps = Vec::new(); + let mut cursor = comp; + while let TypedCompKind::Bind(first, binder, rest) = &cursor.kind { + steps.push(SpineStep { + sig: &cursor.sig, + first, + binder, + first_refs: free_comp_var_witnesses(first), + prev_count: 0, + }); + cursor = rest; + } + let tail = cursor; + + // Backward pass: `live` maps each name to the number of remaining spine + // components (suffix firsts plus the tail) in which it occurs free. A + // binder's occurrences are bound over its rest, so its count is saved and + // withdrawn before the defining component's own free set is added back + // (where the same name may legitimately reference an outer binding). + let mut live: BTreeMap = free_comp_vars(tail) + .into_iter() + .map(|name| (name, 1)) + .collect(); + for step in steps.iter_mut().rev() { + step.prev_count = live.remove(&step.binder.name).unwrap_or(0); + for name in step.first_refs.keys() { + *live.entry(*name).or_insert(0) += 1; + } + } + + // Forward pass: at each level, removing the defining component's + // contribution leaves `live` keyed by exactly the free variables of the + // chain rest with the binder excluded, the `rest_free` of the recursive + // formulation. Ownership then splits as before: names live on both sides + // are dupped, names live on neither are dropped, and the binder joins the + // owned set for the rest of the chain. + let mut owned = owned.clone(); + let mut borrowed = borrowed.clone(); + let mut undo: ScopeUndo = Vec::with_capacity(steps.len()); + let mut levels: Vec> = Vec::with_capacity(steps.len()); + for step in &steps { + for name in step.first_refs.keys() { + if let Some(count) = live.get_mut(name) { + if *count > 1 { + *count -= 1; + } else { + live.remove(name); + } + } + } + let first_owned: Set = owned + .iter() + .filter(|name| step.first_refs.contains_key(*name)) + .copied() + .collect(); + let mut rest_owned: Set = owned + .iter() + .filter(|name| live.contains_key(*name)) + .copied() + .collect(); + let shared = by_name( + first_owned + .iter() + .filter(|name| rest_owned.contains(*name)) + .copied(), + ); + let dead = by_name( + owned + .iter() + .filter(|name| !step.first_refs.contains_key(*name) && !live.contains_key(*name)) + .copied(), + ); + let first_borrowed: Set = borrowed + .iter() + .filter(|name| step.first_refs.contains_key(*name)) + .copied() + .collect(); + let mut rest_borrowed: Set = borrowed + .iter() + .filter(|name| live.contains_key(*name)) + .copied() + .collect(); + // A first that merely renames a loaned reference extends the loan: the + // binder reads the same cell the loan keeps live, so no retain is + // inserted for the occurrence and the binder joins the borrowed set + // for the rest of the chain instead of the owned set. Representation + // wrappers are transparent here exactly as they are under erasure, so + // the erased token checker keys on the identical syntactic shape. + let alias = step.binder.name.as_str() != "_" + && alias_source(step.first).is_some_and(|name| borrowed.contains(&name)); + // A shared name is free in `first` by construction, so its retain names + // the occurrence there rather than a lookup by name. A dead name has no + // occurrence at this level to point at, which is what makes it dead, so + // its release names the binder that introduced it. + let shared_ops: Vec = shared + .iter() + .map(|name| step.first_refs[name].clone()) + .collect(); + let dead_ops: Vec = dead.iter().map(|name| operand(scope, *name)).collect(); + let first = if alias { + step.first.clone() + } else { + rc( + step.first, + &first_owned, + &first_borrowed, + sigs, + scope, + fresh, + ) + }; + undo.push(( + step.binder.name, + scope.insert(step.binder.name, binder_occurrence(step.binder)), + )); + if alias { + rest_borrowed.insert(step.binder.name); + } else { + rest_owned.insert(step.binder.name); + } + owned = rest_owned; + borrowed = rest_borrowed; + if step.prev_count > 0 { + live.insert(step.binder.name, step.prev_count); + } + levels.push(SpineLevel { + sig: step.sig, + binder: step.binder, + first, + shared_ops, + dead_ops, + }); + } + let mut out = rc(tail, &owned, &borrowed, sigs, scope, fresh); + unbind_scope(scope, undo); + + // Reassemble from the tail outward; per level the dups wrap the bind and + // the drops wrap the dups, each in ascending name order. + for level in levels.into_iter().rev() { + out = TypedComp::new( + level.sig.clone(), + TypedCompKind::Bind(Box::new(level.first), level.binder.clone(), Box::new(out)), + ); + for value in level.shared_ops { + out = seq(TypedComp::new(pure_unit(), TypedCompKind::Dup(value)), out); + } + for value in level.dead_ops { + out = seq(TypedComp::new(pure_unit(), TypedCompKind::Drop(value)), out); + } + } + out +} diff --git a/crates/prism-core/src/core/typed/rc/tests.rs b/crates/prism-core/src/core/typed/rc/tests.rs new file mode 100644 index 00000000..0fdd06b8 --- /dev/null +++ b/crates/prism-core/src/core/typed/rc/tests.rs @@ -0,0 +1,734 @@ +//! Fixtures for reference-count insertion. + +use crate::core::{Comp, Value}; +use crate::types::ty::{EffRow, Label}; +use crate::types::Type; +use prism_syntax::names::{self, ALLOC_OP}; + +use super::super::specialize_support::{binder_occurrence, count_free_comp_var_visits}; +use super::super::verify::{OperationSig, VerifyEnv}; +use super::super::{ + verify, CompSig, CoreFnSig, CoreInstantiation, CoreQuantifier, CoreType, LoweredType, + TypedHandler, TypedValue, TypedValueKind, UncheckedTypedCore, +}; +use super::*; + +fn sym(name: &str) -> Sym { + Sym::new(name) +} + +fn source(ty: Type) -> CoreType { + CoreType::Source(ty) +} + +fn pure(result: CoreType) -> CompSig { + CompSig::new(result, EffRow::Empty) +} + +fn var(name: &str, ty: CoreType) -> TypedValue { + TypedValue::new( + ty, + TypedValueKind::Var { + name: sym(name), + instantiation: Vec::new(), + }, + ) +} + +fn ret(value: TypedValue) -> TypedComp { + TypedComp::new(pure(value.ty.clone()), TypedCompKind::Return(value)) +} + +fn function(name: &str, params: Vec, body: TypedComp) -> TypedCoreFn { + let signature = CoreFnSig::new( + Vec::new(), + params.iter().map(|binder| binder.ty.clone()).collect(), + body.sig.clone(), + ); + TypedCoreFn::new(sym(name), params, body, signature, 0) +} + +fn head_dup<'a>(comp: &'a Comp, name: &str) -> &'a Comp { + let Comp::Bind(op, binder, rest) = comp else { + panic!("expected a leading dup, found {comp:?}"); + }; + assert_eq!(binder.as_str(), "_"); + assert!(matches!( + &**op, + Comp::Dup(Value::Var(actual)) if *actual == sym(name) + )); + rest +} + +fn head_drop<'a>(comp: &'a Comp, name: &str) -> &'a Comp { + let Comp::Bind(op, binder, rest) = comp else { + panic!("expected a leading drop, found {comp:?}"); + }; + assert_eq!(binder.as_str(), "_"); + assert!(matches!( + &**op, + Comp::Drop(Value::Var(actual)) if *actual == sym(name) + )); + rest +} + +fn run_and_verify( + input: UncheckedTypedCore, + sigs: &Sigs, + env: &VerifyEnv, +) -> TypedCore { + let input = verify(input, env) + .unwrap_or_else(|violations| panic!("input fixture is invalid: {violations:#?}")); + verify(insert_rc(input, sigs), env) + .unwrap_or_else(|violations| panic!("owned typed Core is invalid: {violations:#?}")) +} + +// `EffectLowered` promises that no source handler remains. The verifier must +// refuse to mint that authority before RC can see the invalid tree. +#[test] +fn surviving_handle_cannot_mint_rc_input_authority() { + let unit = source(Type::Unit); + let body = ret(TypedValue::new(unit.clone(), TypedValueKind::Unit)); + let handled = TypedComp::new( + pure(unit), + TypedCompKind::Handle { + body: Box::new(body), + return_binder: None, + return_body: None, + ops: TypedHandler::new(Vec::new()).unwrap(), + }, + ); + let input = + UncheckedTypedCore::::new(vec![function("main", Vec::new(), handled)]); + verify(input, &VerifyEnv::new()).expect_err("a surviving handler must remain unchecked"); +} + +#[test] +fn borrow_masks_preserve_the_calling_convention() { + let int = source(Type::Int); + let parameter = TypedBinder::new(sym("borrowed"), int.clone()); + let body = ret(var("borrowed", int)); + let observe = function("observe", vec![parameter], body); + let retained = TypedBinder::new(sym("retained"), source(Type::Int)); + let call = TypedComp::new( + pure(source(Type::Int)), + TypedCompKind::Call { + callee: sym("observe"), + instantiation: Vec::new(), + args: vec![var("retained", source(Type::Int))], + }, + ); + let caller = function("caller", vec![retained], call); + let input = UncheckedTypedCore::new(vec![observe, caller]); + let sigs = std::iter::once((sym("observe"), vec![true])).collect(); + let actual = run_and_verify(input, &sigs, &VerifyEnv::new()).erase(); + let observe_rest = head_dup(&actual.fns[0].body, "borrowed"); + assert!(matches!( + observe_rest, + Comp::Return(Value::Var(name)) if *name == sym("borrowed") + )); + let Comp::Bind(call, result, post) = &actual.fns[1].body else { + panic!("borrowed tail call must retain its argument through the call"); + }; + assert!(matches!( + &**call, + Comp::Call(name, args) + if *name == sym("observe") + && matches!(args.as_slice(), [Value::Var(arg)] if *arg == sym("retained")) + )); + assert_eq!(result.as_str(), "%rc0"); + let returned = head_drop(post, "retained"); + assert!(matches!( + returned, + Comp::Return(Value::Var(name)) if name == result + )); +} + +#[test] +fn an_owned_and_borrowed_alias_keeps_a_loan_token_through_the_call() { + let int = source(Type::Int); + let owned = TypedBinder::new(sym("owned"), int.clone()); + let loan = TypedBinder::new(sym("loan"), int.clone()); + let callee = function( + "consume_and_borrow", + vec![owned, loan], + ret(var("owned", int.clone())), + ); + let shared = TypedBinder::new(sym("shared"), int.clone()); + let call = TypedComp::new( + pure(int.clone()), + TypedCompKind::Call { + callee: sym("consume_and_borrow"), + instantiation: Vec::new(), + args: vec![var("shared", int.clone()), var("shared", int)], + }, + ); + let invoking_function = function("caller", vec![shared], call); + let input = UncheckedTypedCore::new(vec![callee, invoking_function]); + let sigs = std::iter::once((sym("consume_and_borrow"), vec![false, true])).collect(); + let actual = run_and_verify(input, &sigs, &VerifyEnv::new()).erase(); + + let after_loan = head_dup(&actual.fns[1].body, "shared"); + let Comp::Bind(call, result, post) = after_loan else { + panic!("aliased call must defer loan cleanup"); + }; + assert!(matches!( + &**call, + Comp::Call(name, args) + if *name == sym("consume_and_borrow") + && matches!( + args.as_slice(), + [Value::Var(lhs), Value::Var(rhs)] + if *lhs == sym("shared") && *rhs == sym("shared") + ) + )); + assert_eq!(result.as_str(), "%rc0"); + let returned = head_drop(post, "shared"); + assert!(matches!( + returned, + Comp::Return(Value::Var(name)) if name == result + )); +} + +// The optimizer may inline a boxed scalar literal directly into a borrowed +// position after masks are committed. The pass must anchor it to a fresh +// binder so the caller owns the cell and releases it once the loan ends; +// leaving it inline would leak the box the backend allocates for it. +#[test] +fn a_borrowed_boxed_literal_is_anchored_and_released_after_the_call() { + let float = source(Type::Float); + let parameter = TypedBinder::new(sym("borrowed"), float.clone()); + let body = ret(var("borrowed", float.clone())); + let observe = function("observe", vec![parameter], body); + let literal = TypedValue::new(float.clone(), TypedValueKind::Float(2.5)); + let call = TypedComp::new( + pure(float), + TypedCompKind::Call { + callee: sym("observe"), + instantiation: Vec::new(), + args: vec![literal], + }, + ); + let caller = function("caller", Vec::new(), call); + let input = UncheckedTypedCore::new(vec![observe, caller]); + let sigs = std::iter::once((sym("observe"), vec![true])).collect(); + let actual = run_and_verify(input, &sigs, &VerifyEnv::new()).erase(); + + let Comp::Bind(anchor, owner, rest) = &actual.fns[1].body else { + panic!("a borrowed literal must be anchored to a binder"); + }; + assert!(matches!( + &**anchor, + Comp::Return(Value::Float(x)) if x.to_bits() == 2.5f64.to_bits() + )); + assert_eq!(owner.as_str(), "%rc0"); + let Comp::Bind(call, result, post) = &**rest else { + panic!("the anchored loan must defer its release past the call"); + }; + assert!(matches!( + &**call, + Comp::Call(name, args) + if *name == sym("observe") + && matches!(args.as_slice(), [Value::Var(arg)] if arg == owner) + )); + assert_eq!(result.as_str(), "%rc1"); + let returned = head_drop(post, "%rc0"); + assert!(matches!( + returned, + Comp::Return(Value::Var(name)) if name == result + )); +} + +#[test] +fn thunk_captures_are_borrowed_inside_the_suspension() { + let int = source(Type::Int); + let capture = TypedBinder::new(sym("capture"), int.clone()); + let thunk = TypedValue::new( + CoreType::Thunk(Box::new(pure(int.clone()))), + TypedValueKind::Thunk(Box::new(ret(var("capture", int)))), + ); + let input = UncheckedTypedCore::new(vec![function("main", vec![capture], ret(thunk))]); + let actual = run_and_verify(input, &Sigs::new(), &VerifyEnv::new()).erase(); + + // The capture is threaded through to the suspension's result. Perceus may + // insert a balancing `Dup` before the return; its placement tracks this + // hand-built fixture's process-global `Sym` supply (adding a builtin or + // prelude effect moves it), not real elaboration, so peel any leading `Dup` + // binds and assert the tail returns `capture` untouched: no rename, no drop + // of the captured value. `run_and_verify` above already proved the RC is + // balanced, and real programs are covered by the parity and snapshot + // corpora, which are byte-identical across this change. + let Comp::Return(Value::Thunk(closure)) = &actual.fns[0].body else { + panic!("expected a returned thunk"); + }; + let mut tail = &**closure; + while let Comp::Bind(bound, _, rest) = tail { + assert!( + matches!(&**bound, Comp::Dup(_)), + "only a balancing Dup may precede the return, got {bound:?}" + ); + tail = rest; + } + assert!(matches!( + tail, + Comp::Return(Value::Var(name)) if *name == sym("capture") + )); +} + +#[test] +fn rc_sequence_binders_do_not_shadow_a_lowered_word_discard() { + let int = source(Type::Int); + let capture = TypedBinder::new(sym("capture"), int.clone()); + let word = CoreType::Lowered(LoweredType::Word); + let discarded = TypedBinder::new(sym("_"), word.clone()); + let lambda_sig = CoreFnSig::new(Vec::new(), vec![word], pure(int.clone())); + let lambda = TypedComp::new( + pure(CoreType::Function(Box::new(lambda_sig))), + TypedCompKind::Lam(vec![discarded], Box::new(ret(var("capture", int)))), + ); + let thunk = TypedValue::new( + CoreType::Thunk(Box::new(lambda.sig.clone())), + TypedValueKind::Thunk(Box::new(lambda)), + ); + let input = UncheckedTypedCore::new(vec![function("main", vec![capture], ret(thunk))]); + let actual = run_and_verify(input, &Sigs::new(), &VerifyEnv::new()); + + let TypedCompKind::Return(thunk) = &actual.functions()[0].body.kind else { + panic!("expected returned thunk"); + }; + let TypedValueKind::Thunk(lambda) = &thunk.kind else { + panic!("expected retained thunk body"); + }; + let TypedCompKind::Lam(_, body) = &lambda.kind else { + panic!("expected retained lambda body"); + }; + let TypedCompKind::Bind(_, first_sequence, rest) = &body.kind else { + panic!("expected the capture dup to be sequenced"); + }; + assert_eq!(first_sequence.name().as_str(), names::RC_SEQUENCE_BINDER); + assert_eq!(first_sequence.erase_name().as_str(), "_"); + let TypedCompKind::Bind(_, second_sequence, _) = &rest.kind else { + panic!("expected the discarded parameter drop to be sequenced"); + }; + assert_eq!(second_sequence.name().as_str(), names::RC_SEQUENCE_BINDER); + assert_eq!(second_sequence.erase_name().as_str(), "_"); +} + +#[test] +fn unboxed_products_rewrite_the_thunks_they_contain() { + let int = source(Type::Int); + let source_function = Type::Fun(Vec::new(), EffRow::Empty, Box::new(Type::Int)); + let captured_thunk = |capture: &str| { + let closure_sig = CoreFnSig::new(Vec::new(), Vec::new(), pure(int.clone())); + let closure = TypedComp::new( + pure(CoreType::Function(Box::new(closure_sig))), + TypedCompKind::Lam(Vec::new(), Box::new(ret(var(capture, int.clone())))), + ); + TypedValue::new( + CoreType::Thunk(Box::new(closure.sig.clone())), + TypedValueKind::Thunk(Box::new(closure)), + ) + }; + + let tuple_capture = TypedBinder::new(sym("tuple_capture"), int.clone()); + let tuple = TypedValue::new( + source(Type::UnboxedTuple(vec![source_function.clone()])), + TypedValueKind::UnboxedTuple(vec![captured_thunk("tuple_capture")]), + ); + let tuple_function = function("tuple", vec![tuple_capture], ret(tuple)); + + let field_name = sym("run"); + let record_capture = TypedBinder::new(sym("record_capture"), int.clone()); + let record = TypedValue::new( + source(Type::UnboxedRecord(vec![(field_name, source_function)])), + TypedValueKind::UnboxedRecord(vec![(field_name, captured_thunk("record_capture"))]), + ); + let record_function = function("record", vec![record_capture], ret(record)); + let input = UncheckedTypedCore::new(vec![tuple_function, record_function]); + let actual = run_and_verify(input, &Sigs::new(), &VerifyEnv::new()).erase(); + + let Comp::Return(Value::UnboxedTuple(tuple_fields)) = &actual.fns[0].body else { + panic!("expected unboxed tuple return"); + }; + let Value::Thunk(tuple_closure) = &tuple_fields[0] else { + panic!("expected tuple thunk"); + }; + let Comp::Lam(_, tuple_body) = &**tuple_closure else { + panic!("expected tuple closure"); + }; + let tuple_rest = head_dup(tuple_body, "tuple_capture"); + assert!(matches!( + tuple_rest, + Comp::Return(Value::Var(name)) if *name == sym("tuple_capture") + )); + + let Comp::Return(Value::UnboxedRecord(record_fields)) = &actual.fns[1].body else { + panic!("expected unboxed record return"); + }; + let Value::Thunk(record_closure) = &record_fields[0].1 else { + panic!("expected record thunk"); + }; + let Comp::Lam(_, record_body) = &**record_closure else { + panic!("expected record closure"); + }; + let record_rest = head_dup(record_body, "record_capture"); + assert!(matches!( + record_rest, + Comp::Return(Value::Var(name)) if *name == sym("record_capture") + )); +} + +#[test] +fn branches_and_refs_balance_on_every_path() { + let int = source(Type::Int); + let condition = TypedBinder::new(sym("condition"), source(Type::Bool)); + let cell_ty = CoreType::Ref(Box::new(int.clone())); + let cell = TypedBinder::new(sym("cell"), cell_ty.clone()); + let get = || { + TypedComp::new( + pure(int.clone()), + TypedCompKind::RefGet(var("cell", cell_ty.clone())), + ) + }; + let body = TypedComp::new( + pure(int.clone()), + TypedCompKind::If( + var("condition", source(Type::Bool)), + Box::new(get()), + Box::new(get()), + ), + ); + let input = UncheckedTypedCore::new(vec![function("main", vec![condition, cell], body)]); + let actual = run_and_verify(input, &Sigs::new(), &VerifyEnv::new()).erase(); + + // Each arm must independently balance: the unused boolean is dropped on + // both paths, and the cell is consumed by its read. + let Comp::If(_, yes, no) = &actual.fns[0].body else { + panic!("expected the branch structure to survive RC insertion"); + }; + for branch in [&**yes, &**no] { + let after_drop = head_drop(branch, "condition"); + assert!(matches!( + after_drop, + Comp::RefGet(Value::Var(name)) if *name == sym("cell") + )); + } +} + +#[test] +fn pattern_arms_duplicate_live_fields_before_dropping_the_scrutinee() { + let int = source(Type::Int); + let tuple_ty = source(Type::Tuple(vec![Type::Int])); + let scrutinee = TypedBinder::new(sym("scrutinee"), tuple_ty.clone()); + let field = TypedBinder::new(sym("field"), int.clone()); + let body = TypedComp::new( + pure(int.clone()), + TypedCompKind::Case( + var("scrutinee", tuple_ty), + vec![( + TypedPattern::Tuple(vec![Some(field)]), + ret(var("field", int)), + )], + ), + ); + let input = UncheckedTypedCore::new(vec![function("main", vec![scrutinee], body)]); + let actual = run_and_verify(input, &Sigs::new(), &VerifyEnv::new()).erase(); + let Comp::Case(_, arms) = &actual.fns[0].body else { + panic!("expected case after RC insertion"); + }; + let field_rest = head_dup(&arms[0].1, "field"); + let scrutinee_rest = head_drop(field_rest, "scrutinee"); + assert!(matches!( + scrutinee_rest, + Comp::Return(Value::Var(name)) if *name == sym("field") + )); +} + +#[test] +fn init_at_consumes_the_cell_and_every_constructor_field() { + let int = source(Type::Int); + let tuple = source(Type::Tuple(vec![Type::Int, Type::Int])); + let cell = TypedBinder::new(sym("cell"), int.clone()); + let field = TypedBinder::new(sym("field"), int.clone()); + let ctor = TypedValue::new( + tuple.clone(), + TypedValueKind::Tuple(vec![var("field", int.clone()), var("field", int.clone())]), + ); + let body = TypedComp::new( + pure(tuple), + TypedCompKind::InitAt(var("cell", int.clone()), ctor), + ); + let input = UncheckedTypedCore::new(vec![function("main", vec![cell, field], body)]); + let mut env = VerifyEnv::new(); + env.insert_operation( + sym(ALLOC_OP), + OperationSig::new( + Vec::new(), + vec![int.clone()], + int, + Label::bare(sym("Arena")), + ), + ); + let actual = run_and_verify(input, &Sigs::new(), &env).erase(); + let after_dup = head_dup(&actual.fns[0].body, "field"); + assert!(matches!( + after_dup, + Comp::InitAt(Value::Var(cell), Value::Tuple(fields)) + if *cell == sym("cell") + && matches!( + fields.as_slice(), + [Value::Var(lhs), Value::Var(rhs)] + if *lhs == sym("field") && *rhs == sym("field") + ) + )); +} + +#[test] +fn each_polymorphic_global_capture_retains_at_its_own_instantiation() { + let id = sym("id"); + let parameter_type = sym("a"); + let generic = source(Type::Var(parameter_type)); + let parameter = TypedBinder::new(sym("value"), generic.clone()); + let id_body = ret(var("value", generic.clone())); + let id_sig = CoreFnSig::new( + vec![CoreQuantifier::Type(parameter_type)], + vec![generic.clone()], + pure(generic), + ); + let id_function = TypedCoreFn::new(id, vec![parameter], id_body, id_sig, 0); + + let capture = |name: &str, ty: Type| { + let instance = CoreFnSig::new( + Vec::new(), + vec![source(ty.clone())], + pure(source(ty.clone())), + ); + let global = TypedValue::new( + CoreType::Function(Box::new(instance)), + TypedValueKind::Var { + name: id, + instantiation: vec![CoreInstantiation::Type(ty)], + }, + ); + let closure_sig = CoreFnSig::new(Vec::new(), Vec::new(), pure(global.ty.clone())); + let closure = TypedComp::new( + pure(CoreType::Function(Box::new(closure_sig))), + TypedCompKind::Lam(Vec::new(), Box::new(ret(global))), + ); + function(name, Vec::new(), closure) + }; + let input = UncheckedTypedCore::new(vec![ + id_function, + capture("int_capture", Type::Int), + capture("bool_capture", Type::Bool), + ]); + let actual = run_and_verify(input, &Sigs::new(), &VerifyEnv::new()); + let instance = |ty: &Type| { + CoreType::Function(Box::new(CoreFnSig::new( + Vec::new(), + vec![source(ty.clone())], + pure(source(ty.clone())), + ))) + }; + + // Both closures capture the same symbol at different types. The retain + // names the occurrence that captured it, so each carries its own + // instantiation: a consumer asking which reference a later release + // discharges gets the instance it was taken at, not whichever one the + // whole-program walk happened to reach first. + for (function, ty) in actual.functions()[1..].iter().zip([Type::Int, Type::Bool]) { + let TypedCompKind::Lam(_, body) = &function.body.kind else { + panic!("expected captured global closure"); + }; + let TypedCompKind::Bind(dup, _, _) = &body.kind else { + panic!("expected a capture dup"); + }; + let TypedCompKind::Dup(operand) = &dup.kind else { + panic!("expected a typed dup operand"); + }; + assert_eq!( + operand.ty, + instance(&ty), + "{} retained at the wrong type", + function.name + ); + let TypedValueKind::Var { + name, + instantiation, + } = &operand.kind + else { + panic!("expected the capturing occurrence as the witness"); + }; + assert_eq!(*name, id); + assert_eq!(instantiation.as_slice(), [CoreInstantiation::Type(ty)]); + } +} + +#[test] +fn a_deferred_release_names_the_binder_not_the_borrowed_occurrence() { + let int = source(Type::Int); + let chr = source(Type::Char); + let borrowed = TypedBinder::new(sym("borrowed"), chr.clone()); + let observe = function("observe", vec![borrowed], ret(var("borrowed", chr.clone()))); + let held = TypedBinder::new(sym("held"), int.clone()); + // The occurrence at the call is deliberately not the binder's own value, + // so the two candidate witnesses are distinguishable in the result. + let wrapped = TypedValue::new( + chr.clone(), + TypedValueKind::Reinterpret(Box::new(var("held", int))), + ); + let call = TypedComp::new( + pure(chr), + TypedCompKind::Call { + callee: sym("observe"), + instantiation: Vec::new(), + args: vec![wrapped], + }, + ); + let caller = function("caller", vec![held], call); + let input = UncheckedTypedCore::new(vec![observe, caller]); + let sigs = std::iter::once((sym("observe"), vec![true])).collect(); + let actual = run_and_verify(input, &sigs, &VerifyEnv::new()); + + let TypedCompKind::Bind(_, _, post) = &actual.functions()[1].body.kind else { + panic!("a borrowed call defers its cleanup past the call"); + }; + let TypedCompKind::Bind(release, _, _) = &post.kind else { + panic!("expected the deferred release"); + }; + let TypedCompKind::Drop(operand) = &release.kind else { + panic!("expected a drop"); + }; + assert!( + matches!( + &operand.kind, + TypedValueKind::Var { name, instantiation } + if *name == sym("held") && instantiation.is_empty() + ), + "a release discharges a reference the site owns and does not use, so it \ + has no occurrence to name and must name the binder: {operand:?}" + ); +} + +#[test] +fn a_same_named_local_elsewhere_cannot_supply_a_capture_witness() { + let global_name = sym("f"); + let int = source(Type::Int); + let unit = source(Type::Unit); + let global_sig = CoreFnSig::new(Vec::new(), vec![unit.clone()], pure(unit.clone())); + + let poison_param = TypedBinder::new(global_name, int.clone()); + let poison = function( + "poison", + vec![poison_param], + ret(TypedValue::new( + int, + TypedValueKind::Var { + name: global_name, + instantiation: Vec::new(), + }, + )), + ); + let global_param = TypedBinder::new(sym("arg"), unit); + let global = TypedCoreFn::new( + global_name, + vec![global_param.clone()], + ret(binder_occurrence(&global_param)), + global_sig.clone(), + 0, + ); + let global_value = TypedValue::new( + CoreType::Function(Box::new(global_sig.clone())), + TypedValueKind::Var { + name: global_name, + instantiation: Vec::new(), + }, + ); + let capture_sig = CoreFnSig::new(Vec::new(), Vec::new(), pure(global_value.ty.clone())); + let capture = function( + "capture", + Vec::new(), + TypedComp::new( + pure(CoreType::Function(Box::new(capture_sig))), + TypedCompKind::Lam(Vec::new(), Box::new(ret(global_value))), + ), + ); + let input = UncheckedTypedCore::new(vec![poison, global, capture]); + let actual = run_and_verify(input, &Sigs::new(), &VerifyEnv::new()); + let TypedCompKind::Lam(_, body) = &actual.functions()[2].body.kind else { + panic!("expected global-capturing closure"); + }; + let TypedCompKind::Bind(dup, _, _) = &body.kind else { + panic!("expected capture dup"); + }; + let TypedCompKind::Dup(operand) = &dup.kind else { + panic!("expected typed dup operand"); + }; + assert_eq!( + operand.ty, + CoreType::Function(Box::new(global_sig)), + "the witness comes from the occurrence that captured f, so the earlier \ + local f:Int is not a candidate for it at all" + ); +} + +#[test] +fn insertion_order_is_name_stable() { + let int = source(Type::Int); + let zulu = TypedBinder::new(sym("zulu"), int.clone()); + let alpha = TypedBinder::new(sym("alpha"), int); + let unit = TypedValue::new(source(Type::Unit), TypedValueKind::Unit); + let input = UncheckedTypedCore::new(vec![function("main", vec![zulu, alpha], ret(unit))]); + let actual = run_and_verify(input, &Sigs::new(), &VerifyEnv::new()).erase(); + let rendered = crate::core::pp_core(&actual); + let alpha_at = rendered.find("drop alpha").expect("alpha drop"); + let zulu_at = rendered.find("drop zulu").expect("zulu drop"); + assert!( + zulu_at < alpha_at, + "name-sorted insertion wraps the later name outermost" + ); +} + +#[test] +fn bind_spine_free_variable_work_scales_linearly() { + fn fixture(bindings: usize) -> UncheckedTypedCore { + let unit = source(Type::Unit); + let returned_unit = || ret(TypedValue::new(unit.clone(), TypedValueKind::Unit)); + let mut body = returned_unit(); + for index in (0..bindings).rev() { + body = TypedComp::new( + pure(unit.clone()), + TypedCompKind::Bind( + Box::new(returned_unit()), + TypedBinder::new(sym(&format!("spine_{index}")), unit.clone()), + Box::new(body), + ), + ); + } + UncheckedTypedCore::new(vec![function("main", Vec::new(), body)]) + } + + fn visits(bindings: usize) -> usize { + let input = fixture(bindings); + let input = verify(input, &VerifyEnv::new()).expect("bind-spine fixture must be valid"); + let (owned, visits) = count_free_comp_var_visits(|| insert_rc(input, &Sigs::new())); + verify(owned, &VerifyEnv::new()).expect("RC output must remain valid"); + visits + } + + const SMALL: usize = 128; + const LARGE: usize = 256; + let small = visits(SMALL); + let large = visits(LARGE); + + assert!( + large <= small * 2 + 2, + "doubling a bind spine must approximately double free-variable work: \ + {SMALL} bindings visited {small} nodes, {LARGE} visited {large}" + ); + assert!( + large <= LARGE * 4, + "free-variable work must stay linear in bind-spine length: \ + {LARGE} bindings visited {large} nodes" + ); +} diff --git a/crates/prism-core/src/core/typed/rc/thunks.rs b/crates/prism-core/src/core/typed/rc/thunks.rs new file mode 100644 index 00000000..a8e84b2f --- /dev/null +++ b/crates/prism-core/src/core/typed/rc/thunks.rs @@ -0,0 +1,177 @@ +//! Recursing into the suspended computations a leaf carries. +//! +//! A leaf's own references are counted where it sits, but a thunk it holds has a +//! body with its own ownership partition, and that body has to be walked before +//! the leaf is emitted. + +use crate::core::fbip::Sigs; +use prism_common::fresh::Fresh; + +use super::super::specialize_support::free_comp_vars; +use super::super::{TypedComp, TypedCompKind, TypedValue, TypedValueKind}; +use super::scope::Scope; +use super::{rc, Set}; + +// A thunk cell owns its captures. The suspended body therefore treats captures +// as borrowed while lambda parameters remain owned. +pub(super) fn rc_value( + value: &TypedValue, + sigs: &Sigs, + scope: &mut Scope, + fresh: &mut Fresh, +) -> TypedValue { + let kind = match &value.kind { + TypedValueKind::Thunk(body) => TypedValueKind::Thunk(Box::new(rc( + body, + &Set::new(), + &free_comp_vars(body), + sigs, + scope, + fresh, + ))), + TypedValueKind::Ctor { + name, + tag, + instantiation, + fields, + } => TypedValueKind::Ctor { + name: *name, + tag: *tag, + instantiation: instantiation.clone(), + fields: fields + .iter() + .map(|field| rc_value(field, sigs, scope, fresh)) + .collect(), + }, + TypedValueKind::Tuple(fields) => TypedValueKind::Tuple( + fields + .iter() + .map(|field| rc_value(field, sigs, scope, fresh)) + .collect(), + ), + TypedValueKind::UnboxedTuple(fields) => TypedValueKind::UnboxedTuple( + fields + .iter() + .map(|field| rc_value(field, sigs, scope, fresh)) + .collect(), + ), + TypedValueKind::UnboxedRecord(fields) => TypedValueKind::UnboxedRecord( + fields + .iter() + .map(|(name, field)| (*name, rc_value(field, sigs, scope, fresh))) + .collect(), + ), + TypedValueKind::Reinterpret(inner) => { + TypedValueKind::Reinterpret(Box::new(rc_value(inner, sigs, scope, fresh))) + } + TypedValueKind::LoweredRepr { value, proof } => TypedValueKind::LoweredRepr { + value: Box::new(rc_value(value, sigs, scope, fresh)), + proof: proof.clone(), + }, + TypedValueKind::NewtypeRepr { + constructor, + instantiation, + value, + } => TypedValueKind::NewtypeRepr { + constructor: *constructor, + instantiation: instantiation.clone(), + value: Box::new(rc_value(value, sigs, scope, fresh)), + }, + _ => return value.clone(), + }; + TypedValue::new(value.ty.clone(), kind) +} + +pub(super) fn rc_thunks( + comp: &TypedComp, + sigs: &Sigs, + scope: &mut Scope, + fresh: &mut Fresh, +) -> TypedComp { + let kind = match &comp.kind { + TypedCompKind::Return(result) => { + TypedCompKind::Return(rc_value(result, sigs, scope, fresh)) + } + TypedCompKind::Force(thunk) => TypedCompKind::Force(rc_value(thunk, sigs, scope, fresh)), + TypedCompKind::Error(error) => TypedCompKind::Error(rc_value(error, sigs, scope, fresh)), + TypedCompKind::Io(op, args) => TypedCompKind::Io( + *op, + args.iter() + .map(|arg| rc_value(arg, sigs, scope, fresh)) + .collect(), + ), + TypedCompKind::FloatBuiltin(op, arg) => { + TypedCompKind::FloatBuiltin(*op, rc_value(arg, sigs, scope, fresh)) + } + TypedCompKind::Neg(lane, arg) => { + TypedCompKind::Neg(*lane, rc_value(arg, sigs, scope, fresh)) + } + TypedCompKind::Prim(op, lhs, rhs) => TypedCompKind::Prim( + *op, + rc_value(lhs, sigs, scope, fresh), + rc_value(rhs, sigs, scope, fresh), + ), + TypedCompKind::Call { + callee, + instantiation, + args, + } => TypedCompKind::Call { + callee: *callee, + instantiation: instantiation.clone(), + args: args + .iter() + .map(|arg| rc_value(arg, sigs, scope, fresh)) + .collect(), + }, + TypedCompKind::Do { + operation, + instantiation, + args, + } => TypedCompKind::Do { + operation: *operation, + instantiation: instantiation.clone(), + args: args + .iter() + .map(|arg| rc_value(arg, sigs, scope, fresh)) + .collect(), + }, + TypedCompKind::StrBuiltin { + op, + instantiation, + args, + } => TypedCompKind::StrBuiltin { + op: *op, + instantiation: instantiation.clone(), + args: args + .iter() + .map(|arg| rc_value(arg, sigs, scope, fresh)) + .collect(), + }, + TypedCompKind::App { + callee, + instantiation, + args, + } => TypedCompKind::App { + callee: Box::new(rc_thunks(callee, sigs, scope, fresh)), + instantiation: instantiation.clone(), + args: args + .iter() + .map(|arg| rc_value(arg, sigs, scope, fresh)) + .collect(), + }, + TypedCompKind::RefNew(initial) => { + TypedCompKind::RefNew(rc_value(initial, sigs, scope, fresh)) + } + TypedCompKind::RefGet(cell) => TypedCompKind::RefGet(rc_value(cell, sigs, scope, fresh)), + TypedCompKind::RefSet(cell, new_value) => TypedCompKind::RefSet( + rc_value(cell, sigs, scope, fresh), + rc_value(new_value, sigs, scope, fresh), + ), + TypedCompKind::InitAt(cell, ctor) => TypedCompKind::InitAt( + rc_value(cell, sigs, scope, fresh), + rc_value(ctor, sigs, scope, fresh), + ), + _ => return comp.clone(), + }; + TypedComp::new(comp.sig.clone(), kind) +} diff --git a/crates/prism-core/src/core/typed/reuse.rs b/crates/prism-core/src/core/typed/reuse.rs index 6d68e408..70dcc422 100644 --- a/crates/prism-core/src/core/typed/reuse.rs +++ b/crates/prism-core/src/core/typed/reuse.rs @@ -12,15 +12,16 @@ use prism_syntax::names::reuse_token; use super::{ CoreType, Owned, ReuseLowered, TypedBinder, TypedComp, TypedCompKind, TypedCore, TypedCoreFn, - TypedHandleOp, TypedHandler, TypedPattern, TypedValue, TypedValueKind, + TypedHandleOp, TypedHandler, TypedPattern, TypedValue, TypedValueKind, UncheckedTypedCore, }; /// Pair released constructor shells with fitting allocations without erasing /// any type witnesses. #[must_use] -pub fn reuse(core: TypedCore) -> TypedCore { - TypedCore::new( - core.fns +pub fn reuse(core: TypedCore) -> UncheckedTypedCore { + UncheckedTypedCore::new( + core.into_unchecked() + .into_functions() .into_iter() .map(|function| { TypedCoreFn::new( @@ -93,19 +94,8 @@ fn reuse_arm(scrutinee: &TypedValue, pattern: &TypedPattern, body: &TypedComp) - else { return body.clone(); }; - // A constructor pattern proves that the selected branch holds a boxed cell, - // including constructors from the effect-runtime representation. Tuples - // still need their source tuple witness because unboxed products share the - // tuple-pattern shape. - let capacity = match (pattern, &scrutinee.ty) { - // The wired nullable frees no cell when matched (its native form is - // the null word or the element itself), so it can never seed a token. - (TypedPattern::Ctor { name, .. }, _) if kw::is_or_null_ctor(name.as_str()) => { - return body.clone() - } - (TypedPattern::Ctor { fields, .. }, _) - | (TypedPattern::Tuple(fields), CoreType::Source(Type::Tuple(_))) => fields.len(), - _ => return body.clone(), + let Some(capacity) = reuse_cell_capacity(pattern, &scrutinee.ty) else { + return body.clone(); }; let token = TypedBinder::new( Sym::from(reuse_token(scrutinee_name.as_str())), @@ -242,8 +232,7 @@ fn consume_alloc(comp: &TypedComp, token: &TypedBinder, capacity: usize) -> Opti )) } TypedCompKind::Return(value) - if ctor_arity(value).is_some_and(|arity| arity <= capacity) - && !is_or_null_alloc(value) => + if rebuild_arity(value).is_some_and(|arity| arity <= capacity) => { Some(TypedComp::new( comp.sig.clone(), @@ -303,13 +292,34 @@ fn pattern_binds(pattern: &TypedPattern, name: Sym) -> bool { } } -// The wired nullable allocates no cell, so it can never spend a reuse credit. -fn is_or_null_alloc(value: &TypedValue) -> bool { - matches!(&value.kind, TypedValueKind::Ctor { name, .. } if kw::is_or_null_ctor(name.as_str())) +/// Capacity of the boxed cell a constructor-pattern arm releases, or `None` +/// when the match frees no reusable cell. A constructor pattern proves that +/// the selected branch holds a boxed cell, including constructors from the +/// effect-runtime representation; the wired nullable is excluded because it +/// frees no cell when matched (its native form is the null word or the +/// element itself). Tuples still need their source tuple witness because +/// unboxed products share the tuple-pattern shape. Counting fields measures +/// the cell exactly because every cell slot is one runtime word, an invariant +/// the typed verifier checks at each constructor and boxed tuple. +pub(crate) fn reuse_cell_capacity( + pattern: &TypedPattern, + scrutinee_ty: &CoreType, +) -> Option { + match (pattern, scrutinee_ty) { + (TypedPattern::Ctor { name, .. }, _) if kw::is_or_null_ctor(name.as_str()) => None, + (TypedPattern::Ctor { fields, .. }, _) + | (TypedPattern::Tuple(fields), CoreType::Source(Type::Tuple(_))) => Some(fields.len()), + _ => None, + } } -const fn ctor_arity(value: &TypedValue) -> Option { +/// Arity of an allocation that can be rebuilt inside a spent shell, or `None` +/// when the value allocates no cell. The wired nullable allocates no cell, so +/// it can never spend a reuse credit. As with the capacity, the field count +/// measures the rebuilt cell exactly because each slot is one runtime word. +pub(crate) fn rebuild_arity(value: &TypedValue) -> Option { match &value.kind { + TypedValueKind::Ctor { name, .. } if kw::is_or_null_ctor(name.as_str()) => None, TypedValueKind::Ctor { fields, .. } | TypedValueKind::Tuple(fields) => Some(fields.len()), TypedValueKind::Var { .. } | TypedValueKind::Int(_) @@ -334,8 +344,9 @@ mod tests { use crate::core::{Comp, Core, CoreFn, Value}; use crate::types::ty::EffRow; - use super::super::verify::{verify, ConstructorSig, VerifyEnv}; - use super::super::{CompSig, CoreFnSig, TypedValueKind}; + use super::super::verify::{ConstructorSig, VerifyEnv}; + use super::super::violation::{ArityBound, ReuseFault, Violation}; + use super::super::{verify, CompSig, CoreFnSig, TypedValueKind}; use super::*; fn sym(name: &str) -> Sym { @@ -468,19 +479,17 @@ mod tests { } fn assert_differential( - input: TypedCore, + input: UncheckedTypedCore, env: &VerifyEnv, balance: bool, ) -> TypedCore { - if let Err(violations) = verify(&input, env) { - panic!("owned fixture is invalid: {violations:#?}"); - } + let input = verify(input, env) + .unwrap_or_else(|violations| panic!("owned fixture is invalid: {violations:#?}")); let legacy_input = input.clone().erase(); let expected = legacy_reuse(&legacy_input); - let actual = reuse(input); - if let Err(violations) = verify(&actual, env) { - panic!("reuse-lowered fixture is invalid: {violations:#?}"); - } + let actual = verify(reuse(input), env).unwrap_or_else(|violations| { + panic!("reuse-lowered fixture is invalid: {violations:#?}") + }); assert_eq!(actual.clone().erase(), expected); if balance { if let Err(error) = balanced(&actual.clone().erase(), &Sigs::new()) { @@ -504,7 +513,7 @@ mod tests { pattern("Wide", 2), after_drop("cell", shape, ret(rebuild)), ); - let input = TypedCore::new(vec![function("main", vec![scrutinee], body)]); + let input = UncheckedTypedCore::new(vec![function("main", vec![scrutinee], body)]); let actual = assert_differential(input, &env, true).erase(); let Comp::Case(_, arms) = single_body(&actual) else { @@ -531,7 +540,7 @@ mod tests { pattern("Wide", 2), ret(ctor("Narrow", 1, shape, vec![int(7)])), ); - let input = TypedCore::new(vec![function("main", vec![scrutinee], body)]); + let input = UncheckedTypedCore::new(vec![function("main", vec![scrutinee], body)]); let actual = assert_differential(input, &env, false).erase(); assert!(!format!("{:?}", single_body(&actual)).contains("WithReuse")); } @@ -546,7 +555,7 @@ mod tests { pattern("Narrow", 1), after_drop("cell", shape, ret(rebuild)), ); - let input = TypedCore::new(vec![function("main", vec![scrutinee], body)]); + let input = UncheckedTypedCore::new(vec![function("main", vec![scrutinee], body)]); let actual = assert_differential(input, &env, true).erase(); assert!(!format!("{:?}", single_body(&actual)).contains("WithReuse")); } @@ -568,7 +577,7 @@ mod tests { pattern("Wide", 2), after_drop("cell", shape, branches), ); - let input = TypedCore::new(vec![function("main", vec![scrutinee], body)]); + let input = UncheckedTypedCore::new(vec![function("main", vec![scrutinee], body)]); let actual = assert_differential(input, &env, true).erase(); let Comp::Case(_, arms) = single_body(&actual) else { panic!("expected a case"); @@ -610,7 +619,7 @@ mod tests { after_drop("cell", shape, branches), ); let main = function("main", vec![scrutinee], body); - let input = TypedCore::new(vec![main, factory]); + let input = UncheckedTypedCore::new(vec![main, factory]); let actual = assert_differential(input, &env, true).erase(); assert!(!format!("{:?}", single_body(&actual)).contains("WithReuse")); } @@ -631,10 +640,10 @@ mod tests { shadowed_tail, ); let body = case(var("cell", shape), pattern("Wide", 2), arm); - let input = TypedCore::new(vec![function("main", vec![outer], body)]); - verify(&input, &env).expect("shadowing fixture is valid Owned Core"); + let input = UncheckedTypedCore::new(vec![function("main", vec![outer], body)]); + let input = verify(input, &env).expect("shadowing fixture is valid Owned Core"); let actual = reuse(input); - verify(&actual, &env).expect("the safe no-reuse result remains valid"); + let actual = verify(actual, &env).expect("the safe no-reuse result remains valid"); assert!(!format!("{:?}", single_body(&actual.erase())).contains("WithReuse")); } @@ -653,10 +662,10 @@ mod tests { ret(ctor("Narrow", 1, shape.clone(), vec![int(1)])), ); let body = case(var("cell", shape), pattern, arm); - let input = TypedCore::new(vec![function("main", vec![outer], body)]); - verify(&input, &env).expect("pattern-shadow fixture is valid Owned Core"); + let input = UncheckedTypedCore::new(vec![function("main", vec![outer], body)]); + let input = verify(input, &env).expect("pattern-shadow fixture is valid Owned Core"); let actual = reuse(input); - verify(&actual, &env).expect("the safe no-reuse result remains valid"); + let actual = verify(actual, &env).expect("the safe no-reuse result remains valid"); assert!(!format!("{:?}", single_body(&actual.erase())).contains("WithReuse")); } @@ -674,11 +683,11 @@ mod tests { ); let arm = after_drop("cell", shape.clone(), shadowed_tail); let body = case(var("cell", shape), pattern("Wide", 2), arm); - let input = TypedCore::new(vec![function("main", vec![cell, other], body)]); - verify(&input, &env).expect("token-capture fixture is valid Owned Core"); + let input = UncheckedTypedCore::new(vec![function("main", vec![cell, other], body)]); + let input = verify(input, &env).expect("token-capture fixture is valid Owned Core"); balanced(&input.clone().erase(), &Sigs::new()).expect("the Owned fixture is balanced"); let actual = reuse(input); - verify(&actual, &env).expect("the safe no-reuse result remains valid"); + let actual = verify(actual, &env).expect("the safe no-reuse result remains valid"); assert!(!format!("{:?}", single_body(&actual.erase())).contains("WithReuse")); } @@ -701,7 +710,7 @@ mod tests { ); let inner_case = case(var("inner", shape.clone()), pattern("Wide", 2), inner_body); let outer_case = case(var("outer", shape), pattern("Wide", 2), inner_case); - let input = TypedCore::new(vec![function("main", vec![outer, inner], outer_case)]); + let input = UncheckedTypedCore::new(vec![function("main", vec![outer, inner], outer_case)]); let actual = assert_differential(input, &env, true).erase(); let rendered = format!("{:?}", single_body(&actual)); assert_eq!(rendered.matches("WithReuse").count(), 2); @@ -735,7 +744,7 @@ mod tests { pattern("OldShell", 2), after_drop("old", old_ty, ret(rebuild)), ); - let input = TypedCore::new(vec![function("main", vec![old], body)]); + let input = UncheckedTypedCore::new(vec![function("main", vec![old], body)]); let actual = assert_differential(input, &env, true).erase(); assert!(format!("{:?}", single_body(&actual)).contains("WithReuse")); } @@ -768,11 +777,13 @@ mod tests { }, ); let body = case(var("cell", shape), pattern("Wide", 2), body); - let forged = TypedCore::::new(vec![function("main", vec![freed], body)]); - let violations = verify(&forged, &env).expect_err("one branch leaves the credit live"); - assert!(violations.iter().any(|violation| violation - .message() - .contains("branches consume different reuse-token credits"))); + let forged = + UncheckedTypedCore::::new(vec![function("main", vec![freed], body)]); + let violations = verify(forged, &env).expect_err("one branch leaves the credit live"); + assert!(violations.iter().any(|violation| matches!( + violation.kind(), + Violation::Reuse(ReuseFault::UnequalCredits(_)) + ))); } #[test] @@ -797,11 +808,16 @@ mod tests { }, ); let body = case(var("cell", shape), pattern("Narrow", 1), body); - let forged = TypedCore::::new(vec![function("main", vec![freed], body)]); - let violations = verify(&forged, &env).expect_err("the rebuild exceeds the shell"); - assert!(violations - .iter() - .any(|violation| violation.message().contains("exceeds shell capacity"))); + let forged = + UncheckedTypedCore::::new(vec![function("main", vec![freed], body)]); + let violations = verify(forged, &env).expect_err("the rebuild exceeds the shell"); + assert!(violations.iter().any(|violation| matches!( + violation.kind(), + Violation::Arity { + bound: ArityBound::ShellCapacity, + .. + } + ))); } #[test] @@ -827,11 +843,15 @@ mod tests { body: Box::new(spend), }, ); - let forged = TypedCore::::new(vec![function("main", vec![freed], body)]); - let violations = verify(&forged, &env).expect_err("reuse needs case-shell authority"); - assert!(violations.iter().any(|violation| violation - .message() - .contains("does not free the active boxed case scrutinee"))); + let forged = + UncheckedTypedCore::::new(vec![function("main", vec![freed], body)]); + let violations = verify(forged, &env).expect_err("reuse needs case-shell authority"); + assert!( + violations + .iter() + .any(|violation| violation.kind() + == &Violation::Reuse(ReuseFault::ScrutineeNotActive)) + ); } #[test] @@ -877,11 +897,12 @@ mod tests { }, ); let body = case(var("cell", shape), pattern("Wide", 2), body); - let forged = TypedCore::::new(vec![function("main", vec![freed], body)]); - let violations = verify(&forged, &env).expect_err("one shell cannot be freed twice"); - assert!(violations - .iter() - .any(|violation| violation.message().contains("freed more than once"))); + let forged = + UncheckedTypedCore::::new(vec![function("main", vec![freed], body)]); + let violations = verify(forged, &env).expect_err("one shell cannot be freed twice"); + assert!(violations.iter().any( + |violation| violation.kind() == &Violation::Reuse(ReuseFault::ScrutineeFreedTwice) + )); } #[test] diff --git a/crates/prism-core/src/core/typed/simplify.rs b/crates/prism-core/src/core/typed/simplify.rs index 55447504..ec030184 100644 --- a/crates/prism-core/src/core/typed/simplify.rs +++ b/crates/prism-core/src/core/typed/simplify.rs @@ -23,8 +23,8 @@ use prism_syntax::error::TypedCoreSimplifyFailure; use super::specialize_support::{free_comp_vars, free_value_vars, Rewrite}; use super::verify::instantiate_value_scheme; use super::{ - CompSig, TypedBinder, TypedComp, TypedCompKind, TypedCore, TypedHandleOp, TypedHandler, - TypedPattern, TypedValue, TypedValueKind, + CompSig, TypedBinder, TypedComp, TypedCompKind, TypedHandleOp, TypedHandler, TypedPattern, + TypedValue, TypedValueKind, UncheckedTypedCore, }; // A runaway guard: a correct fixed point converges far below this, so exceeding @@ -51,13 +51,19 @@ impl SimplifyStats { /// the runaway guard, which means a rule is fighting itself rather than /// converging. pub fn simplify

( - core: TypedCore

, -) -> Result<(TypedCore

, SimplifyStats), TypedCoreSimplifyFailure> { + core: UncheckedTypedCore

, +) -> Result<(UncheckedTypedCore

, SimplifyStats), TypedCoreSimplifyFailure> { let mut current = core; let mut total = 0u64; loop { let mut pass = Simplifier { ticks: 0 }; - current = pass.core(¤t, &Env::new()); + current = UncheckedTypedCore::new( + current + .functions() + .iter() + .map(|function| pass.function(function, &Env::new())) + .collect(), + ); total += pass.ticks; if total > MAX_TICKS { return Err(TypedCoreSimplifyFailure::RunawayRewrite { ticks: total }); @@ -737,9 +743,10 @@ mod tests { use crate::types::Type; use super::super::effect_lower::lower_effects; - use super::super::verify::{verify, OperationSig, VerifyEnv}; + use super::super::verify::{OperationSig, VerifyEnv}; use super::super::{ - CoreFnSig, CoreQuantifier, CoreType, EffectLowered, Elaborated, TypedCoreFn, TypedLowering, + verify, CoreFnSig, CoreQuantifier, CoreType, EffectLowered, Elaborated, TypedCore, + TypedCoreFn, TypedLowering, UncheckedTypedCore, }; use super::*; @@ -788,18 +795,15 @@ mod tests { } fn run_simplify(functions: Vec, env: &VerifyEnv) -> (TypedCore, u64) { - let input = TypedCore::new(functions); - if let Err(violations) = verify(&input, env) { - panic!("input fixture is invalid: {violations:#?}"); - } + let input = UncheckedTypedCore::new(functions); let (actual, stats) = simplify(input).expect("typed simplification"); - if let Err(violations) = verify(&actual, env) { - panic!("simplified typed Core is invalid: {violations:#?}"); - } + let actual = verify(actual, env).unwrap_or_else(|violations| { + panic!("simplified typed Core is invalid: {violations:#?}") + }); (actual, stats.ticks()) } - fn lowered_simplify_fixture() -> (TypedCore, VerifyEnv) { + fn lowered_simplify_fixture() -> (UncheckedTypedCore, VerifyEnv) { let operation = sym("ask"); let effect = sym("Ask"); let mut env = VerifyEnv::new(); @@ -832,10 +836,9 @@ mod tests { ), 0, ); - let input = TypedCore::::new(vec![main]); - if let Err(violations) = verify(&input, &env) { - panic!("elaborated late-pass fixture is invalid: {violations:#?}"); - } + let input = verify(UncheckedTypedCore::::new(vec![main]), &env).unwrap_or_else( + |violations| panic!("elaborated late-pass fixture is invalid: {violations:#?}"), + ); let flags = DynFlags { effect_tier: EffectTier::FreeMonad, quiet: true, @@ -875,10 +878,7 @@ mod tests { ); let mut functions = lowered.functions().to_vec(); functions.push(target); - let core = TypedCore::::new(functions); - if let Err(violations) = verify(&core, &env) { - panic!("effect-lowered late-pass fixture is invalid: {violations:#?}"); - } + let core = UncheckedTypedCore::::new(functions); (core, env) } @@ -886,9 +886,9 @@ mod tests { fn effect_lowered_simplify_collapses_the_copy_binding() { let (input, env) = lowered_simplify_fixture(); let (actual, stats) = simplify(input).expect("effect-lowered fixture simplifies"); - if let Err(violations) = verify(&actual, &env) { - panic!("effect-lowered Simplify output is invalid: {violations:#?}"); - } + let actual = verify(actual, &env).unwrap_or_else(|violations| { + panic!("effect-lowered Simplify output is invalid: {violations:#?}") + }); assert!(stats.ticks() >= 2, "the lowered fixture must simplify"); let target = actual .functions() @@ -978,12 +978,11 @@ mod tests { 0, ); let env = VerifyEnv::new(); - let input = TypedCore::::new(vec![caller, consume]); - assert_eq!(verify(&input, &env), Ok(())); + let input = UncheckedTypedCore::::new(vec![caller, consume]); let (output, stats) = simplify(input).expect("simplification converges"); assert!(stats.ticks() > 0); - assert_eq!(verify(&output, &env), Ok(())); + let output = verify(output, &env).expect("simplified alias fixture verifies"); // Copy propagation rewrites the `t` occurrence to `h` reinstantiated at // the use site's empty row, so the `let t = h` binding is dead and the diff --git a/crates/prism-core/src/core/typed/specialize.rs b/crates/prism-core/src/core/typed/specialize.rs index b5cf3e2d..cf5833ba 100644 --- a/crates/prism-core/src/core/typed/specialize.rs +++ b/crates/prism-core/src/core/typed/specialize.rs @@ -21,7 +21,7 @@ use super::verify::{substitute_core_type, substitute_sig}; use super::{ instantiate_fn, CoreFnSig, CoreInstantiation, CoreQuantifier, CoreType, TypedBinder, TypedComp, TypedCompKind, TypedCore, TypedCoreFn, TypedHandleOp, TypedHandler, TypedPattern, TypedValue, - TypedValueKind, + TypedValueKind, UncheckedTypedCore, }; /// Rewrite counts for typed dictionary specialization. @@ -47,14 +47,14 @@ impl SpecializeStats { /// whose erasure would change the compatibility tree. pub fn specialize

( core: TypedCore

, -) -> Result<(TypedCore

, SpecializeStats), TypedCoreSpecializationFailure> { +) -> Result<(UncheckedTypedCore

, SpecializeStats), TypedCoreSpecializationFailure> { let builders = builders(&core); let constrained = constrained(&core); if builders.is_empty() || constrained.is_empty() { - return Ok((core, SpecializeStats::default())); + return Ok((core.into_unchecked(), SpecializeStats::default())); } - let bodies = core - .fns + let source_functions = core.into_unchecked().into_functions(); + let bodies = source_functions .iter() .map(|function| (function.name, function.clone())) .collect(); @@ -70,8 +70,7 @@ pub fn specialize

( failure: None, }; let empty = BTreeMap::new(); - let mut functions: Vec<_> = core - .fns + let mut functions: Vec<_> = source_functions .iter() .map(|function| pass.function(function, &empty)) .collect(); @@ -87,7 +86,10 @@ pub fn specialize

( .iter() .map(|function| dce.function(function, &())) .collect(); - Ok((TypedCore::new(functions), SpecializeStats { ticks })) + Ok(( + UncheckedTypedCore::new(functions), + SpecializeStats { ticks }, + )) } #[derive(Clone)] @@ -96,7 +98,7 @@ struct Builder { } fn builders

(core: &TypedCore

) -> BTreeMap { - core.fns + core.functions() .iter() .filter(|function| function.params.is_empty()) .filter_map(|function| match &function.body.kind { @@ -115,7 +117,7 @@ fn builders

(core: &TypedCore

) -> BTreeMap { } fn constrained

(core: &TypedCore

) -> BTreeMap { - core.fns + core.functions() .iter() .filter(|function| function.dict_arity > 0) .map(|function| (function.name, function.dict_arity)) @@ -1761,14 +1763,12 @@ mod tests { functions: Vec, env: &VerifyEnv, ) -> (TypedCore, u64) { - let input = TypedCore::new(functions); - if let Err(violations) = verify(&input, env) { - panic!("input fixture is invalid: {violations:#?}"); - } + let input = verify(UncheckedTypedCore::::new(functions), env) + .unwrap_or_else(|violations| panic!("input fixture is invalid: {violations:#?}")); let (actual, stats) = specialize(input).expect("typed specialization"); - if let Err(violations) = verify(&actual, env) { - panic!("specialized typed Core is invalid: {violations:#?}"); - } + let actual = verify(actual, env).unwrap_or_else(|violations| { + panic!("specialized typed Core is invalid: {violations:#?}") + }); (actual, stats.ticks()) } diff --git a/crates/prism-core/src/core/typed/specialize_support.rs b/crates/prism-core/src/core/typed/specialize_support.rs index ea05da8a..9f98055e 100644 --- a/crates/prism-core/src/core/typed/specialize_support.rs +++ b/crates/prism-core/src/core/typed/specialize_support.rs @@ -10,13 +10,15 @@ use std::collections::{BTreeMap, BTreeSet}; use prism_common::sym::Sym; use prism_syntax::names; +use crate::core::work; + use super::verify::{ substitute_core_type, substitute_fn_sig, substitute_label, substitute_row, substitute_sig, substitute_type, }; use super::{ CompSig, CoreFnSig, CoreInstantiation, CoreQuantifier, CoreType, TypedBinder, TypedComp, - TypedCompKind, TypedCore, TypedCoreFn, TypedForward, TypedHandleOp, TypedHandler, TypedPattern, + TypedCompKind, TypedCoreFn, TypedForward, TypedHandleOp, TypedHandler, TypedPattern, TypedValue, TypedValueKind, }; @@ -104,15 +106,6 @@ pub(crate) trait Rewrite { ) } - fn core

(&mut self, core: &TypedCore

, cx: &Self::Ctx) -> TypedCore

{ - TypedCore::new( - core.fns - .iter() - .map(|function| self.function(function, cx)) - .collect(), - ) - } - fn instantiations( &mut self, instantiations: &[CoreInstantiation], @@ -126,6 +119,8 @@ pub(crate) trait Rewrite { #[allow(clippy::too_many_lines)] fn descend_value(&mut self, value: &TypedValue, cx: &Self::Ctx) -> TypedValue { + let _frame = work::frame(); + work::rebuild(); let kind = match &value.kind { TypedValueKind::Var { name, @@ -187,6 +182,8 @@ pub(crate) trait Rewrite { #[allow(clippy::too_many_lines)] fn descend_comp(&mut self, comp: &TypedComp, cx: &Self::Ctx) -> TypedComp { + let _frame = work::frame(); + work::rebuild(); let kind = match &comp.kind { TypedCompKind::Return(value) => TypedCompKind::Return(self.value(value, cx)), TypedCompKind::Bind(first, binder, rest) => TypedCompKind::Bind( @@ -659,6 +656,68 @@ impl Rewrite for TermSubstitution<'_> { } } +/// What the traversal hands a sink for one free reference. +/// +/// Almost every reference is a value occurrence, which is the term the name was +/// read through and therefore the only witness that records its instantiation. A +/// reuse token is the exception: it names a binder directly with no value around +/// it, so its witness is the binder itself. +pub(crate) enum FreeRef<'a> { + Occurrence(&'a TypedValue), + Token(&'a TypedBinder), +} + +/// Where a free-variable walk sends the references it finds. +/// +/// The traversal carries no policy: a caller that wants names alone collects +/// into a set, and a caller that has to justify a later rewrite collects the +/// witnesses too, both from this one walk. Reference counting needs the second +/// and must not pay for a second traversal to get it. +pub(crate) trait FreeRefs { + fn see(&mut self, name: Sym, reference: &FreeRef<'_>); +} + +impl FreeRefs for BTreeSet { + fn see(&mut self, name: Sym, _: &FreeRef<'_>) { + self.insert(name); + } +} + +/// The value a binder denotes where it is in scope. +/// +/// A binder is monomorphic at its own binding site, so its occurrence carries an +/// empty instantiation by construction. +pub(crate) fn binder_occurrence(binder: &TypedBinder) -> TypedValue { + TypedValue::new( + binder.ty.clone(), + TypedValueKind::Var { + name: binder.name, + instantiation: Vec::new(), + }, + ) +} + +/// First witness per free name in a typed computation. +/// +/// "First" is in traversal order, which makes the map deterministic, and every +/// entry is a reference live somewhere inside `comp`. That is the property a +/// consumer needs: a witness taken from this map justifies an operation emitted +/// against this subtree, unlike one taken from a whole-program index. +pub(crate) fn free_comp_var_witnesses(comp: &TypedComp) -> BTreeMap { + struct First(BTreeMap); + impl FreeRefs for First { + fn see(&mut self, name: Sym, reference: &FreeRef<'_>) { + self.0.entry(name).or_insert_with(|| match reference { + FreeRef::Occurrence(value) => (*value).clone(), + FreeRef::Token(binder) => binder_occurrence(binder), + }); + } + } + let mut sink = First(BTreeMap::new()); + collect_comp_vars(comp, &mut BoundStack::new(), &mut sink); + sink.0 +} + /// Free local/global term references in a typed computation. pub(crate) fn free_comp_vars(comp: &TypedComp) -> BTreeSet { let mut free = BTreeSet::new(); @@ -752,17 +811,17 @@ impl BoundStack { } } -fn collect_ref(name: Sym, bound: &BoundStack, free: &mut BTreeSet) { +fn collect_ref(name: Sym, reference: &FreeRef<'_>, bound: &BoundStack, free: &mut S) { if !bound.contains(name) { - free.insert(name); + free.see(name, reference); } } -fn under( +fn under( bound: &mut BoundStack, names: impl IntoIterator, body: &TypedComp, - free: &mut BTreeSet, + free: &mut S, ) { let mark = bound.mark(); bound.push_all(names); @@ -770,9 +829,11 @@ fn under( bound.pop_to(mark); } -fn collect_value_vars(value: &TypedValue, bound: &mut BoundStack, free: &mut BTreeSet) { +fn collect_value_vars(value: &TypedValue, bound: &mut BoundStack, free: &mut S) { match &value.kind { - TypedValueKind::Var { name, .. } => collect_ref(*name, bound, free), + TypedValueKind::Var { name, .. } => { + collect_ref(*name, &FreeRef::Occurrence(value), bound, free); + } TypedValueKind::Reinterpret(value) | TypedValueKind::LoweredRepr { value, proof: _ } | TypedValueKind::NewtypeRepr { value, .. } => { @@ -802,7 +863,7 @@ fn collect_value_vars(value: &TypedValue, bound: &mut BoundStack, free: &mut BTr } #[allow(clippy::too_many_lines)] -fn collect_comp_vars(comp: &TypedComp, bound: &mut BoundStack, free: &mut BTreeSet) { +fn collect_comp_vars(comp: &TypedComp, bound: &mut BoundStack, free: &mut S) { #[cfg(test)] FREE_COMP_VAR_VISITS.with(|visits| { if let Some(count) = visits.get() { @@ -821,7 +882,7 @@ fn collect_comp_vars(comp: &TypedComp, bound: &mut BoundStack, free: &mut BTreeS | TypedCompKind::RefNew(value) | TypedCompKind::RefGet(value) => collect_value_vars(value, bound, free), TypedCompKind::Reuse(token, value) => { - collect_ref(token.name, bound, free); + collect_ref(token.name, &FreeRef::Token(token), bound, free); collect_value_vars(value, bound, free); } TypedCompKind::Prim(_, lhs, rhs) diff --git a/crates/prism-core/src/core/typed/verify.rs b/crates/prism-core/src/core/typed/verify.rs deleted file mode 100644 index 29f27757..00000000 --- a/crates/prism-core/src/core/typed/verify.rs +++ /dev/null @@ -1,3965 +0,0 @@ -//! Independent proof checker for witness-carrying Core. -//! -//! This module deliberately does not call inference or unification. Every -//! polymorphic use carries an explicit instantiation; checking substitutes that -//! evidence into a declared scheme and compares the stored witnesses exactly. - -use std::collections::{BTreeMap, BTreeSet}; -use std::fmt; -use std::marker::PhantomData; - -use crate::types::ty::{EffRow, Label}; -use crate::types::{repr_of_type, Type}; -use prism_common::sym::Sym; -use prism_syntax::names::{self, ALLOC_OP, IO_EFFECT}; - -use super::build::lower_value_type; -use super::{ - ArenaPrepared, BinderErasure, CompSig, CoreFnSig, CoreInstantiation, CoreQuantifier, CoreType, - EffectLowered, Elaborated, LoweredType, Owned, ReuseLowered, TypedBinder, TypedComp, - TypedCompKind, TypedCore, TypedCoreFn, TypedHandleOp, TypedHandler, TypedPattern, TypedValue, - TypedValueKind, -}; -use super::{CORE_GROW_STACK, CORE_MIN_STACK}; -use crate::core::builtins::Builtin; -use crate::core::CoreOp::{ - Add, Addf, Div, Divf, Eq, Eqf, Ge, Gef, Gt, Gtf, Le, Lef, Lt, Ltf, Mul, Mulf, Ne, Nef, Rem, - Sub, Subf, -}; -use crate::core::{CoreOp, IoOp, NegLane}; - -/// The declared shape of a data constructor. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ConstructorSig { - quantifiers: Vec, - tag: usize, - fields: Vec, - result: CoreType, -} - -impl ConstructorSig { - #[must_use] - pub const fn new( - quantifiers: Vec, - tag: usize, - fields: Vec, - result: CoreType, - ) -> Self { - Self { - quantifiers, - tag, - fields, - result, - } - } - - #[must_use] - pub fn quantifiers(&self) -> &[CoreQuantifier] { - &self.quantifiers - } -} - -/// The declared signature and owning effect of an operation. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct OperationSig { - quantifiers: Vec, - params: Vec, - result: CoreType, - effect: Label, -} - -impl OperationSig { - #[must_use] - pub const fn new( - quantifiers: Vec, - params: Vec, - result: CoreType, - effect: Label, - ) -> Self { - Self { - quantifiers, - params, - result, - effect, - } - } - - #[must_use] - pub fn quantifiers(&self) -> &[CoreQuantifier] { - &self.quantifiers - } - - #[must_use] - pub fn params(&self) -> &[CoreType] { - &self.params - } - - #[must_use] - pub const fn result(&self) -> &CoreType { - &self.result - } - - #[must_use] - pub const fn effect(&self) -> &Label { - &self.effect - } -} - -/// Declarations needed to check Core nodes independently of the producer. -#[derive(Clone, Debug, Default)] -pub struct VerifyEnv { - constructors: BTreeMap, - newtype_constructors: BTreeSet, - operations: BTreeMap, - builtin_overrides: BTreeMap, -} - -impl VerifyEnv { - /// An empty environment, suitable for Core containing only functions and - /// intrinsic nodes. - #[must_use] - pub const fn new() -> Self { - Self { - constructors: BTreeMap::new(), - newtype_constructors: BTreeSet::new(), - operations: BTreeMap::new(), - builtin_overrides: BTreeMap::new(), - } - } - - pub fn insert_constructor(&mut self, name: Sym, sig: ConstructorSig) { - self.constructors.insert(name, sig); - } - - pub fn mark_newtype_constructor(&mut self, name: Sym) { - self.newtype_constructors.insert(name); - } - - pub fn insert_operation(&mut self, name: Sym, sig: OperationSig) { - self.operations.insert(name, sig); - } - - pub fn insert_builtin_override(&mut self, op: Builtin, sig: CoreFnSig) { - self.builtin_overrides.insert(op.wire(), sig); - } - - #[must_use] - pub fn constructor(&self, name: Sym) -> Option<&ConstructorSig> { - self.constructors.get(&name) - } - - #[must_use] - pub fn operation(&self, name: Sym) -> Option<&OperationSig> { - self.operations.get(&name) - } - - #[must_use] - pub const fn operations(&self) -> &BTreeMap { - &self.operations - } - - #[must_use] - pub fn builtin_override(&self, op: Builtin) -> Option<&CoreFnSig> { - self.builtin_overrides.get(&op.wire()) - } -} - -/// One failed typed-Core judgment. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CoreViolation { - function: Sym, - path: String, - message: String, -} - -impl CoreViolation { - /// Function containing the invalid node. - #[must_use] - pub const fn function(&self) -> Sym { - self.function - } - - /// Stable structural path from the function body to the invalid witness. - #[must_use] - pub fn path(&self) -> &str { - &self.path - } - - /// Human-readable failed judgment. - #[must_use] - pub fn message(&self) -> &str { - &self.message - } -} - -impl fmt::Display for CoreViolation { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} at {}: {}", self.function, self.path, self.message) - } -} - -impl std::error::Error for CoreViolation {} - -mod sealed { - pub trait Sealed {} -} - -/// A typed-Core stage with a fixed legal node vocabulary. -pub trait TypedCorePhase: sealed::Sealed { - #[doc(hidden)] - const ALLOW_EFFECT_NODES: bool; - #[doc(hidden)] - const ALLOW_INIT_AT_NODES: bool; - #[doc(hidden)] - const ALLOW_REF_NODES: bool; - #[doc(hidden)] - const ALLOW_RC_NODES: bool; - #[doc(hidden)] - const ALLOW_REUSE_NODES: bool; - #[doc(hidden)] - const ALLOW_LOWERED_ABI: bool; - #[doc(hidden)] - const NAME: &'static str; -} - -macro_rules! phase { - ($phase:ty, $name:literal, $effect:literal, $init_at:literal, $refs:literal, $rc:literal, $reuse:literal, $lowered:literal) => { - impl sealed::Sealed for $phase {} - impl TypedCorePhase for $phase { - const ALLOW_EFFECT_NODES: bool = $effect; - const ALLOW_INIT_AT_NODES: bool = $init_at; - const ALLOW_REF_NODES: bool = $refs; - const ALLOW_RC_NODES: bool = $rc; - const ALLOW_REUSE_NODES: bool = $reuse; - const ALLOW_LOWERED_ABI: bool = $lowered; - const NAME: &'static str = $name; - } - }; -} - -phase!( - Elaborated, - "elaborated", - true, - false, - false, - false, - false, - false -); -phase!( - ArenaPrepared, - "arena-prepared", - true, - true, - false, - false, - false, - false -); -phase!( - EffectLowered, - "effect-lowered", - false, - true, - true, - false, - false, - true -); -phase!(Owned, "owned", false, true, true, true, false, true); -phase!( - ReuseLowered, - "reuse-lowered", - false, - true, - true, - true, - true, - true -); - -/// Check all stored Core judgments without inference or unification. -/// -/// # Errors -/// Returns every independently observed violation. Errors at a parent whose -/// premise is already invalid may be omitted to avoid cascading diagnostics. -pub fn verify( - core: &TypedCore

, - env: &VerifyEnv, -) -> Result<(), Vec> { - let mut globals = BTreeMap::new(); - let mut duplicate_globals = BTreeSet::new(); - for function in core.functions() { - if globals - .insert(function.name(), function.sig().clone()) - .is_some() - { - duplicate_globals.insert(function.name()); - } - } - - let mut violations = Vec::new(); - for function in core.functions() { - let mut checker = Checker::

::new(function.name(), env, &globals); - if duplicate_globals.contains(&function.name()) { - checker.fail("duplicate global function identity"); - } - checker.function(function); - violations.extend(checker.violations); - } - - if violations.is_empty() { - Ok(()) - } else { - Err(violations) - } -} - -struct Checker<'a, P> { - function: Sym, - env: &'a VerifyEnv, - globals: &'a BTreeMap, - locals: BTreeMap>, - token_uses: BTreeMap>, - token_capacities: BTreeMap>, - reuse_shells: BTreeMap>, - allowed_types: BTreeSet, - allowed_rows: BTreeSet, - path: Vec, - violations: Vec, - phase: PhantomData

, -} - -impl<'a, P: TypedCorePhase> Checker<'a, P> { - fn new(function: Sym, env: &'a VerifyEnv, globals: &'a BTreeMap) -> Self { - Self { - function, - env, - globals, - locals: BTreeMap::new(), - token_uses: BTreeMap::new(), - token_capacities: BTreeMap::new(), - reuse_shells: BTreeMap::new(), - allowed_types: BTreeSet::new(), - allowed_rows: BTreeSet::new(), - path: vec!["body".into()], - violations: Vec::new(), - phase: PhantomData, - } - } - - fn fail(&mut self, message: impl Into) { - self.violations.push(CoreViolation { - function: self.function, - path: self.path.join("."), - message: message.into(), - }); - } - - fn at(&mut self, segment: impl Into, f: impl FnOnce(&mut Self)) { - self.path.push(segment.into()); - f(self); - self.path.pop(); - } - - fn function(&mut self, function: &TypedCoreFn) { - for quantifier in function.sig().quantifiers() { - match quantifier { - CoreQuantifier::Type(name) => { - if self.allowed_rows.contains(name) || !self.allowed_types.insert(*name) { - self.fail(format!("duplicate type quantifier {name}")); - } - } - CoreQuantifier::Row(name) => { - if self.allowed_types.contains(name) || !self.allowed_rows.insert(*name) { - self.fail(format!("duplicate row quantifier {name}")); - } - } - } - } - self.check_fn_sig(function.sig()); - - if function.dict_arity() > function.params().len() { - self.fail(format!( - "dictionary arity {} exceeds parameter arity {}", - function.dict_arity(), - function.params().len() - )); - } - if function.params().len() != function.sig().params().len() { - self.fail(format!( - "parameter arity {} does not match signature arity {}", - function.params().len(), - function.sig().params().len() - )); - } - let mut parameter_names = BTreeSet::new(); - for (index, parameter) in function.params().iter().enumerate() { - self.at(format!("param[{index}]"), |this| { - if let Some(expected) = function.sig().params().get(index) { - this.expect_type(parameter.ty(), expected, "parameter witness"); - } - if !parameter_names.insert(parameter.name()) { - this.fail(format!( - "binder identity {} is duplicated in the parameter list", - parameter.name() - )); - } - this.bind(parameter); - }); - } - - self.comp(function.body()); - self.expect_subtype_sig( - function.body().sig(), - function.sig().body(), - "function body", - ); - } - - fn bind(&mut self, binder: &TypedBinder) { - if binder.erasure == BinderErasure::RcSequence { - self.fail("RC sequence witness used outside an administrative dup/drop bind"); - } - if binder.name() == Sym::new(names::RC_SEQUENCE_BINDER) { - self.fail("reserved RC sequence identity lacks its erasure witness"); - } - self.check_core_type(binder.ty()); - self.locals - .entry(binder.name()) - .or_default() - .push(binder.ty().clone()); - } - - fn unbind(&mut self, name: Sym) { - if let Some(stack) = self.locals.get_mut(&name) { - stack.pop(); - if stack.is_empty() { - self.locals.remove(&name); - } - } - } - - fn local(&self, name: Sym) -> Option { - self.locals - .get(&name) - .and_then(|stack| stack.last()) - .cloned() - } - - fn scoped_binders(&mut self, binders: &[&TypedBinder], f: impl FnOnce(&mut Self)) { - let mut names = BTreeSet::new(); - for binder in binders { - if !names.insert(binder.name()) { - self.fail(format!( - "binder identity {} is duplicated in one binding group", - binder.name() - )); - } - self.bind(binder); - } - f(self); - for binder in binders.iter().rev() { - self.unbind(binder.name()); - } - } - - fn value(&mut self, value: &TypedValue) { - self.check_core_type(value.ty()); - match value.kind() { - TypedValueKind::Var { - name, - instantiation, - } => { - if let Some(local) = self.local(*name) { - self.check_instantiation(instantiation); - let instantiated = if instantiation.is_empty() && value.ty() == &local { - Ok(local.clone()) - } else { - instantiate_value_scheme(&local, instantiation) - }; - match instantiated { - Ok(instantiated) => { - self.expect_type( - value.ty(), - &instantiated, - &format!("local reference `{name}`"), - ); - } - Err(message) => { - self.fail(format!("invalid local {name} instantiation: {message}")); - } - } - if matches!(local, CoreType::ReuseToken(_)) { - self.fail(format!( - "reuse token {name} escapes its dedicated reuse operand" - )); - } - } else if let Some(global) = self.globals.get(name).cloned() { - if let Some(sig) = self.instantiate_fn(&global, instantiation, "global") { - self.expect_type( - value.ty(), - &CoreType::Function(Box::new(sig)), - "global function reference", - ); - } - } else { - self.fail(format!("reference {name} is neither local nor global")); - } - } - TypedValueKind::Int(_) => { - if !matches!(value.ty(), CoreType::Source(Type::Int | Type::Char)) { - self.fail(format!("integer literal has witness {:?}", value.ty())); - } - } - TypedValueKind::I64(_) => self.expect_source(value.ty(), &Type::I64, "i64 literal"), - TypedValueKind::U64(_) => self.expect_source(value.ty(), &Type::U64, "u64 literal"), - TypedValueKind::Float(_) => { - self.expect_source(value.ty(), &Type::Float, "float literal"); - } - TypedValueKind::Bool(_) => self.expect_source(value.ty(), &Type::Bool, "bool literal"), - TypedValueKind::Unit => self.expect_source(value.ty(), &Type::Unit, "unit literal"), - TypedValueKind::Str(_) => self.expect_source(value.ty(), &Type::Str, "string literal"), - TypedValueKind::Reinterpret(inner) => { - self.at("reinterpret", |this| this.value(inner)); - if !representation_preserving(inner.ty(), value.ty()) { - self.fail(format!( - "illegal representation-preserving coercion {:?} to {:?}", - inner.ty(), - value.ty() - )); - } - } - TypedValueKind::LoweredRepr { - value: inner, - proof, - } => { - self.at("lowered-repr", |this| this.value(inner)); - if !P::ALLOW_LOWERED_ABI { - self.fail(format!( - "lowered representation evidence is not legal in {} Core", - P::NAME - )); - } - if !proof.validates(inner.ty(), value.ty()) { - self.fail(format!( - "illegal lowered representation conversion {:?} to {:?}", - inner.ty(), - value.ty() - )); - } - } - TypedValueKind::NewtypeRepr { - constructor, - instantiation, - value: inner, - } => { - self.at("newtype-repr", |this| this.value(inner)); - if !self.env.newtype_constructors.contains(constructor) { - self.fail(format!( - "representation coercion names non-newtype constructor {constructor}" - )); - return; - } - let Some(declared) = self.env.constructors.get(constructor).cloned() else { - self.fail(format!( - "representation coercion names unknown constructor {constructor}" - )); - return; - }; - let Some(instantiated) = self.instantiate_constructor(&declared, instantiation) - else { - return; - }; - let [field] = instantiated.fields.as_slice() else { - self.fail(format!( - "newtype constructor {constructor} has {} fields rather than one", - instantiated.fields.len() - )); - return; - }; - let construction = inner.ty() == field && value.ty() == &instantiated.result; - let projection = inner.ty() == &instantiated.result && value.ty() == field; - if !construction && !projection { - self.fail(format!( - "newtype representation coercion for {constructor} does not connect field {field:?} and result {:?}: inner {:?}, outer {:?}", - instantiated.result, - inner.ty(), - value.ty() - )); - } - } - TypedValueKind::Thunk(body) => { - let token_state = self.token_uses.clone(); - let shell_state = self.reuse_shells.clone(); - let quantifiers = match body.sig().result() { - CoreType::Function(signature) => signature.quantifiers().to_vec(), - _ => Vec::new(), - }; - self.scoped_quantifiers(&quantifiers, |this| { - this.at("thunk", |this| this.comp(body)); - }); - if self.token_uses != token_state { - self.fail("a suspended computation consumes an enclosing reuse token"); - } - if self.reuse_shells != shell_state { - self.fail("a suspended computation frees an enclosing reuse shell"); - } - self.token_uses = token_state; - self.reuse_shells = shell_state; - self.expect_type( - value.ty(), - &CoreType::Thunk(Box::new(body.sig().clone())), - "thunk witness", - ); - } - TypedValueKind::Ctor { - name, - tag, - instantiation, - fields, - } => self.constructor_value(*name, *tag, instantiation, fields, value.ty()), - TypedValueKind::Tuple(fields) => { - self.product_value(fields, value.ty(), ProductKind::Tuple); - } - TypedValueKind::UnboxedTuple(fields) => { - self.product_value(fields, value.ty(), ProductKind::UnboxedTuple); - } - TypedValueKind::UnboxedRecord(fields) => self.record_value(fields, value.ty()), - } - } - - fn constructor_value( - &mut self, - name: Sym, - tag: usize, - instantiation: &[CoreInstantiation], - fields: &[TypedValue], - witness: &CoreType, - ) { - let Some(declared) = self.env.constructors.get(&name).cloned() else { - self.fail(format!("unknown constructor {name}")); - fields.iter().enumerate().for_each(|(index, field)| { - self.at(format!("field[{index}]"), |this| this.value(field)); - }); - return; - }; - let Some(instantiated) = self.instantiate_constructor(&declared, instantiation) else { - return; - }; - if tag != instantiated.tag { - self.fail(format!( - "constructor {name} tag {tag} does not match declared tag {}", - instantiated.tag - )); - } - self.values(fields, &instantiated.fields, "constructor field"); - self.expect_type(witness, &instantiated.result, "constructor result"); - } - - fn product_value(&mut self, fields: &[TypedValue], witness: &CoreType, kind: ProductKind) { - let expected = match witness { - CoreType::Source(Type::Tuple(types)) if kind == ProductKind::Tuple => Some(types), - CoreType::Source(Type::UnboxedTuple(types)) if kind == ProductKind::UnboxedTuple => { - Some(types) - } - CoreType::Source(Type::UnboxedRecord(expected)) - if kind == ProductKind::UnboxedTuple => - { - let types: Vec<_> = expected.iter().map(|(_, ty)| ty.clone()).collect(); - self.values( - fields, - &types.iter().map(lower_value_type).collect::>(), - "product field", - ); - return; - } - _ => None, - }; - let expected = expected.cloned(); - if let Some(expected) = expected { - let expected: Vec<_> = expected.iter().map(lower_value_type).collect(); - self.values(fields, &expected, "product field"); - } else { - self.fail(format!("product shape does not match witness {witness:?}")); - for (index, field) in fields.iter().enumerate() { - self.at(format!("field[{index}]"), |this| this.value(field)); - } - } - } - - fn record_value(&mut self, fields: &[(Sym, TypedValue)], witness: &CoreType) { - let Some(expected) = (match witness { - CoreType::Source(Type::UnboxedRecord(fields)) => Some(fields.clone()), - _ => None, - }) else { - self.fail(format!("unboxed record has non-record witness {witness:?}")); - for (name, value) in fields { - self.at(format!("field[{name}]"), |this| this.value(value)); - } - return; - }; - if fields.len() != expected.len() { - self.fail(format!( - "record field arity {} does not match witness arity {}", - fields.len(), - expected.len() - )); - } - for (index, (name, value)) in fields.iter().enumerate() { - self.at(format!("field[{name}]"), |this| { - this.value(value); - if let Some((expected_name, ty)) = expected.get(index) { - if name != expected_name { - this.fail(format!( - "record field {name} does not match witness field {expected_name}" - )); - } - this.expect_type(value.ty(), &lower_value_type(ty), "record field"); - } - }); - } - } - - fn comp(&mut self, comp: &TypedComp) { - // The verifier recurses per typed node; grow stack segments inside the - // recursion, same discipline as the builder it checks. - stacker::maybe_grow(CORE_MIN_STACK, CORE_GROW_STACK, || { - self.comp_inner(comp); - }); - } - - #[allow(clippy::too_many_lines)] - fn comp_inner(&mut self, comp: &TypedComp) { - self.check_sig(comp.sig()); - match comp.kind() { - TypedCompKind::Return(value) => { - self.value(value); - self.expect_sig( - comp.sig(), - &CompSig::new(value.ty().clone(), EffRow::Empty), - "return", - ); - } - TypedCompKind::Bind(first, binder, rest) => { - self.at("first", |this| this.comp(first)); - self.expect_type(binder.ty(), first.sig().result(), "bind binder"); - if binder.erasure == BinderErasure::RcSequence { - if !P::ALLOW_RC_NODES { - self.fail(format!( - "RC sequence witness is illegal in {} Core", - P::NAME - )); - } - if binder.name() != Sym::new(names::RC_SEQUENCE_BINDER) { - self.fail("RC sequence witness has the wrong reserved identity"); - } - self.expect_type( - binder.ty(), - &CoreType::Source(Type::Unit), - "RC sequence witness", - ); - if !matches!(first.kind(), TypedCompKind::Dup(_) | TypedCompKind::Drop(_)) { - self.fail("RC sequence witness does not sequence a dup or drop"); - } - self.check_core_type(binder.ty()); - self.at("rest", |this| this.comp(rest)); - } else { - self.at("rest", |this| { - this.scoped_binders(&[binder], |this| this.comp(rest)); - }); - } - if let Some(effects) = self.union_rows( - first.sig().effects(), - rest.sig().effects(), - "bind effect union", - ) { - self.expect_subtype_type(comp.sig().result(), rest.sig().result(), "bind"); - if !row_included(&effects, comp.sig().effects()) { - self.fail(format!( - "bind row mismatch: stored {}, does not include derived {}", - comp.sig().effects().show(), - effects.show() - )); - } - } - } - TypedCompKind::Force(value) => { - self.value(value); - match value.ty() { - CoreType::Thunk(sig) => { - self.expect_supertype_sig(comp.sig(), sig, "force"); - } - other => self.fail(format!("force operand is not a thunk: {other:?}")), - } - } - TypedCompKind::Lam(params, body) => { - let token_state = self.token_uses.clone(); - let shell_state = self.reuse_shells.clone(); - let Some(signature) = (match comp.sig().result() { - CoreType::Function(signature) => Some(signature.as_ref()), - other => { - self.fail(format!("lambda result is not a function: {other:?}")); - None - } - }) else { - return; - }; - self.expect_row(comp.sig().effects(), &EffRow::Empty, "lambda"); - if params.len() != signature.params().len() { - self.fail(format!( - "lambda parameter arity {} does not match witness arity {}", - params.len(), - signature.params().len() - )); - } - self.scoped_quantifiers(signature.quantifiers(), |this| { - for (parameter, expected) in params.iter().zip(signature.params()) { - this.expect_type(parameter.ty(), expected, "lambda parameter"); - } - this.at("lambda", |this| { - let binders: Vec<_> = params.iter().collect(); - this.scoped_binders(&binders, |this| this.comp(body)); - }); - this.expect_subtype_sig(body.sig(), signature.body(), "lambda body"); - }); - if self.token_uses != token_state { - self.fail("a function closure consumes an enclosing reuse token"); - } - if self.reuse_shells != shell_state { - self.fail("a function closure frees an enclosing reuse shell"); - } - self.token_uses = token_state; - self.reuse_shells = shell_state; - } - TypedCompKind::App { - callee, - instantiation, - args, - } => { - self.at("callee", |this| this.comp(callee)); - let Some(signature) = (match callee.sig().result() { - CoreType::Function(sig) => { - self.instantiate_fn(sig, instantiation, "computed application") - } - other => { - self.fail(format!("application callee is not a function: {other:?}")); - None - } - }) else { - return; - }; - self.values(args, signature.params(), "application argument"); - if let Some(effects) = self.union_rows( - callee.sig().effects(), - signature.body().effects(), - "application effect union", - ) { - self.expect_sig( - comp.sig(), - &CompSig::new(signature.body().result().clone(), effects), - "application", - ); - } - } - TypedCompKind::If(condition, yes, no) => { - self.value(condition); - self.expect_source(condition.ty(), &Type::Bool, "if condition"); - let token_state = self.token_uses.clone(); - let shell_state = self.reuse_shells.clone(); - self.at("yes", |this| this.comp(yes)); - let yes_tokens = self.token_uses.clone(); - let yes_shells = self.reuse_shells.clone(); - self.token_uses = token_state; - self.reuse_shells = shell_state; - self.at("no", |this| this.comp(no)); - let no_tokens = self.token_uses.clone(); - let no_shells = self.reuse_shells.clone(); - if yes_tokens != no_tokens { - self.fail("if branches consume different reuse-token credits"); - } - self.token_uses = merge_token_states(&yes_tokens, &no_tokens); - self.reuse_shells = merge_shell_states(&yes_shells, &no_shells); - self.expect_type(yes.sig().result(), no.sig().result(), "if branch result"); - if let Some(effects) = - self.union_rows(yes.sig().effects(), no.sig().effects(), "if effect union") - { - self.expect_sig( - comp.sig(), - &CompSig::new(yes.sig().result().clone(), effects), - "if", - ); - } - } - TypedCompKind::Prim(op, lhs, rhs) => self.primitive(comp, *op, lhs, rhs), - TypedCompKind::Call { - callee, - instantiation, - args, - } => { - let Some(declared) = self.globals.get(callee).cloned() else { - self.fail(format!("call targets unknown function {callee}")); - self.values(args, &[], "call argument"); - return; - }; - let Some(signature) = self.instantiate_fn(&declared, instantiation, "call") else { - return; - }; - self.values(args, signature.params(), "call argument"); - self.expect_sig(comp.sig(), signature.body(), "direct call"); - } - TypedCompKind::Io(op, args) => self.io(comp, *op, args), - TypedCompKind::Error(value) => { - self.value(value); - if !matches!(value.ty(), CoreType::Source(Type::Int | Type::Str)) { - self.fail(format!( - "error argument has unsupported witness {:?}", - value.ty() - )); - } - // `Core::Error` is an aborting runtime trap, not the source - // `Exn` effect. Its result and row witnesses are unreachable - // and therefore inherited from the surrounding computation. - } - TypedCompKind::Case(scrutinee, arms) => self.case(comp, scrutinee, arms), - TypedCompKind::FloatBuiltin(op, value) => { - self.value(value); - if let Some(signature) = self.registry_signature(op.signature(), "float builtin") { - self.values( - std::slice::from_ref(value), - signature.params(), - "float argument", - ); - self.expect_sig(comp.sig(), signature.body(), "float builtin"); - } - } - TypedCompKind::Neg(lane, value) => { - self.value(value); - let ty = match lane { - NegLane::Int => Type::Int, - NegLane::I64 => Type::I64, - NegLane::Float => Type::Float, - }; - self.expect_source(value.ty(), &ty, "negation operand"); - self.expect_sig( - comp.sig(), - &CompSig::new(CoreType::Source(ty), EffRow::Empty), - "negation", - ); - } - TypedCompKind::UnboxedProject(value, field) => { - self.value(value); - let Some(field_ty) = (match value.ty() { - CoreType::Source(Type::UnboxedRecord(fields)) => fields - .iter() - .find_map(|(name, ty)| (name == field).then(|| ty.clone())), - _ => None, - }) else { - self.fail(format!( - "field {field} is absent from unboxed-record operand {:?}", - value.ty() - )); - return; - }; - self.expect_sig( - comp.sig(), - &CompSig::new(lower_value_type(&field_ty), EffRow::Empty), - "unboxed projection", - ); - } - TypedCompKind::Do { - operation, - instantiation, - args, - } => self.operation(comp, *operation, instantiation, args), - TypedCompKind::Handle { - body, - return_binder, - return_body, - ops, - } => self.handle( - comp, - body, - return_binder.as_ref(), - return_body.as_deref(), - ops, - ), - TypedCompKind::Mask(effects, body) => { - self.require_effect_node("mask"); - self.at("masked", |this| this.comp(body)); - let residual = subtract_names(body.sig().effects(), effects); - self.expect_sig( - comp.sig(), - &CompSig::new(body.sig().result().clone(), residual), - "mask", - ); - } - TypedCompKind::StrBuiltin { - op, - instantiation, - args, - } => self.builtin(comp, *op, instantiation, args), - TypedCompKind::Dup(value) => { - self.require_rc_node("dup"); - self.value(value); - self.expect_sig( - comp.sig(), - &CompSig::new(CoreType::Source(Type::Unit), EffRow::Empty), - "dup", - ); - } - TypedCompKind::Drop(value) => { - self.require_rc_node("drop"); - self.value(value); - self.expect_sig( - comp.sig(), - &CompSig::new(CoreType::Source(Type::Unit), EffRow::Empty), - "drop", - ); - } - TypedCompKind::WithReuse { token, freed, body } => { - self.require_reuse_node("with-reuse"); - self.value(freed); - self.expect_type( - token.ty(), - &CoreType::ReuseToken(Box::new(freed.ty().clone())), - "reuse-token binder", - ); - let capacity = match self.claim_reuse_shell(freed) { - Ok(capacity) => capacity, - Err(message) => { - self.fail(message); - 0 - } - }; - self.token_uses.entry(token.name()).or_default().push(1); - self.token_capacities - .entry(token.name()) - .or_default() - .push(capacity); - self.at("reuse-body", |this| { - this.scoped_binders(&[token], |this| this.comp(body)); - }); - let credit = pop_scoped(&mut self.token_uses, token.name()).unwrap_or(1); - pop_scoped(&mut self.token_capacities, token.name()); - if credit != 0 { - self.fail(format!( - "reuse token {} is not consumed exactly once on every path", - token.name() - )); - } - self.expect_sig(comp.sig(), body.sig(), "with-reuse"); - } - TypedCompKind::Reuse(token, value) => { - self.require_reuse_node("reuse"); - self.value(value); - let rebuild_arity = match value.kind() { - TypedValueKind::Ctor { fields, .. } | TypedValueKind::Tuple(fields) => { - Some(fields.len()) - } - _ => { - self.fail("reuse rebuild is not a constructor or boxed tuple"); - None - } - }; - let local = self.local(token.name()); - match local { - Some(local) => { - self.expect_type(token.ty(), &local, "reuse token reference"); - if let (Some(arity), Some(capacity)) = ( - rebuild_arity, - self.token_capacities - .get(&token.name()) - .and_then(|capacities| capacities.last()) - .copied(), - ) { - if arity > capacity { - self.fail(format!( - "reuse rebuild arity {arity} exceeds shell capacity {capacity}" - )); - } - } - if let Some(credit) = self - .token_uses - .get_mut(&token.name()) - .and_then(|credits| credits.last_mut()) - { - if *credit == 1 { - *credit = 0; - } else { - self.fail(format!( - "reuse token {} is consumed more than once on one path", - token.name() - )); - } - } else { - self.fail(format!("{} is not an active reuse token", token.name())); - } - } - None => self.fail(format!("reuse token {} is out of scope", token.name())), - } - self.expect_sig( - comp.sig(), - &CompSig::new(value.ty().clone(), EffRow::Empty), - "reuse", - ); - } - TypedCompKind::InitAt(cell, ctor) => { - self.require_init_at_node("init-at"); - self.value(cell); - self.value(ctor); - // The cell is whatever the checked `alloc` operation hands out, - // read from the environment rather than named here: the node is - // a proof that this allocator's cell now holds this - // constructor, so the two must agree by declaration. - match self.env.operation(Sym::new(ALLOC_OP)) { - Some(alloc) => { - let expected = alloc.result().clone(); - self.expect_type(cell.ty(), &expected, "init-at cell"); - } - None => self.fail("init-at without a declared alloc operation"), - } - if !matches!( - ctor.kind(), - TypedValueKind::Ctor { .. } | TypedValueKind::Tuple(_) - ) { - self.fail("init-at payload is not a constructor or boxed tuple"); - } - self.expect_sig( - comp.sig(), - &CompSig::new(ctor.ty().clone(), EffRow::Empty), - "init-at", - ); - } - TypedCompKind::RefNew(value) => { - self.require_ref_node("ref-new"); - self.value(value); - self.expect_sig( - comp.sig(), - &CompSig::new(CoreType::Ref(Box::new(value.ty().clone())), EffRow::Empty), - "ref-new", - ); - } - TypedCompKind::RefGet(value) => { - self.require_ref_node("ref-get"); - self.value(value); - match value.ty() { - CoreType::Ref(inner) => self.expect_sig( - comp.sig(), - &CompSig::new(inner.as_ref().clone(), EffRow::Empty), - "ref-get", - ), - other => self.fail(format!("ref-get operand is not a reference: {other:?}")), - } - } - TypedCompKind::RefSet(cell, value) => { - self.require_ref_node("ref-set"); - self.value(cell); - self.value(value); - match cell.ty() { - CoreType::Ref(inner) => { - self.expect_type(value.ty(), inner, "ref-set value"); - } - other => self.fail(format!("ref-set target is not a reference: {other:?}")), - } - self.expect_sig( - comp.sig(), - &CompSig::new(CoreType::Source(Type::Unit), EffRow::Empty), - "ref-set", - ); - } - } - } - - fn primitive(&mut self, comp: &TypedComp, op: CoreOp, lhs: &TypedValue, rhs: &TypedValue) { - self.value(lhs); - self.value(rhs); - let (operand, result) = match op { - Add | Sub | Mul | Div | Rem => (CoreType::Source(Type::Int), Type::Int), - Addf | Subf | Mulf | Divf => (CoreType::Source(Type::Float), Type::Float), - Eqf | Nef | Ltf | Lef | Gtf | Gef => (CoreType::Source(Type::Float), Type::Bool), - Eq | Ne | Lt | Le | Gt | Ge => { - if lhs.ty() != rhs.ty() - || !matches!( - lhs.ty(), - CoreType::Source(Type::Int | Type::Bool | Type::Char) - ) - { - self.fail(format!( - "integer-lane comparison has operands {:?} and {:?}", - lhs.ty(), - rhs.ty() - )); - } - (lhs.ty().clone(), Type::Bool) - } - }; - self.expect_type(lhs.ty(), &operand, "primitive lhs"); - self.expect_type(rhs.ty(), &operand, "primitive rhs"); - self.expect_sig( - comp.sig(), - &CompSig::new(CoreType::Source(result), EffRow::Empty), - "primitive", - ); - } - - fn io(&mut self, comp: &TypedComp, op: IoOp, args: &[TypedValue]) { - if args.len() != op.arity() { - self.fail(format!( - "I/O argument arity {} does not match expected arity {}", - args.len(), - op.arity() - )); - } - for (index, argument) in args.iter().enumerate() { - self.at(format!("arg[{index}]"), |this| this.value(argument)); - } - if let Some(argument) = args.first() { - match op { - // The raw printer is the lowering of `forall a. (a) -> Unit`; - // concrete Float/String sites use their specialized nodes while - // a rigid polymorphic value legitimately remains arbitrary. - IoOp::PrintF => { - self.expect_source(argument.ty(), &Type::Float, "float print argument"); - } - IoOp::PrintS => { - self.expect_source(argument.ty(), &Type::Str, "string print argument"); - } - IoOp::Srand => { - self.expect_source(argument.ty(), &Type::Int, "random seed argument"); - } - IoOp::Print | IoOp::PrintNl | IoOp::ReadInt | IoOp::ReadLine | IoOp::Rand => {} - } - } - let result = match op { - IoOp::ReadInt | IoOp::Rand => Type::Int, - IoOp::ReadLine => Type::Str, - IoOp::Print | IoOp::PrintF | IoOp::PrintS | IoOp::PrintNl | IoOp::Srand => Type::Unit, - }; - self.expect_sig( - comp.sig(), - &CompSig::new(CoreType::Source(result), EffRow::singleton(IO_EFFECT)), - "I/O operation", - ); - } - - fn case( - &mut self, - comp: &TypedComp, - scrutinee: &TypedValue, - arms: &[(TypedPattern, TypedComp)], - ) { - self.value(scrutinee); - if arms.is_empty() { - self.fail("case has no arms"); - return; - } - let mut effects = EffRow::Empty; - let token_state = self.token_uses.clone(); - let shell_state = self.reuse_shells.clone(); - let mut merged_tokens = None; - let mut merged_shells = None; - for (index, (pattern, body)) in arms.iter().enumerate() { - self.token_uses = token_state.clone(); - self.reuse_shells = shell_state.clone(); - self.at(format!("arm[{index}]"), |this| { - let binders = this.pattern(pattern, scrutinee.ty()); - let shell = this.case_reuse_shell(scrutinee, pattern); - let pushes_shell = shell.as_ref().is_some_and(|(name, shell)| { - !this.reuse_shells.get(name).is_some_and(|shells| { - shells - .last() - .is_some_and(|active| active.binding_depth == shell.binding_depth) - }) - }); - if pushes_shell { - if let Some((name, shell)) = &shell { - this.reuse_shells - .entry(*name) - .or_default() - .push(shell.clone()); - } - } - let refs: Vec<_> = binders.iter().collect(); - this.scoped_binders(&refs, |this| this.comp(body)); - if pushes_shell { - if let Some((name, _)) = shell { - pop_scoped(&mut this.reuse_shells, name); - } - } - this.expect_subtype_type( - body.sig().result(), - comp.sig().result(), - "case arm result", - ); - }); - let arm_tokens = self.token_uses.clone(); - let arm_shells = self.reuse_shells.clone(); - if let Some(previous) = &merged_tokens { - if previous != &arm_tokens { - self.fail("case arms consume different reuse-token credits"); - } - merged_tokens = Some(merge_token_states(previous, &arm_tokens)); - } else { - merged_tokens = Some(arm_tokens); - } - merged_shells = Some(match &merged_shells { - Some(previous) => merge_shell_states(previous, &arm_shells), - None => arm_shells, - }); - if let Some(union) = - self.union_rows(&effects, body.sig().effects(), "case effect union") - { - effects = union; - } - } - self.token_uses = merged_tokens.unwrap_or(token_state); - self.reuse_shells = merged_shells.unwrap_or(shell_state); - self.expect_row(comp.sig().effects(), &effects, "case effects"); - } - - fn case_reuse_shell( - &self, - scrutinee: &TypedValue, - pattern: &TypedPattern, - ) -> Option<(Sym, ReuseShell)> { - // A constructor arm supplies boxed-shell authority even when its - // scrutinee belongs to the lowered effect representation. Tuple syntax - // also covers unboxed products, so only a source boxed tuple qualifies. - let capacity = match (pattern, scrutinee.ty()) { - (TypedPattern::Ctor { fields, .. }, _) - | (TypedPattern::Tuple(fields), CoreType::Source(Type::Tuple(_))) => fields.len(), - _ => return None, - }; - let TypedValueKind::Var { name, .. } = scrutinee.kind() else { - return None; - }; - let binding_depth = self.locals.get(name)?.len(); - Some(( - *name, - ReuseShell { - scrutinee: scrutinee.clone(), - binding_depth, - capacity, - remaining: 1, - }, - )) - } - - fn claim_reuse_shell(&mut self, freed: &TypedValue) -> Result { - let TypedValueKind::Var { name, .. } = freed.kind() else { - return Err("with-reuse does not free the active boxed case scrutinee"); - }; - let binding_depth = self.locals.get(name).map_or(0, Vec::len); - let Some(shell) = self - .reuse_shells - .get_mut(name) - .and_then(|shells| shells.last_mut()) - .filter(|shell| shell.scrutinee == *freed && shell.binding_depth == binding_depth) - else { - return Err("with-reuse does not free the active boxed case scrutinee"); - }; - if shell.remaining == 0 { - return Err("the active boxed case scrutinee is freed more than once on one path"); - } - shell.remaining = 0; - Ok(shell.capacity) - } - - fn pattern(&mut self, pattern: &TypedPattern, scrutinee: &CoreType) -> Vec { - match pattern { - TypedPattern::Wild => Vec::new(), - TypedPattern::Var(binder) => { - self.expect_type(binder.ty(), scrutinee, "pattern binder"); - vec![binder.clone()] - } - TypedPattern::Tuple(fields) => { - let expected = match scrutinee { - CoreType::Source(Type::Tuple(types) | Type::UnboxedTuple(types)) => { - Some(types.clone()) - } - CoreType::Source(Type::UnboxedRecord(fields)) => { - Some(fields.iter().map(|(_, ty)| ty.clone()).collect()) - } - _ => None, - }; - let Some(expected) = expected else { - self.fail(format!( - "tuple pattern has non-product scrutinee {scrutinee:?}" - )); - return fields.iter().filter_map(Clone::clone).collect(); - }; - self.pattern_fields(fields, &expected) - } - TypedPattern::Ctor { - name, - instantiation, - fields, - } => { - let Some(declared) = self.env.constructors.get(name).cloned() else { - self.fail(format!("pattern names unknown constructor {name}")); - return fields.iter().filter_map(Clone::clone).collect(); - }; - let Some(instantiated) = self.instantiate_constructor(&declared, instantiation) - else { - return fields.iter().filter_map(Clone::clone).collect(); - }; - self.expect_type( - scrutinee, - &instantiated.result, - "constructor pattern result", - ); - if fields.len() != instantiated.fields.len() { - self.fail(format!( - "constructor pattern arity {} does not match declared arity {}", - fields.len(), - instantiated.fields.len() - )); - } - let mut binders = Vec::new(); - for (index, binder) in fields.iter().enumerate() { - if let Some(binder) = binder { - if let Some(expected) = instantiated.fields.get(index) { - self.expect_type(binder.ty(), expected, "constructor pattern field"); - } - binders.push(binder.clone()); - } - } - binders - } - } - } - - fn pattern_fields( - &mut self, - fields: &[Option], - expected: &[Type], - ) -> Vec { - if fields.len() != expected.len() { - self.fail(format!( - "tuple pattern arity {} does not match scrutinee arity {}", - fields.len(), - expected.len() - )); - } - fields - .iter() - .enumerate() - .filter_map(|(index, binder)| { - binder.as_ref().map(|binder| { - if let Some(expected) = expected.get(index) { - self.expect_type( - binder.ty(), - &lower_value_type(expected), - "tuple pattern field", - ); - } - binder.clone() - }) - }) - .collect() - } - - fn operation( - &mut self, - comp: &TypedComp, - name: Sym, - instantiation: &[CoreInstantiation], - args: &[TypedValue], - ) { - self.require_effect_node("operation"); - let Some(declared) = self.env.operations.get(&name).cloned() else { - self.fail(format!("unknown effect operation {name}")); - return; - }; - let Some(instantiated) = self.instantiate_operation(&declared, instantiation) else { - return; - }; - self.values(args, &instantiated.params, "operation argument"); - self.expect_sig( - comp.sig(), - &CompSig::new( - instantiated.result, - EffRow::canonical([instantiated.effect], EffRow::Empty), - ), - "effect operation", - ); - } - - fn handle( - &mut self, - comp: &TypedComp, - body: &TypedComp, - return_binder: Option<&TypedBinder>, - return_body: Option<&TypedComp>, - handler: &TypedHandler, - ) { - self.require_effect_node("handler"); - self.at("handled", |this| this.comp(body)); - let arms = handler.arms(); - if return_binder.is_some() != return_body.is_some() { - self.fail("handler return binder and return body must appear together"); - } - - let mut clause_effects = - if let (Some(binder), Some(return_body)) = (return_binder, return_body) { - self.expect_type(binder.ty(), body.sig().result(), "handler return binder"); - self.at("return", |this| { - this.scoped_binders(&[binder], |this| this.comp(return_body)); - }); - self.expect_subtype_type( - return_body.sig().result(), - comp.sig().result(), - "handler return result", - ); - return_body.sig().effects().clone() - } else { - self.expect_type( - body.sig().result(), - comp.sig().result(), - "handler identity return", - ); - EffRow::Empty - }; - - let mut instantiated_arms = BTreeMap::new(); - for (index, arm) in arms.iter().enumerate() { - self.at(format!("op[{}]", arm.name()), |this| { - let Some(declared) = this.env.operations.get(&arm.name()).cloned() else { - this.fail(format!("handler names unknown operation {}", arm.name())); - return; - }; - let Some(operation) = this.instantiate_operation(&declared, arm.instantiation()) - else { - return; - }; - this.check_handler_arm(arm, &operation, comp.sig()); - instantiated_arms.insert(arm.name(), operation.effect.clone()); - }); - if let Some(union) = self.union_rows( - &clause_effects, - arm.body().sig().effects(), - "handler clause effect union", - ) { - clause_effects = union; - } - let _ = index; - } - - let expected_forwarding = self.residual_forwarding(&instantiated_arms); - let stored_forwarding: Vec<_> = handler - .forwarded() - .iter() - .map(|forward| (forward.operation(), forward.effect().clone())) - .collect(); - if stored_forwarding != expected_forwarding { - self.fail(format!( - "handler residual-forwarding witness mismatch: derived {expected_forwarding:?}, stored {stored_forwarding:?}" - )); - } - - let discharged = self.exhaustively_handled_labels(body.sig().effects(), &instantiated_arms); - let residual = subtract_labels(body.sig().effects(), &discharged); - if let Some(effects) = self.union_rows(&residual, &clause_effects, "handler effect union") { - if !row_included(&effects, comp.sig().effects()) { - self.fail(format!( - "handler residual effects row mismatch: derived {}, stored upper bound {}", - effects.show(), - comp.sig().effects().show() - )); - } - } - } - - fn residual_forwarding(&self, arms: &BTreeMap) -> Vec<(Sym, Label)> { - let effects: BTreeMap = arms - .values() - .map(|label| (label.name, label.clone())) - .collect(); - self.env - .operations - .iter() - .filter_map(|(operation, declared)| { - effects - .get(&declared.effect.name) - .filter(|_| !arms.contains_key(operation)) - .cloned() - .map(|effect| (*operation, effect)) - }) - .collect() - } - - fn check_handler_arm( - &mut self, - arm: &TypedHandleOp, - operation: &MonoOperation, - outer: &CompSig, - ) { - if arm.params().len() != operation.params.len() { - self.fail(format!( - "operation arm arity {} does not match declared arity {}", - arm.params().len(), - operation.params.len() - )); - } - for (binder, expected) in arm.params().iter().zip(&operation.params) { - self.expect_type(binder.ty(), expected, "operation arm parameter"); - } - let resume = CoreType::Thunk(Box::new(CompSig::new( - CoreType::Function(Box::new(CoreFnSig::new( - Vec::new(), - vec![operation.result.clone()], - outer.clone(), - ))), - EffRow::Empty, - ))); - self.expect_type(arm.resume().ty(), &resume, "operation resumption"); - let mut binders: Vec<_> = arm.params().iter().collect(); - binders.push(arm.resume()); - self.scoped_binders(&binders, |this| this.comp(arm.body())); - self.expect_subtype_type( - arm.body().sig().result(), - outer.result(), - "operation arm result", - ); - } - - fn exhaustively_handled_labels( - &self, - body: &EffRow, - arms: &BTreeMap, - ) -> BTreeSet

(body: &TypedComp) -> TypedCore

{ - TypedCore::new(vec![TypedCoreFn::new( - Sym::new("main"), - Vec::new(), - body.clone(), - CoreFnSig::new(Vec::new(), Vec::new(), body.sig().clone()), - 0, - )]) - } - - fn local(name: &str, ty: Type) -> TypedValue { - value( - ty, - TypedValueKind::Var { - name: Sym::new(name), - instantiation: Vec::new(), - }, - ) - } - - #[test] - fn accepts_a_closed_well_typed_program() { - let body = return_value(value(Type::Int, TypedValueKind::Int(42))); - assert_eq!( - verify(&function::(&body), &VerifyEnv::new()), - Ok(()) - ); - } - - #[test] - fn case_arms_may_widen_latent_effect_rows_but_not_narrow_them() { - let row_name = Sym::new("e"); - let closure = |effects| { - CoreType::Thunk(Box::new(pure(CoreType::Function(Box::new( - CoreFnSig::new( - Vec::new(), - vec![source(Type::U64)], - CompSig::new(source(Type::Int), effects), - ), - ))))) - }; - let pure_closure = closure(EffRow::Empty); - let open_closure = closure(EffRow::Var(row_name)); - let program = |arm_ty: CoreType, result_ty: CoreType| { - let choice = TypedBinder::new(Sym::new("choice"), source(Type::Bool)); - let selected = TypedBinder::new(Sym::new("selected"), arm_ty.clone()); - let arm_value = TypedValue::new( - arm_ty.clone(), - TypedValueKind::Var { - name: selected.name(), - instantiation: Vec::new(), - }, - ); - let body = TypedComp::new( - pure(result_ty.clone()), - TypedCompKind::Case( - TypedValue::new( - choice.ty().clone(), - TypedValueKind::Var { - name: choice.name(), - instantiation: Vec::new(), - }, - ), - vec![(TypedPattern::Wild, return_value(arm_value))], - ), - ); - TypedCore::::new(vec![TypedCoreFn::new( - Sym::new("main"), - vec![choice, selected], - body, - CoreFnSig::new( - vec![CoreQuantifier::Row(row_name)], - vec![source(Type::Bool), arm_ty], - pure(result_ty), - ), - 0, - )]) - }; - - assert_eq!( - verify( - &program(pure_closure.clone(), open_closure.clone()), - &VerifyEnv::new() - ), - Ok(()) - ); - let errors = verify(&program(open_closure, pure_closure), &VerifyEnv::new()).unwrap_err(); - assert!(errors.iter().any(|error| { - error.path().ends_with("body.arm[0]") - && error.message().contains("expected a subtype of Thunk") - })); - } - - #[test] - fn rc_sequence_witness_is_confined_to_administrative_owned_binds() { - let unit = source(Type::Unit); - let unit_value = || value(Type::Unit, TypedValueKind::Unit); - let sequence = |binder: TypedBinder, rest: TypedComp| { - TypedComp::new( - rest.sig().clone(), - TypedCompKind::Bind( - Box::new(TypedComp::new( - pure(unit.clone()), - TypedCompKind::Dup(unit_value()), - )), - binder, - Box::new(rest), - ), - ) - }; - - let valid = sequence(TypedBinder::rc_sequence(), return_value(unit_value())); - let valid_core = function::(&valid); - assert_eq!(verify(&valid_core, &VerifyEnv::new()), Ok(())); - let Comp::Bind(_, erased_binder, _) = &valid_core.erase().fns[0].body else { - panic!("expected erased administrative bind"); - }; - assert_eq!(erased_binder.as_str(), "_"); - - let too_early = sequence(TypedBinder::rc_sequence(), return_value(unit_value())); - let errors = verify(&function::(&too_early), &VerifyEnv::new()).unwrap_err(); - assert!(errors - .iter() - .any(|error| error.message().contains("illegal in effect-lowered Core"))); - - let ordinary_first = TypedComp::new( - pure(unit.clone()), - TypedCompKind::Bind( - Box::new(return_value(unit_value())), - TypedBinder::rc_sequence(), - Box::new(return_value(unit_value())), - ), - ); - let errors = verify(&function::(&ordinary_first), &VerifyEnv::new()).unwrap_err(); - assert!(errors - .iter() - .any(|error| error.message().contains("does not sequence a dup or drop"))); - - let missing_witness = sequence( - TypedBinder::new(Sym::new(names::RC_SEQUENCE_BINDER), unit.clone()), - return_value(unit_value()), - ); - let errors = verify(&function::(&missing_witness), &VerifyEnv::new()).unwrap_err(); - assert!(errors - .iter() - .any(|error| error.message().contains("lacks its erasure witness"))); - - let wrong_name = sequence( - TypedBinder { - name: Sym::new("wrong"), - ty: unit.clone(), - erasure: BinderErasure::RcSequence, - }, - return_value(unit_value()), - ); - let errors = verify(&function::(&wrong_name), &VerifyEnv::new()).unwrap_err(); - assert!(errors - .iter() - .any(|error| error.message().contains("wrong reserved identity"))); - - let wrong_type = sequence( - TypedBinder { - name: Sym::new(names::RC_SEQUENCE_BINDER), - ty: source(Type::Int), - erasure: BinderErasure::RcSequence, - }, - return_value(unit_value()), - ); - let errors = verify(&function::(&wrong_type), &VerifyEnv::new()).unwrap_err(); - assert!(errors - .iter() - .any(|error| error.message().contains("RC sequence witness"))); - - let lambda_body = return_value(unit_value()); - let lambda = TypedComp::new( - pure(CoreType::Function(Box::new(CoreFnSig::new( - Vec::new(), - vec![unit.clone()], - lambda_body.sig().clone(), - )))), - TypedCompKind::Lam(vec![TypedBinder::rc_sequence()], Box::new(lambda_body)), - ); - let errors = verify(&function::(&lambda), &VerifyEnv::new()).unwrap_err(); - assert!(errors.iter().any(|error| error - .message() - .contains("outside an administrative dup/drop bind"))); - - let parameter_body = return_value(unit_value()); - let parameter_core = TypedCore::::new(vec![TypedCoreFn::new( - Sym::new("parameter"), - vec![TypedBinder::rc_sequence()], - parameter_body.clone(), - CoreFnSig::new(Vec::new(), vec![unit.clone()], parameter_body.sig().clone()), - 0, - )]); - let errors = verify(¶meter_core, &VerifyEnv::new()).unwrap_err(); - assert!(errors.iter().any(|error| error - .message() - .contains("outside an administrative dup/drop bind"))); - - let dangling = TypedValue::new( - unit.clone(), - TypedValueKind::Var { - name: Sym::new(names::RC_SEQUENCE_BINDER), - instantiation: Vec::new(), - }, - ); - let referenced = sequence(TypedBinder::rc_sequence(), return_value(dangling)); - let errors = verify(&function::(&referenced), &VerifyEnv::new()).unwrap_err(); - assert!(errors - .iter() - .any(|error| error.message().contains("neither local nor global"))); - } - - #[test] - fn rejects_a_drifting_literal_witness() { - let body = return_value(value(Type::Bool, TypedValueKind::Int(42))); - let errors = verify(&function::(&body), &VerifyEnv::new()).unwrap_err(); - assert!(errors - .iter() - .any(|error| error.message().contains("integer literal"))); - } - - #[test] - fn rejects_effect_row_drift() { - let body = TypedComp::new( - pure(source(Type::Int)), - TypedCompKind::Io(IoOp::ReadInt, Vec::new()), - ); - let errors = verify(&function::(&body), &VerifyEnv::new()).unwrap_err(); - assert!(errors - .iter() - .any(|error| error.message().contains("I/O operation row mismatch"))); - } - - #[test] - fn accepts_error_with_arbitrary_well_formed_inherited_witnesses() { - let inherited = fatal_error(CompSig::new( - source(Type::Bool), - EffRow::singleton(prism_syntax::names::IO_EFFECT), - )); - assert_eq!( - verify(&function::(&inherited), &VerifyEnv::new()), - Ok(()) - ); - } - - #[test] - fn rejects_error_with_an_unbound_result_type_witness() { - let unbound_result = Sym::new("unbound_error_result"); - let bad_result = fatal_error(pure(source(Type::Var(unbound_result)))); - let bad_result_core = TypedCore::::new(vec![TypedCoreFn::new( - Sym::new("bad_result"), - Vec::new(), - bad_result, - CoreFnSig::new(Vec::new(), Vec::new(), pure(source(Type::Unit))), - 0, - )]); - let errors = verify(&bad_result_core, &VerifyEnv::new()).unwrap_err(); - assert!(errors.iter().any(|error| { - error - .message() - .contains(&format!("unbound rigid type variable {unbound_result}")) - })); - } - - #[test] - fn rejects_error_with_an_unbound_effect_row_witness() { - let unbound_effects = Sym::new("unbound_error_effects"); - let bad_effects = fatal_error(CompSig::new( - source(Type::Unit), - EffRow::Var(unbound_effects), - )); - let bad_effects_core = TypedCore::::new(vec![TypedCoreFn::new( - Sym::new("bad_effects"), - Vec::new(), - bad_effects, - CoreFnSig::new(Vec::new(), Vec::new(), pure(source(Type::Unit))), - 0, - )]); - let errors = verify(&bad_effects_core, &VerifyEnv::new()).unwrap_err(); - assert!(errors.iter().any(|error| { - error.message().contains(&format!( - "unbound rigid effect-row variable {unbound_effects}" - )) - })); - } - - #[test] - fn rejects_a_bind_that_hides_a_child_effect() { - let unit = source(Type::Unit); - let io = TypedComp::new( - CompSig::new( - unit.clone(), - EffRow::singleton(prism_syntax::names::IO_EFFECT), - ), - TypedCompKind::Io(IoOp::PrintNl, Vec::new()), - ); - let rest = return_value(value(Type::Unit, TypedValueKind::Unit)); - let hidden = TypedComp::new( - pure(unit.clone()), - TypedCompKind::Bind( - Box::new(io), - TypedBinder::new(Sym::new("ignored"), unit), - Box::new(rest), - ), - ); - let errors = verify(&function::(&hidden), &VerifyEnv::new()).unwrap_err(); - assert!(errors - .iter() - .any(|error| error.message().contains("does not include derived {IO}"))); - } - - #[test] - fn rejects_unknown_references_and_duplicate_binders() { - let binder = TypedBinder::new(Sym::new("x"), source(Type::Int)); - let unknown = value( - Type::Int, - TypedValueKind::Var { - name: Sym::new("missing"), - instantiation: Vec::new(), - }, - ); - let lambda_body = return_value(unknown); - let lambda_sig = CoreFnSig::new( - Vec::new(), - vec![source(Type::Int), source(Type::Int)], - lambda_body.sig().clone(), - ); - let body = TypedComp::new( - pure(CoreType::Function(Box::new(lambda_sig))), - TypedCompKind::Lam(vec![binder.clone(), binder], Box::new(lambda_body)), - ); - let errors = verify(&function::(&body), &VerifyEnv::new()).unwrap_err(); - assert!(errors - .iter() - .any(|error| error.message().contains("duplicated in one binding group"))); - assert!(errors - .iter() - .any(|error| error.message().contains("neither local nor global"))); - } - - #[test] - fn checks_explicit_polymorphic_call_instantiation() { - let type_parameter = Sym::new("a"); - let parameter = TypedBinder::new(Sym::new("x"), source(Type::Var(type_parameter))); - let id_body = return_value(local("x", Type::Var(type_parameter))); - let id = TypedCoreFn::new( - Sym::new("id"), - vec![parameter], - id_body.clone(), - CoreFnSig::new( - vec![CoreQuantifier::Type(type_parameter)], - vec![source(Type::Var(type_parameter))], - id_body.sig().clone(), - ), - 0, - ); - let call = TypedComp::new( - pure(source(Type::Int)), - TypedCompKind::Call { - callee: Sym::new("id"), - instantiation: vec![CoreInstantiation::Type(Type::Int)], - args: vec![value(Type::Int, TypedValueKind::Int(1))], - }, - ); - let main = TypedCoreFn::new( - Sym::new("main"), - Vec::new(), - call.clone(), - CoreFnSig::new(Vec::new(), Vec::new(), call.sig().clone()), - 0, - ); - let core = TypedCore::::new(vec![id.clone(), main]); - assert_eq!(verify(&core, &VerifyEnv::new()), Ok(())); - - let bad_call = TypedComp::new( - pure(source(Type::Int)), - TypedCompKind::Call { - callee: Sym::new("id"), - instantiation: vec![CoreInstantiation::Row(EffRow::Empty)], - args: vec![value(Type::Int, TypedValueKind::Int(1))], - }, - ); - let bad_main = TypedCoreFn::new( - Sym::new("main"), - Vec::new(), - bad_call.clone(), - CoreFnSig::new(Vec::new(), Vec::new(), bad_call.sig().clone()), - 0, - ); - let errors = verify( - &TypedCore::::new(vec![id, bad_main]), - &VerifyEnv::new(), - ) - .unwrap_err(); - assert!(errors - .iter() - .any(|error| error.message().contains("wrong kind"))); - } - - #[test] - fn rejects_constructor_tag_and_field_drift() { - let parameter = Sym::new("a"); - let mut env = VerifyEnv::new(); - env.insert_constructor( - Sym::new("Some"), - ConstructorSig::new( - vec![CoreQuantifier::Type(parameter)], - 7, - vec![source(Type::Var(parameter))], - source(Type::Con(Sym::new("Option"), vec![Type::Var(parameter)])), - ), - ); - let option_int = Type::Con(Sym::new("Option"), vec![Type::Int]); - let constructor = TypedValue::new( - source(option_int), - TypedValueKind::Ctor { - name: Sym::new("Some"), - tag: 8, - instantiation: vec![CoreInstantiation::Type(Type::Int)], - fields: vec![value(Type::Bool, TypedValueKind::Bool(true))], - }, - ); - let errors = verify(&function::(&return_value(constructor)), &env).unwrap_err(); - assert!(errors - .iter() - .any(|error| error.message().contains("declared tag 7"))); - assert!(errors - .iter() - .any(|error| error.message().contains("constructor field type mismatch"))); - } - - #[test] - fn checks_handler_residual_rows_and_resumption_type() { - let operation_name = Sym::new("get"); - let effect_name = Sym::new("State"); - let mut env = VerifyEnv::new(); - env.insert_operation( - operation_name, - OperationSig::new( - Vec::new(), - Vec::new(), - source(Type::Int), - Label::bare(effect_name), - ), - ); - let handled = TypedComp::new( - CompSig::new(source(Type::Int), EffRow::singleton(effect_name)), - TypedCompKind::Do { - operation: operation_name, - instantiation: Vec::new(), - args: Vec::new(), - }, - ); - let outer = pure(source(Type::Int)); - let resume = TypedBinder::new( - Sym::new("resume"), - CoreType::Thunk(Box::new(pure(CoreType::Function(Box::new( - CoreFnSig::new(Vec::new(), vec![source(Type::Int)], outer.clone()), - ))))), - ); - let arm = TypedHandleOp::new( - operation_name, - Vec::new(), - Vec::new(), - resume, - return_value(value(Type::Int, TypedValueKind::Int(0))), - ); - let clauses = TypedHandler::new(vec![arm]).unwrap(); - let body = TypedComp::new( - outer, - TypedCompKind::Handle { - body: Box::new(handled.clone()), - return_binder: None, - return_body: None, - ops: clauses, - }, - ); - assert_eq!(verify(&function::(&body), &env), Ok(())); - - env.insert_operation( - Sym::new("put"), - OperationSig::new( - Vec::new(), - vec![source(Type::Int)], - source(Type::Unit), - Label::bare(effect_name), - ), - ); - let residual = CompSig::new(source(Type::Int), EffRow::singleton(effect_name)); - let resume = TypedBinder::new( - Sym::new("resume_partial"), - CoreType::Thunk(Box::new(pure(CoreType::Function(Box::new( - CoreFnSig::new(Vec::new(), vec![source(Type::Int)], residual.clone()), - ))))), - ); - let arm = TypedHandleOp::new( - operation_name, - Vec::new(), - Vec::new(), - resume, - return_value(value(Type::Int, TypedValueKind::Int(0))), - ); - let partial = - TypedComp::new( - residual, - TypedCompKind::Handle { - body: Box::new(handled), - return_binder: None, - return_body: None, - ops: TypedHandler::new(vec![arm]).unwrap().with_forwarded(vec![ - TypedForward::new(Sym::new("put"), Label::bare(effect_name)), - ]), - }, - ); - assert_eq!(verify(&function::(&partial), &env), Ok(())); - } - - #[test] - fn rejects_nodes_outside_their_phase() { - let integer = value(Type::Int, TypedValueKind::Int(1)); - let ref_new = TypedComp::new( - pure(CoreType::Ref(Box::new(source(Type::Int)))), - TypedCompKind::RefNew(integer), - ); - let elaborated_errors = - verify(&function::(&ref_new), &VerifyEnv::new()).unwrap_err(); - assert!(elaborated_errors - .iter() - .any(|error| error.message().contains("illegal in elaborated Core"))); - - let returned = return_value(value(Type::Int, TypedValueKind::Int(1))); - let mask = TypedComp::new( - returned.sig().clone(), - TypedCompKind::Mask(Vec::new(), Box::new(returned)), - ); - let lowered_errors = - verify(&function::(&mask), &VerifyEnv::new()).unwrap_err(); - assert!(lowered_errors - .iter() - .any(|error| error.message().contains("illegal in effect-lowered Core"))); - } - - // `init_at` is the proof that a cell an allocator handed out now holds a - // constructor. Each premise of that claim is independent, so each is pinned: - // the phase it may appear in, that the cell is the declared `alloc` result, - // that the payload is something a cell can hold, and that the node's own - // witness is the constructor's. - #[test] - fn init_at_checks_every_premise_of_its_claim() { - let boxed = Type::Con(Sym::new("Boxed"), Vec::new()); - let cell = Type::Con(Sym::new("Arena.Cell"), Vec::new()); - let mut env = VerifyEnv::new(); - env.insert_constructor( - Sym::new("Boxed"), - ConstructorSig::new(Vec::new(), 0, Vec::new(), source(boxed.clone())), - ); - env.insert_operation( - Sym::new(ALLOC_OP), - OperationSig::new( - Vec::new(), - vec![source(Type::Int)], - source(cell.clone()), - Label::bare(ALLOC_EFFECT), - ), - ); - let ctor = || { - TypedValue::new( - source(boxed.clone()), - TypedValueKind::Ctor { - name: Sym::new("Boxed"), - tag: 0, - instantiation: Vec::new(), - fields: Vec::new(), - }, - ) - }; - let init_at = |cell_value: TypedValue, payload: TypedValue, result: Type| { - TypedComp::new( - pure(source(result)), - TypedCompKind::InitAt(cell_value, payload), - ) - }; - let good = || init_at(local("c", cell.clone()), ctor(), boxed.clone()); - let in_scope = |body: &TypedComp| { - TypedComp::new( - CompSig::new(body.sig().result().clone(), EffRow::singleton(ALLOC_EFFECT)), - TypedCompKind::Bind( - Box::new(TypedComp::new( - CompSig::new(source(cell.clone()), EffRow::singleton(ALLOC_EFFECT)), - TypedCompKind::Do { - operation: Sym::new(ALLOC_OP), - instantiation: Vec::new(), - args: vec![value(Type::Int, TypedValueKind::Int(0))], - }, - )), - TypedBinder::new(Sym::new("c"), source(cell.clone())), - Box::new(body.clone()), - ), - ) - }; - - // Legal once an arena has been prepared, and never before. - assert_eq!( - verify(&function::(&in_scope(&good())), &env), - Ok(()) - ); - let too_early = verify(&function::(&in_scope(&good())), &env).unwrap_err(); - assert!(too_early - .iter() - .any(|error| error.message().contains("illegal in elaborated Core"))); - - // The cell must be what this allocator hands out. - let wrong_cell = in_scope(&init_at(local("c", Type::Int), ctor(), boxed.clone())); - let errors = verify(&function::(&wrong_cell), &env).unwrap_err(); - assert!(errors - .iter() - .any(|error| error.message().contains("init-at cell"))); - - // A cell holds a constructor, not an arbitrary value. - let not_a_ctor = in_scope(&init_at( - local("c", cell.clone()), - value(Type::Int, TypedValueKind::Int(1)), - Type::Int, - )); - let errors = verify(&function::(¬_a_ctor), &env).unwrap_err(); - assert!(errors.iter().any(|error| error - .message() - .contains("init-at payload is not a constructor"))); - - // The node returns the constructor it wrote, purely. - let drifting = in_scope(&init_at(local("c", cell.clone()), ctor(), Type::Int)); - let errors = verify(&function::(&drifting), &env).unwrap_err(); - assert!(errors - .iter() - .any(|error| error.message().contains("init-at type mismatch"))); - } - - #[test] - fn reference_count_operations_return_unit() { - let dup = TypedComp::new( - pure(source(Type::Unit)), - TypedCompKind::Dup(value(Type::Int, TypedValueKind::Int(1))), - ); - assert_eq!(verify(&function::(&dup), &VerifyEnv::new()), Ok(())); - - let drifting = TypedComp::new( - pure(source(Type::Int)), - TypedCompKind::Dup(value(Type::Int, TypedValueKind::Int(1))), - ); - let errors = verify(&function::(&drifting), &VerifyEnv::new()).unwrap_err(); - assert!(errors - .iter() - .any(|error| error.message().contains("dup type mismatch"))); - } - - #[test] - fn row_instantiation_recanonicalizes_duplicate_labels() { - let row_parameter = Sym::new("e"); - let signature = CoreFnSig::new( - vec![CoreQuantifier::Row(row_parameter)], - Vec::new(), - CompSig::new( - source(Type::Unit), - EffRow::Extend(Label::bare(IO_EFFECT), Box::new(EffRow::Var(row_parameter))), - ), - ); - let instantiated = instantiate_fn( - &signature, - &[CoreInstantiation::Row(EffRow::singleton(IO_EFFECT))], - ) - .unwrap(); - assert_eq!(instantiated.body().effects(), &EffRow::singleton(IO_EFFECT)); - } - - #[test] - fn scheme_instantiation_is_simultaneous() { - let first = Sym::new("a"); - let second = Sym::new("b"); - let signature = CoreFnSig::new( - vec![CoreQuantifier::Type(first), CoreQuantifier::Type(second)], - vec![source(Type::Var(first))], - pure(source(Type::Var(second))), - ); - let instantiated = instantiate_fn( - &signature, - &[ - CoreInstantiation::Type(Type::Var(second)), - CoreInstantiation::Type(Type::Int), - ], - ) - .unwrap(); - assert_eq!(instantiated.params(), &[source(Type::Var(second))]); - assert_eq!(instantiated.body().result(), &source(Type::Int)); - } - - #[test] - fn canonical_builtin_signatures_are_checked_without_inference() { - let sqrt = TypedComp::new( - pure(source(Type::Float)), - TypedCompKind::FloatBuiltin( - FloatOp::Sqrt, - value(Type::Float, TypedValueKind::Float(4.0)), - ), - ); - assert_eq!( - verify(&function::(&sqrt), &VerifyEnv::new()), - Ok(()) - ); - - let array_int = Type::Con(Sym::new("Array"), vec![Type::Int]); - let get = TypedComp::new( - pure(source(Type::Int)), - TypedCompKind::StrBuiltin { - op: Builtin::ArrayGet, - instantiation: vec![CoreInstantiation::Type(Type::Int)], - args: vec![ - local("array", array_int.clone()), - value(Type::Int, TypedValueKind::Int(0)), - ], - }, - ); - let array = TypedBinder::new(Sym::new("array"), source(array_int.clone())); - let core = TypedCore::::new(vec![TypedCoreFn::new( - Sym::new("main"), - vec![array], - get.clone(), - CoreFnSig::new(Vec::new(), vec![source(array_int)], get.sig().clone()), - 0, - )]); - assert_eq!(verify(&core, &VerifyEnv::new()), Ok(())); - } - - #[test] - fn reuse_credit_must_be_consumed_once_on_every_branch() { - let boxed = Type::Con(Sym::new("Boxed"), Vec::new()); - let mut env = VerifyEnv::new(); - env.insert_constructor( - Sym::new("Boxed"), - ConstructorSig::new(Vec::new(), 0, Vec::new(), source(boxed.clone())), - ); - let old = TypedBinder::new(Sym::new("old"), source(boxed.clone())); - let token = TypedBinder::new( - Sym::new("token"), - CoreType::ReuseToken(Box::new(source(boxed.clone()))), - ); - let rebuild = || { - TypedValue::new( - source(boxed.clone()), - TypedValueKind::Ctor { - name: Sym::new("Boxed"), - tag: 0, - instantiation: Vec::new(), - fields: Vec::new(), - }, - ) - }; - let reuse = || { - TypedComp::new( - pure(source(boxed.clone())), - TypedCompKind::Reuse(token.clone(), rebuild()), - ) - }; - let branches = TypedComp::new( - pure(source(boxed.clone())), - TypedCompKind::If( - value(Type::Bool, TypedValueKind::Bool(true)), - Box::new(reuse()), - Box::new(reuse()), - ), - ); - let body = TypedComp::new( - branches.sig().clone(), - TypedCompKind::WithReuse { - token: token.clone(), - freed: local("old", boxed.clone()), - body: Box::new(branches), - }, - ); - let make_program = |body: TypedComp| { - let body = TypedComp::new( - body.sig().clone(), - TypedCompKind::Case( - local("old", boxed.clone()), - vec![( - TypedPattern::Ctor { - name: Sym::new("Boxed"), - instantiation: Vec::new(), - fields: Vec::new(), - }, - body, - )], - ), - ); - TypedCore::::new(vec![TypedCoreFn::new( - Sym::new("main"), - vec![old.clone()], - body.clone(), - CoreFnSig::new(Vec::new(), vec![source(boxed.clone())], body.sig().clone()), - 0, - )]) - }; - assert_eq!(verify(&make_program(body), &env), Ok(())); - - let unbalanced = TypedComp::new( - pure(source(boxed.clone())), - TypedCompKind::If( - value(Type::Bool, TypedValueKind::Bool(true)), - Box::new(reuse()), - Box::new(return_value(rebuild())), - ), - ); - let unbalanced = TypedComp::new( - unbalanced.sig().clone(), - TypedCompKind::WithReuse { - token, - freed: local("old", boxed.clone()), - body: Box::new(unbalanced), - }, - ); - let errors = verify(&make_program(unbalanced), &env).unwrap_err(); - assert!(errors.iter().any(|error| error - .message() - .contains("branches consume different reuse-token credits"))); - } - - #[test] - fn polymorphic_function_subtyping_is_alpha_invariant() { - let a = Sym::new("a"); - let renamed = Sym::new("a$typedq0"); - let function = |name| { - CoreType::Function(Box::new(CoreFnSig::new( - vec![CoreQuantifier::Type(name)], - vec![source(Type::Var(name))], - pure(source(Type::Var(name))), - ))) - }; - assert!(core_subtype(&function(a), &function(renamed))); - assert!(core_subtype(&function(renamed), &function(a))); - } - - #[test] - fn alpha_alignment_does_not_capture_a_free_type_variable() { - let bound = Sym::new("bound"); - let other_bound = Sym::new("other_bound"); - let free = Sym::new("free"); - let actual = CoreType::Function(Box::new(CoreFnSig::new( - vec![CoreQuantifier::Type(bound)], - vec![source(Type::Var(bound)), source(Type::Var(free))], - pure(source(Type::Var(bound))), - ))); - let expected = CoreType::Function(Box::new(CoreFnSig::new( - vec![CoreQuantifier::Type(other_bound)], - vec![ - source(Type::Var(other_bound)), - source(Type::Var(other_bound)), - ], - pure(source(Type::Var(other_bound))), - ))); - assert!(!core_subtype(&actual, &expected)); - } -} diff --git a/crates/prism-core/src/core/typed/verify/check/expect.rs b/crates/prism-core/src/core/typed/verify/check/expect.rs new file mode 100644 index 00000000..c823f25a --- /dev/null +++ b/crates/prism-core/src/core/typed/verify/check/expect.rs @@ -0,0 +1,235 @@ +//! Expected-type, signature, instantiation, and row-join checks. + +use crate::types::ty::EffRow; +use crate::types::Type; + +use super::super::super::violation::{ + InstantiationSubject, RowRelation, Site, TypeRelation, Violation, +}; +use super::super::super::{CompSig, CoreFnSig, CoreInstantiation, CoreType}; +use super::super::compat::{core_subtype, row_included, union_rows as canonical_union_rows}; +use super::super::env::{ConstructorSig, OperationSig}; +use super::super::instantiate::{ + instantiate_constructor as instantiate_constructor_sig, instantiate_fn as instantiate_fn_sig, + instantiate_operation as instantiate_operation_sig, scheme_to_fn_sig, MonoConstructor, + MonoOperation, +}; +use super::phase::TypedCorePhase; +use super::Checker; + +impl Checker<'_, P> { + pub(super) fn registry_signature( + &mut self, + text: &str, + context: impl Into, + ) -> Option { + let context = context.into(); + match crate::types::sig::parse_checked_signature("typed-core verifier", text) { + Ok(ty) => match scheme_to_fn_sig(ty) { + Ok(signature) => Some(signature), + Err(error) => { + self.fail(Violation::CanonicalSignature { + site: context, + error, + }); + None + } + }, + Err(error) => { + self.fail(Violation::CanonicalSignatureParse { + site: context, + error: error.to_string(), + }); + None + } + } + } + + pub(super) fn instantiate_fn( + &mut self, + signature: &CoreFnSig, + arguments: &[CoreInstantiation], + context: impl Into, + ) -> Option { + let context = context.into(); + self.check_instantiation(arguments); + match instantiate_fn_sig(signature, arguments) { + Ok(signature) => Some(signature), + Err(error) => { + self.fail(Violation::Instantiation { + subject: InstantiationSubject::At(context), + error, + }); + None + } + } + } + + pub(super) fn instantiate_constructor( + &mut self, + signature: &ConstructorSig, + arguments: &[CoreInstantiation], + ) -> Option { + self.check_instantiation(arguments); + match instantiate_constructor_sig(signature, arguments) { + Ok(signature) => Some(signature), + Err(error) => { + self.fail(Violation::Instantiation { + subject: InstantiationSubject::At(Site::At("constructor")), + error, + }); + None + } + } + } + + pub(super) fn instantiate_operation( + &mut self, + signature: &OperationSig, + arguments: &[CoreInstantiation], + ) -> Option { + self.check_instantiation(arguments); + match instantiate_operation_sig(signature, arguments) { + Ok(signature) => Some(signature), + Err(error) => { + self.fail(Violation::Instantiation { + subject: InstantiationSubject::At(Site::At("operation")), + error, + }); + None + } + } + } + + pub(super) fn expect_source( + &mut self, + actual: &CoreType, + expected: &Type, + context: impl Into, + ) { + let context = context.into(); + self.expect_type(actual, &CoreType::Source(expected.clone()), context); + } + + pub(super) fn expect_type( + &mut self, + actual: &CoreType, + expected: &CoreType, + context: impl Into, + ) { + let context = context.into(); + if actual != expected { + self.fail(Violation::TypeMismatch { + site: context, + relation: TypeRelation::Equal, + actual: actual.clone(), + expected: expected.clone(), + }); + } + } + + pub(super) fn expect_subtype_type( + &mut self, + actual: &CoreType, + expected: &CoreType, + context: impl Into, + ) { + let context = context.into(); + if !core_subtype(actual, expected) { + self.fail(Violation::TypeMismatch { + site: context, + relation: TypeRelation::Subtype, + actual: actual.clone(), + expected: expected.clone(), + }); + } + } + + pub(super) fn expect_row( + &mut self, + actual: &EffRow, + expected: &EffRow, + context: impl Into, + ) { + let context = context.into(); + if actual != expected { + self.fail(Violation::RowMismatch { + site: context, + relation: RowRelation::Equal, + actual: actual.clone(), + expected: expected.clone(), + }); + } + } + + pub(super) fn expect_sig( + &mut self, + actual: &CompSig, + expected: &CompSig, + context: impl Into, + ) { + let context = context.into(); + self.expect_type(actual.result(), expected.result(), context); + self.expect_row(actual.effects(), expected.effects(), context); + } + + pub(super) fn expect_subtype_sig( + &mut self, + actual: &CompSig, + expected: &CompSig, + context: impl Into, + ) { + let context = context.into(); + self.expect_subtype_type(actual.result(), expected.result(), context); + if !row_included(actual.effects(), expected.effects()) { + self.fail(Violation::RowMismatch { + site: context, + relation: RowRelation::Subrow, + actual: actual.effects().clone(), + expected: expected.effects().clone(), + }); + } + } + + /// The Bind discipline for a node whose signature is derived from a + /// subcomputation it observes: the stored result may refine the derived + /// result, but the stored row must include every derived effect. A node + /// never sheds effects it observes (forcing Thunk(Int ! {IO}) cannot be + /// labelled Int ! {}). + pub(super) fn expect_supertype_sig( + &mut self, + actual: &CompSig, + derived: &CompSig, + context: impl Into, + ) { + let context = context.into(); + self.expect_subtype_type(actual.result(), derived.result(), context); + if !row_included(derived.effects(), actual.effects()) { + self.fail(Violation::RowMismatch { + site: context, + relation: RowRelation::Includes, + actual: actual.effects().clone(), + expected: derived.effects().clone(), + }); + } + } + + pub(super) fn union_rows( + &mut self, + left: &EffRow, + right: &EffRow, + context: impl Into, + ) -> Option { + let context = context.into(); + match canonical_union_rows(left, right) { + Ok(row) => Some(row), + Err(error) => { + self.fail(Violation::RowUnion { + site: context, + error, + }); + None + } + } + } +} diff --git a/crates/prism-core/src/core/typed/verify/check/mod.rs b/crates/prism-core/src/core/typed/verify/check/mod.rs new file mode 100644 index 00000000..ab2af87a --- /dev/null +++ b/crates/prism-core/src/core/typed/verify/check/mod.rs @@ -0,0 +1,70 @@ +mod expect; +mod phase; +mod state; +mod walk; + +use std::collections::{BTreeMap, BTreeSet}; +use std::marker::PhantomData; + +use prism_common::sym::Sym; + +use super::super::{CoreFnSig, CoreType, TypedCoreFn}; +use super::{CoreViolation, VerifyEnv}; +use state::ReuseShell; + +pub use phase::TypedCorePhase; + +/// Check all stored Core judgments without inference or unification. +/// +/// # Errors +/// Returns every independently observed violation. Errors at a parent whose +/// premise is already invalid may be omitted to avoid cascading diagnostics. +pub(in crate::core::typed) fn check_functions( + functions: &[TypedCoreFn], + env: &VerifyEnv, +) -> Result<(), Vec> { + let mut globals = BTreeMap::new(); + let mut duplicate_globals = BTreeSet::new(); + for function in functions { + if globals + .insert(function.name(), function.sig().clone()) + .is_some() + { + duplicate_globals.insert(function.name()); + } + } + + let mut violations = Vec::new(); + for function in functions { + let mut checker = Checker::

::new(function.name(), env, &globals); + if duplicate_globals.contains(&function.name()) { + checker.fail(super::Violation::DuplicateGlobal); + } + checker.function(function); + violations.extend(checker.violations); + } + + if violations.is_empty() { + Ok(()) + } else { + Err(violations) + } +} + +struct Checker<'a, P> { + function: Sym, + env: &'a VerifyEnv, + globals: &'a BTreeMap, + // Each binding records the suspension depth it was introduced at, so a + // reference from a deeper depth is known to read a closure capture slot. + locals: BTreeMap>, + thunk_depth: usize, + token_uses: BTreeMap>, + token_capacities: BTreeMap>, + reuse_shells: BTreeMap>, + allowed_types: BTreeSet, + allowed_rows: BTreeSet, + path: Vec, + violations: Vec, + phase: PhantomData

, +} diff --git a/crates/prism-core/src/core/typed/verify/check/phase.rs b/crates/prism-core/src/core/typed/verify/check/phase.rs new file mode 100644 index 00000000..eedfddac --- /dev/null +++ b/crates/prism-core/src/core/typed/verify/check/phase.rs @@ -0,0 +1,324 @@ +//! Phase vocabulary and well-formedness checks for typed Core witnesses. + +use std::collections::BTreeSet; + +use crate::types::ty::EffRow; +use crate::types::Type; + +use super::super::super::violation::{QuantifierKind, Site, Violation}; +use super::super::super::{ + ArenaPrepared, CompSig, CoreFnSig, CoreInstantiation, CoreQuantifier, CoreType, EffectLowered, + Elaborated, LoweredType, Owned, ReuseLowered, +}; +use super::Checker; + +mod sealed { + pub trait Sealed {} +} + +/// A typed-Core stage with a fixed legal node vocabulary. +pub trait TypedCorePhase: sealed::Sealed { + #[doc(hidden)] + const ALLOW_EFFECT_NODES: bool; + #[doc(hidden)] + const ALLOW_INIT_AT_NODES: bool; + #[doc(hidden)] + const ALLOW_REF_NODES: bool; + #[doc(hidden)] + const ALLOW_RC_NODES: bool; + #[doc(hidden)] + const ALLOW_REUSE_NODES: bool; + #[doc(hidden)] + const ALLOW_LOWERED_ABI: bool; + #[doc(hidden)] + const NAME: &'static str; +} + +macro_rules! phase { + ($phase:ty, $name:literal, $effect:literal, $init_at:literal, $refs:literal, $rc:literal, $reuse:literal, $lowered:literal) => { + impl sealed::Sealed for $phase {} + impl TypedCorePhase for $phase { + const ALLOW_EFFECT_NODES: bool = $effect; + const ALLOW_INIT_AT_NODES: bool = $init_at; + const ALLOW_REF_NODES: bool = $refs; + const ALLOW_RC_NODES: bool = $rc; + const ALLOW_REUSE_NODES: bool = $reuse; + const ALLOW_LOWERED_ABI: bool = $lowered; + const NAME: &'static str = $name; + } + }; +} + +phase!( + Elaborated, + "elaborated", + true, + false, + false, + false, + false, + false +); +phase!( + ArenaPrepared, + "arena-prepared", + true, + true, + false, + false, + false, + false +); +phase!( + EffectLowered, + "effect-lowered", + false, + true, + true, + false, + false, + true +); +phase!(Owned, "owned", false, true, true, true, false, true); +phase!( + ReuseLowered, + "reuse-lowered", + false, + true, + true, + true, + true, + true +); + +impl Checker<'_, P> { + pub(super) fn check_instantiation(&mut self, arguments: &[CoreInstantiation]) { + for argument in arguments { + match argument { + CoreInstantiation::Type(ty) => self.check_source_type(ty), + CoreInstantiation::Row(row) => self.check_row(row), + } + } + } + + pub(super) fn check_fn_sig(&mut self, signature: &CoreFnSig) { + for parameter in signature.params() { + self.check_core_type(parameter); + } + self.check_sig(signature.body()); + } + + pub(super) fn check_sig(&mut self, signature: &CompSig) { + self.check_core_type(signature.result()); + self.check_row(signature.effects()); + } + + pub(super) fn check_core_type(&mut self, ty: &CoreType) { + match ty { + CoreType::Source(ty) => self.check_source_type(ty), + CoreType::Thunk(signature) => self.check_sig(signature), + CoreType::Function(signature) => { + let old_types = self.allowed_types.clone(); + let old_rows = self.allowed_rows.clone(); + let mut local_types = BTreeSet::new(); + let mut local_rows = BTreeSet::new(); + for quantifier in signature.quantifiers() { + match quantifier { + CoreQuantifier::Type(name) => { + if local_rows.contains(name) || !local_types.insert(*name) { + self.fail(Violation::DuplicateQuantifier { + kind: QuantifierKind::Type, + nested: true, + name: *name, + }); + } + self.allowed_types.insert(*name); + } + CoreQuantifier::Row(name) => { + if local_types.contains(name) || !local_rows.insert(*name) { + self.fail(Violation::DuplicateQuantifier { + kind: QuantifierKind::Row, + nested: true, + name: *name, + }); + } + self.allowed_rows.insert(*name); + } + } + } + self.check_fn_sig(signature); + self.allowed_types = old_types; + self.allowed_rows = old_rows; + } + CoreType::Ref(inner) | CoreType::ReuseToken(inner) => self.check_core_type(inner), + CoreType::Lowered(kind) => { + if !P::ALLOW_LOWERED_ABI { + self.fail(Violation::LoweredAbiIllegal { + phase: P::NAME, + found: kind.clone(), + }); + } + match kind { + LoweredType::Word => {} + LoweredType::Eff(row) + | LoweredType::Queue(row) + | LoweredType::QueueView(row) => self.check_row(row), + } + } + } + } + + pub(super) fn check_source_type(&mut self, ty: &Type) { + let mut existentials = BTreeSet::new(); + ty.free_exist(&mut existentials); + if !existentials.is_empty() { + self.fail(Violation::UnsolvedMeta { + kind: QuantifierKind::Type, + ty: ty.clone(), + }); + } + let mut row_existentials = BTreeSet::new(); + ty.free_exist_row(&mut row_existentials); + if !row_existentials.is_empty() { + self.fail(Violation::UnsolvedMeta { + kind: QuantifierKind::Row, + ty: ty.clone(), + }); + } + let mut type_variables = BTreeSet::new(); + ty.free_ty_vars(&mut type_variables); + let unbound_types: Vec<_> = type_variables + .difference(&self.allowed_types) + .copied() + .collect(); + for name in unbound_types { + self.fail(Violation::UnboundRigid { + kind: QuantifierKind::Type, + name, + ty: ty.clone(), + }); + } + let mut row_variables = BTreeSet::new(); + ty.free_row_vars(&mut row_variables); + let unbound_rows: Vec<_> = row_variables + .difference(&self.allowed_rows) + .copied() + .collect(); + for name in unbound_rows { + self.fail(Violation::UnboundRigid { + kind: QuantifierKind::Row, + name, + ty: ty.clone(), + }); + } + check_type_rows(ty, &mut |row| self.check_row(row)); + } + + pub(super) fn check_row(&mut self, row: &EffRow) { + if !row.is_canonical() { + self.fail(Violation::RowNotCanonical { row: row.clone() }); + } + let mut exists = BTreeSet::new(); + row.free_exist_row(&mut exists); + if !exists.is_empty() { + self.fail(Violation::UnsolvedRowMeta { row: row.clone() }); + } + if let EffRow::Var(name) = row.tail() { + if !self.allowed_rows.contains(name) { + self.fail(Violation::UnboundRigidRow { name: *name }); + } + } + for label in row.labels() { + for argument in &label.args { + self.check_source_type(argument); + } + } + } + + pub(super) fn require_effect_node(&mut self, node: &'static str) { + if !P::ALLOW_EFFECT_NODES { + self.fail(Violation::PhaseIllegal { + what: Site::At(node), + phase: P::NAME, + }); + } + } + + pub(super) fn require_init_at_node(&mut self, node: &'static str) { + if !P::ALLOW_INIT_AT_NODES { + self.fail(Violation::PhaseIllegal { + what: Site::At(node), + phase: P::NAME, + }); + } + } + + pub(super) fn require_ref_node(&mut self, node: &'static str) { + if !P::ALLOW_REF_NODES { + self.fail(Violation::PhaseIllegal { + what: Site::At(node), + phase: P::NAME, + }); + } + } + + pub(super) fn require_rc_node(&mut self, node: &'static str) { + if !P::ALLOW_RC_NODES { + self.fail(Violation::PhaseIllegal { + what: Site::At(node), + phase: P::NAME, + }); + } + } + + pub(super) fn require_reuse_node(&mut self, node: &'static str) { + if !P::ALLOW_REUSE_NODES { + self.fail(Violation::PhaseIllegal { + what: Site::At(node), + phase: P::NAME, + }); + } + } +} + +fn check_type_rows(ty: &Type, f: &mut impl FnMut(&EffRow)) { + match ty { + Type::Forall(_, body) + | Type::RowForall(_, body) + | Type::OrNull(body) + | Type::Coeffect(body, _) => check_type_rows(body, f), + Type::Fun(params, row, result) => { + for ty in params { + check_type_rows(ty, f); + } + f(row); + check_type_rows(result, f); + } + Type::Con(_, arguments) | Type::Tuple(arguments) | Type::UnboxedTuple(arguments) => { + for ty in arguments { + check_type_rows(ty, f); + } + } + Type::UnboxedRecord(fields) => { + for (_, ty) in fields { + check_type_rows(ty, f); + } + } + Type::App(head, argument) => { + check_type_rows(head, f); + check_type_rows(argument, f); + } + Type::Row(row) => f(row), + Type::Unit + | Type::Int + | Type::I64 + | Type::U64 + | Type::Bool + | Type::Float + | Type::Char + | Type::Str + | Type::Var(_) + | Type::Exist(_) + | Type::Nat(_) => {} + } +} diff --git a/crates/prism-core/src/core/typed/verify/check/state.rs b/crates/prism-core/src/core/typed/verify/check/state.rs new file mode 100644 index 00000000..d428b15b --- /dev/null +++ b/crates/prism-core/src/core/typed/verify/check/state.rs @@ -0,0 +1,247 @@ +//! Lexical, diagnostic, and reuse-credit state for the typed Core checker. + +use std::collections::{BTreeMap, BTreeSet}; +use std::marker::PhantomData; + +use prism_common::sym::Sym; +use prism_syntax::names; + +use super::super::super::reuse::reuse_cell_capacity; +use super::super::super::violation::{RcSequenceFault, ReuseFault, Violation}; +use super::super::super::{ + BinderErasure, CoreFnSig, CoreQuantifier, CoreType, TypedBinder, TypedPattern, TypedValue, + TypedValueKind, +}; +use super::super::{CoreViolation, VerifyEnv}; +use super::phase::TypedCorePhase; +use super::Checker; + +impl<'a, P: TypedCorePhase> Checker<'a, P> { + pub(super) fn new( + function: Sym, + env: &'a VerifyEnv, + globals: &'a BTreeMap, + ) -> Self { + Self { + function, + env, + globals, + locals: BTreeMap::new(), + thunk_depth: 0, + token_uses: BTreeMap::new(), + token_capacities: BTreeMap::new(), + reuse_shells: BTreeMap::new(), + allowed_types: BTreeSet::new(), + allowed_rows: BTreeSet::new(), + path: vec!["body".into()], + violations: Vec::new(), + phase: PhantomData, + } + } + + pub(super) fn fail(&mut self, kind: Violation) { + self.violations.push(CoreViolation { + function: self.function, + path: self.path.join("."), + kind, + }); + } + + pub(super) fn at(&mut self, segment: impl Into, f: impl FnOnce(&mut Self)) { + self.path.push(segment.into()); + f(self); + self.path.pop(); + } + + pub(super) fn bind(&mut self, binder: &TypedBinder) { + if binder.erasure == BinderErasure::RcSequence { + self.fail(Violation::RcSequence( + RcSequenceFault::OutsideAdministrativeBind, + )); + } + if binder.name() == Sym::new(names::RC_SEQUENCE_BINDER) { + self.fail(Violation::RcSequence( + RcSequenceFault::MissingErasureWitness, + )); + } + self.check_core_type(binder.ty()); + self.locals + .entry(binder.name()) + .or_default() + .push((binder.ty().clone(), self.thunk_depth)); + } + + fn unbind(&mut self, name: Sym) { + if let Some(stack) = self.locals.get_mut(&name) { + stack.pop(); + if stack.is_empty() { + self.locals.remove(&name); + } + } + } + + pub(super) fn local(&self, name: Sym) -> Option { + self.locals + .get(&name) + .and_then(|stack| stack.last()) + .map(|(ty, _)| ty.clone()) + } + + // Whether a reference to `name` crosses a suspension boundary: the binder + // was introduced outside the thunk being checked, so at runtime the + // reference reads a closure capture slot rather than a live binding. + pub(super) fn captured(&self, name: Sym) -> bool { + self.locals + .get(&name) + .and_then(|stack| stack.last()) + .is_some_and(|(_, depth)| *depth < self.thunk_depth) + } + + pub(super) fn scoped_binders(&mut self, binders: &[&TypedBinder], f: impl FnOnce(&mut Self)) { + let mut names = BTreeSet::new(); + for binder in binders { + if !names.insert(binder.name()) { + self.fail(Violation::DuplicateBinder { + name: binder.name(), + }); + } + self.bind(binder); + } + f(self); + for binder in binders.iter().rev() { + self.unbind(binder.name()); + } + } + + pub(super) fn case_reuse_shell( + &self, + scrutinee: &TypedValue, + pattern: &TypedPattern, + ) -> Option<(Sym, ReuseShell)> { + let capacity = reuse_cell_capacity(pattern, scrutinee.ty())?; + let TypedValueKind::Var { name, .. } = scrutinee.kind() else { + return None; + }; + let binding_depth = self.locals.get(name)?.len(); + Some(( + *name, + ReuseShell { + scrutinee: scrutinee.clone(), + binding_depth, + capacity, + remaining: 1, + }, + )) + } + + pub(super) fn claim_reuse_shell(&mut self, freed: &TypedValue) -> Result { + let TypedValueKind::Var { name, .. } = freed.kind() else { + return Err(ReuseFault::ScrutineeNotActive); + }; + let binding_depth = self.locals.get(name).map_or(0, Vec::len); + let Some(shell) = self + .reuse_shells + .get_mut(name) + .and_then(|shells| shells.last_mut()) + .filter(|shell| shell.scrutinee == *freed && shell.binding_depth == binding_depth) + else { + return Err(ReuseFault::ScrutineeNotActive); + }; + if shell.remaining == 0 { + return Err(ReuseFault::ScrutineeFreedTwice); + } + shell.remaining = 0; + Ok(shell.capacity) + } + + pub(super) fn scoped_quantifiers( + &mut self, + quantifiers: &[CoreQuantifier], + f: impl FnOnce(&mut Self), + ) { + let old_types = self.allowed_types.clone(); + let old_rows = self.allowed_rows.clone(); + for quantifier in quantifiers { + match quantifier { + CoreQuantifier::Type(name) => { + self.allowed_types.insert(*name); + } + CoreQuantifier::Row(name) => { + self.allowed_rows.insert(*name); + } + } + } + f(self); + self.allowed_types = old_types; + self.allowed_rows = old_rows; + } +} + +#[derive(Clone, Debug, PartialEq)] +pub(super) struct ReuseShell { + scrutinee: TypedValue, + pub(super) binding_depth: usize, + capacity: usize, + remaining: u8, +} + +pub(super) fn pop_scoped(scopes: &mut BTreeMap>, name: Sym) -> Option { + let (value, empty) = { + let stack = scopes.get_mut(&name)?; + let value = stack.pop(); + (value, stack.is_empty()) + }; + if empty { + scopes.remove(&name); + } + value +} + +pub(super) fn merge_token_states( + left: &BTreeMap>, + right: &BTreeMap>, +) -> BTreeMap> { + left.iter() + .map(|(name, credits)| { + ( + *name, + credits + .iter() + .enumerate() + .map(|(index, credit)| { + (*credit).min( + right + .get(name) + .and_then(|other| other.get(index)) + .copied() + .unwrap_or_default(), + ) + }) + .collect(), + ) + }) + .collect() +} + +pub(super) fn merge_shell_states( + left: &BTreeMap>, + right: &BTreeMap>, +) -> BTreeMap> { + let mut merged = left.clone(); + for (name, shells) in &mut merged { + for (index, shell) in shells.iter_mut().enumerate() { + let other = right.get(name).and_then(|others| others.get(index)); + shell.remaining = other.map_or(0, |other| { + if shell.scrutinee == other.scrutinee + && shell.binding_depth == other.binding_depth + && shell.capacity == other.capacity + { + shell.remaining.min(other.remaining) + } else { + 0 + } + }); + } + } + merged +} diff --git a/crates/prism-core/src/core/typed/verify/check/walk.rs b/crates/prism-core/src/core/typed/verify/check/walk.rs new file mode 100644 index 00000000..09a64bd8 --- /dev/null +++ b/crates/prism-core/src/core/typed/verify/check/walk.rs @@ -0,0 +1,1509 @@ +//! Syntax-directed traversal of witness-carrying Core. + +use std::collections::{BTreeMap, BTreeSet}; + +use prism_common::sym::Sym; +use prism_syntax::names::{self, ALLOC_OP, IO_EFFECT}; + +use crate::core::builtins::Builtin; +use crate::core::typed::build::lower_value_type; +use crate::core::typed::reuse::rebuild_arity; +use crate::core::typed::violation::{ + ArityBound, ArityRelation, Form, InstantiationSubject, NameKind, QuantifierKind, + RcOperandFault, RcSequenceFault, ReuseFault, RowRelation, Site, Violation, +}; +use crate::core::typed::{ + BinderErasure, CompSig, CoreFnSig, CoreInstantiation, CoreQuantifier, CoreType, TypedBinder, + TypedComp, TypedCompKind, TypedCoreFn, TypedHandleOp, TypedHandler, TypedPattern, TypedValue, + TypedValueKind, CORE_GROW_STACK, CORE_MIN_STACK, +}; +use crate::core::CoreOp::{ + Add, Addf, Div, Divf, Eq, Eqf, Ge, Gef, Gt, Gtf, Le, Lef, Lt, Ltf, Mul, Mulf, Ne, Nef, Rem, + Sub, Subf, +}; +use crate::core::{CoreOp, IoOp, NegLane}; +use crate::types::ty::{EffRow, Label}; +use crate::types::{layout_of_type_in, AbiLayout, Repr, Type}; + +use super::super::compat::{representation_preserving, row_included}; +use super::super::env::MonoOperation; +use super::super::instantiate::instantiate_value_scheme; +use super::super::{ + SITE_CONSTRUCTOR_FIELD, SITE_DUP, SITE_INIT_AT, SITE_INIT_AT_CELL, SITE_INTEGER_LITERAL, + SITE_IO_OPERATION, SITE_PRODUCT_FIELD, SITE_RC_SEQUENCE_WITNESS, +}; +use super::phase::TypedCorePhase; +use super::state::{merge_shell_states, merge_token_states, pop_scoped}; +use super::Checker; + +impl Checker<'_, P> { + pub(super) fn function(&mut self, function: &TypedCoreFn) { + for quantifier in function.sig().quantifiers() { + match quantifier { + CoreQuantifier::Type(name) => { + if self.allowed_rows.contains(name) || !self.allowed_types.insert(*name) { + self.fail(Violation::DuplicateQuantifier { + kind: QuantifierKind::Type, + nested: false, + name: *name, + }); + } + } + CoreQuantifier::Row(name) => { + if self.allowed_types.contains(name) || !self.allowed_rows.insert(*name) { + self.fail(Violation::DuplicateQuantifier { + kind: QuantifierKind::Row, + nested: false, + name: *name, + }); + } + } + } + } + self.check_fn_sig(function.sig()); + + if function.dict_arity() > function.params().len() { + self.fail(Violation::Arity { + counted: Site::At("dictionary"), + relation: ArityRelation::AtMost, + bound: ArityBound::Parameter, + found: function.dict_arity(), + expected: function.params().len(), + }); + } + if function.params().len() != function.sig().params().len() { + self.fail(Violation::Arity { + counted: Site::At("parameter"), + relation: ArityRelation::Exact, + bound: ArityBound::Signature, + found: function.params().len(), + expected: function.sig().params().len(), + }); + } + let mut parameter_names = BTreeSet::new(); + for (index, parameter) in function.params().iter().enumerate() { + self.at(format!("param[{index}]"), |this| { + if let Some(expected) = function.sig().params().get(index) { + this.expect_type(parameter.ty(), expected, "parameter witness"); + } + if !parameter_names.insert(parameter.name()) { + this.fail(Violation::DuplicateBinder { + name: parameter.name(), + }); + } + this.bind(parameter); + }); + } + + self.comp(function.body()); + self.expect_subtype_sig( + function.body().sig(), + function.sig().body(), + "function body", + ); + } + + fn value(&mut self, value: &TypedValue) { + self.check_core_type(value.ty()); + match value.kind() { + TypedValueKind::Var { + name, + instantiation, + } => { + if let Some(local) = self.local(*name) { + self.check_instantiation(instantiation); + let instantiated = if instantiation.is_empty() && value.ty() == &local { + Ok(local.clone()) + } else { + instantiate_value_scheme(&local, instantiation) + }; + match instantiated { + Ok(instantiated) => { + self.expect_type( + value.ty(), + &instantiated, + Site::LocalReference(*name), + ); + } + Err(error) => { + self.fail(Violation::Instantiation { + subject: InstantiationSubject::Local(*name), + error, + }); + } + } + if matches!(local, CoreType::ReuseToken(_)) { + self.fail(Violation::Reuse(ReuseFault::Escapes(*name))); + } + if self.captured(*name) && !self.one_boundary_word(&local) { + self.fail(Violation::CellSlotNotOneWord { + site: Site::LocalReference(*name), + ty: local, + }); + } + } else if let Some(global) = self.globals.get(name).cloned() { + if let Some(sig) = self.instantiate_fn(&global, instantiation, "global") { + self.expect_type( + value.ty(), + &CoreType::Function(Box::new(sig)), + "global function reference", + ); + } + } else { + self.fail(Violation::UnboundReference { name: *name }); + } + } + TypedValueKind::Int(_) => { + if !matches!(value.ty(), CoreType::Source(Type::Int | Type::Char)) { + self.fail(Violation::LiteralWitness { + site: Site::At(SITE_INTEGER_LITERAL), + witness: value.ty().clone(), + }); + } + } + TypedValueKind::I64(_) => self.expect_source(value.ty(), &Type::I64, "i64 literal"), + TypedValueKind::U64(_) => self.expect_source(value.ty(), &Type::U64, "u64 literal"), + TypedValueKind::Float(_) => { + self.expect_source(value.ty(), &Type::Float, "float literal"); + } + TypedValueKind::Bool(_) => self.expect_source(value.ty(), &Type::Bool, "bool literal"), + TypedValueKind::Unit => self.expect_source(value.ty(), &Type::Unit, "unit literal"), + TypedValueKind::Str(_) => self.expect_source(value.ty(), &Type::Str, "string literal"), + TypedValueKind::Reinterpret(inner) => { + self.at("reinterpret", |this| this.value(inner)); + if !representation_preserving(inner.ty(), value.ty()) { + self.fail(Violation::ReprCoercionIllegal { + from: inner.ty().clone(), + to: value.ty().clone(), + }); + } + } + TypedValueKind::LoweredRepr { + value: inner, + proof, + } => { + self.at("lowered-repr", |this| this.value(inner)); + if !P::ALLOW_LOWERED_ABI { + self.fail(Violation::PhaseIllegal { + what: Site::At("lowered representation evidence"), + phase: P::NAME, + }); + } + if !proof.validates(inner.ty(), value.ty()) { + self.fail(Violation::ReprConversionIllegal { + from: inner.ty().clone(), + to: value.ty().clone(), + }); + } + } + TypedValueKind::NewtypeRepr { + constructor, + instantiation, + value: inner, + } => { + self.at("newtype-repr", |this| this.value(inner)); + if !self.env.newtype_constructors.contains(constructor) { + self.fail(Violation::NotANewtype { + constructor: *constructor, + }); + return; + } + let Some(declared) = self.env.constructor(*constructor).cloned() else { + self.fail(Violation::UnknownName { + kind: NameKind::CoercionConstructor, + name: *constructor, + }); + return; + }; + let Some(instantiated) = self.instantiate_constructor(&declared, instantiation) + else { + return; + }; + let [field] = instantiated.fields.as_slice() else { + self.fail(Violation::NewtypeFieldCount { + constructor: *constructor, + found: instantiated.fields.len(), + }); + return; + }; + let construction = inner.ty() == field && value.ty() == &instantiated.result; + let projection = inner.ty() == &instantiated.result && value.ty() == field; + if !construction && !projection { + self.fail(Violation::NewtypeCoercionDisconnected { + constructor: *constructor, + field: field.clone(), + result: instantiated.result.clone(), + inner: inner.ty().clone(), + outer: value.ty().clone(), + }); + } + } + TypedValueKind::Thunk(body) => { + let token_state = self.token_uses.clone(); + let shell_state = self.reuse_shells.clone(); + let quantifiers = match body.sig().result() { + CoreType::Function(signature) => signature.quantifiers().to_vec(), + _ => Vec::new(), + }; + self.thunk_depth += 1; + self.scoped_quantifiers(&quantifiers, |this| { + this.at("thunk", |this| this.comp(body)); + }); + self.thunk_depth -= 1; + if self.token_uses != token_state { + self.fail(Violation::Reuse(ReuseFault::CapturesToken(Site::At( + "a suspended computation", + )))); + } + if self.reuse_shells != shell_state { + self.fail(Violation::Reuse(ReuseFault::FreesShell(Site::At( + "a suspended computation", + )))); + } + self.token_uses = token_state; + self.reuse_shells = shell_state; + self.expect_type( + value.ty(), + &CoreType::Thunk(Box::new(body.sig().clone())), + "thunk witness", + ); + } + TypedValueKind::Ctor { + name, + tag, + instantiation, + fields, + } => self.constructor_value(*name, *tag, instantiation, fields, value.ty()), + TypedValueKind::Tuple(fields) => { + self.product_value(fields, value.ty(), ProductKind::Tuple); + } + TypedValueKind::UnboxedTuple(fields) => { + self.product_value(fields, value.ty(), ProductKind::UnboxedTuple); + } + TypedValueKind::UnboxedRecord(fields) => self.record_value(fields, value.ty()), + } + } + + fn constructor_value( + &mut self, + name: Sym, + tag: usize, + instantiation: &[CoreInstantiation], + fields: &[TypedValue], + witness: &CoreType, + ) { + let Some(declared) = self.env.constructor(name).cloned() else { + self.fail(Violation::UnknownName { + kind: NameKind::Constructor, + name, + }); + fields.iter().enumerate().for_each(|(index, field)| { + self.at(format!("field[{index}]"), |this| this.value(field)); + }); + return; + }; + let Some(instantiated) = self.instantiate_constructor(&declared, instantiation) else { + return; + }; + if tag != instantiated.tag { + self.fail(Violation::ConstructorTag { + name, + found: tag, + declared: instantiated.tag, + }); + } + self.values(fields, &instantiated.fields, SITE_CONSTRUCTOR_FIELD); + self.cell_slots(&instantiated.fields, SITE_CONSTRUCTOR_FIELD); + self.expect_type(witness, &instantiated.result, "constructor result"); + } + + fn product_value(&mut self, fields: &[TypedValue], witness: &CoreType, kind: ProductKind) { + let expected = match witness { + CoreType::Source(Type::Tuple(types)) if kind == ProductKind::Tuple => Some(types), + CoreType::Source(Type::UnboxedTuple(types)) if kind == ProductKind::UnboxedTuple => { + Some(types) + } + CoreType::Source(Type::UnboxedRecord(expected)) + if kind == ProductKind::UnboxedTuple => + { + let types: Vec<_> = expected.iter().map(|(_, ty)| ty.clone()).collect(); + self.values( + fields, + &types.iter().map(lower_value_type).collect::>(), + SITE_PRODUCT_FIELD, + ); + return; + } + _ => None, + }; + let expected = expected.cloned(); + if let Some(expected) = expected { + let expected: Vec<_> = expected.iter().map(lower_value_type).collect(); + self.values(fields, &expected, SITE_PRODUCT_FIELD); + // Only a boxed tuple allocates a cell; an unboxed product has no + // cell, and its fields keep their component layouts by design. + if kind == ProductKind::Tuple { + self.cell_slots(&expected, SITE_PRODUCT_FIELD); + } + } else { + self.fail(Violation::ProductShape { + witness: witness.clone(), + }); + for (index, field) in fields.iter().enumerate() { + self.at(format!("field[{index}]"), |this| this.value(field)); + } + } + } + + fn record_value(&mut self, fields: &[(Sym, TypedValue)], witness: &CoreType) { + let Some(expected) = (match witness { + CoreType::Source(Type::UnboxedRecord(fields)) => Some(fields.clone()), + _ => None, + }) else { + self.fail(Violation::UnboxedRecordWitness { + witness: witness.clone(), + }); + for (name, value) in fields { + self.at(format!("field[{name}]"), |this| this.value(value)); + } + return; + }; + if fields.len() != expected.len() { + self.fail(Violation::Arity { + counted: Site::At("record field"), + relation: ArityRelation::Exact, + bound: ArityBound::Witness, + found: fields.len(), + expected: expected.len(), + }); + } + for (index, (name, value)) in fields.iter().enumerate() { + self.at(format!("field[{name}]"), |this| { + this.value(value); + if let Some((expected_name, ty)) = expected.get(index) { + if name != expected_name { + this.fail(Violation::RecordField { + found: *name, + expected: *expected_name, + }); + } + this.expect_type(value.ty(), &lower_value_type(ty), "record field"); + } + }); + } + } + + fn comp(&mut self, comp: &TypedComp) { + // The verifier recurses per typed node; grow stack segments inside the + // recursion, same discipline as the builder it checks. + stacker::maybe_grow(CORE_MIN_STACK, CORE_GROW_STACK, || { + self.comp_inner(comp); + }); + } + + fn comp_inner(&mut self, comp: &TypedComp) { + self.check_sig(comp.sig()); + match comp.kind() { + TypedCompKind::Return(value) => { + self.value(value); + self.expect_sig( + comp.sig(), + &CompSig::new(value.ty().clone(), EffRow::Empty), + "return", + ); + } + TypedCompKind::Bind(first, binder, rest) => { + self.check_bind_comp(comp, first, binder, rest); + } + TypedCompKind::Force(value) => { + self.value(value); + match value.ty() { + CoreType::Thunk(sig) => { + self.expect_supertype_sig(comp.sig(), sig, "force"); + } + other => self.fail(Violation::NotAForm { + site: Site::At("force operand"), + expected: Form::Thunk, + found: other.clone(), + }), + } + } + TypedCompKind::Lam(params, body) => self.check_lambda_comp(comp, params, body), + TypedCompKind::App { + callee, + instantiation, + args, + } => { + self.at("callee", |this| this.comp(callee)); + let Some(signature) = (match callee.sig().result() { + CoreType::Function(sig) => { + self.instantiate_fn(sig, instantiation, "computed application") + } + other => { + self.fail(Violation::NotAForm { + site: Site::At("application callee"), + expected: Form::Function, + found: other.clone(), + }); + None + } + }) else { + return; + }; + self.values(args, signature.params(), "application argument"); + if let Some(effects) = self.union_rows( + callee.sig().effects(), + signature.body().effects(), + "application effect union", + ) { + self.expect_sig( + comp.sig(), + &CompSig::new(signature.body().result().clone(), effects), + "application", + ); + } + } + TypedCompKind::If(condition, yes, no) => { + self.check_if_comp(comp, condition, yes, no); + } + TypedCompKind::Prim(op, lhs, rhs) => self.primitive(comp, *op, lhs, rhs), + TypedCompKind::Call { + callee, + instantiation, + args, + } => { + let Some(declared) = self.globals.get(callee).cloned() else { + self.fail(Violation::UnknownName { + kind: NameKind::Function, + name: *callee, + }); + self.values(args, &[], "call argument"); + return; + }; + let Some(signature) = self.instantiate_fn(&declared, instantiation, "call") else { + return; + }; + self.values(args, signature.params(), "call argument"); + self.expect_sig(comp.sig(), signature.body(), "direct call"); + } + TypedCompKind::Io(op, args) => self.io(comp, *op, args), + TypedCompKind::Error(value) => { + self.value(value); + if !matches!(value.ty(), CoreType::Source(Type::Int | Type::Str)) { + self.fail(Violation::ErrorArgumentWitness { + witness: value.ty().clone(), + }); + } + // `Core::Error` is an aborting runtime trap, not the source + // `Exn` effect. Its result and row witnesses are unreachable + // and therefore inherited from the surrounding computation. + } + TypedCompKind::Case(scrutinee, arms) => self.case(comp, scrutinee, arms), + TypedCompKind::FloatBuiltin(op, value) => { + self.value(value); + if let Some(signature) = self.registry_signature(op.signature(), "float builtin") { + self.values( + std::slice::from_ref(value), + signature.params(), + "float argument", + ); + self.expect_sig(comp.sig(), signature.body(), "float builtin"); + } + } + TypedCompKind::Neg(lane, value) => { + self.value(value); + let ty = match lane { + NegLane::Int => Type::Int, + NegLane::I64 => Type::I64, + NegLane::Float => Type::Float, + }; + self.expect_source(value.ty(), &ty, "negation operand"); + self.expect_sig( + comp.sig(), + &CompSig::new(CoreType::Source(ty), EffRow::Empty), + "negation", + ); + } + TypedCompKind::UnboxedProject(value, field) => { + self.value(value); + let Some(field_ty) = (match value.ty() { + CoreType::Source(Type::UnboxedRecord(fields)) => fields + .iter() + .find_map(|(name, ty)| (name == field).then(|| ty.clone())), + _ => None, + }) else { + self.fail(Violation::AbsentField { + field: *field, + operand: value.ty().clone(), + }); + return; + }; + self.expect_sig( + comp.sig(), + &CompSig::new(lower_value_type(&field_ty), EffRow::Empty), + "unboxed projection", + ); + } + TypedCompKind::Do { + operation, + instantiation, + args, + } => self.operation(comp, *operation, instantiation, args), + TypedCompKind::Handle { + body, + return_binder, + return_body, + ops, + } => self.handle( + comp, + body, + return_binder.as_ref(), + return_body.as_deref(), + ops, + ), + TypedCompKind::Mask(effects, body) => { + self.require_effect_node("mask"); + self.at("masked", |this| this.comp(body)); + let residual = subtract_names(body.sig().effects(), effects); + self.expect_sig( + comp.sig(), + &CompSig::new(body.sig().result().clone(), residual), + "mask", + ); + } + TypedCompKind::StrBuiltin { + op, + instantiation, + args, + } => self.builtin(comp, *op, instantiation, args), + TypedCompKind::Dup(value) => { + self.require_rc_node(SITE_DUP); + self.value(value); + self.check_rc_operand(value); + self.expect_sig( + comp.sig(), + &CompSig::new(CoreType::Source(Type::Unit), EffRow::Empty), + SITE_DUP, + ); + } + TypedCompKind::Drop(value) => { + self.require_rc_node("drop"); + self.value(value); + self.check_rc_operand(value); + self.expect_sig( + comp.sig(), + &CompSig::new(CoreType::Source(Type::Unit), EffRow::Empty), + "drop", + ); + } + TypedCompKind::WithReuse { token, freed, body } => { + self.require_reuse_node("with-reuse"); + self.value(freed); + self.check_rc_operand(freed); + self.expect_type( + token.ty(), + &CoreType::ReuseToken(Box::new(freed.ty().clone())), + "reuse-token binder", + ); + let capacity = match self.claim_reuse_shell(freed) { + Ok(capacity) => capacity, + Err(fault) => { + self.fail(Violation::Reuse(fault)); + 0 + } + }; + self.token_uses.entry(token.name()).or_default().push(1); + self.token_capacities + .entry(token.name()) + .or_default() + .push(capacity); + self.at("reuse-body", |this| { + this.scoped_binders(&[token], |this| this.comp(body)); + }); + let credit = pop_scoped(&mut self.token_uses, token.name()).unwrap_or(1); + pop_scoped(&mut self.token_capacities, token.name()); + if credit != 0 { + self.fail(Violation::Reuse(ReuseFault::NotConsumedOnce(token.name()))); + } + self.expect_sig(comp.sig(), body.sig(), "with-reuse"); + } + TypedCompKind::Reuse(token, value) => self.check_reuse_comp(comp, token, value), + TypedCompKind::InitAt(cell, ctor) => { + self.require_init_at_node(SITE_INIT_AT); + self.value(cell); + self.value(ctor); + // The cell is whatever the checked `alloc` operation hands out, + // read from the environment rather than named here: the node is + // a proof that this allocator's cell now holds this + // constructor, so the two must agree by declaration. + match self.env.operation(Sym::new(ALLOC_OP)) { + Some(alloc) => { + let expected = alloc.result().clone(); + self.expect_type(cell.ty(), &expected, SITE_INIT_AT_CELL); + } + None => self.fail(Violation::InitAtWithoutAlloc), + } + if !matches!( + ctor.kind(), + TypedValueKind::Ctor { .. } | TypedValueKind::Tuple(_) + ) { + self.fail(Violation::InitAtPayloadIsNotAllocation); + } + self.expect_sig( + comp.sig(), + &CompSig::new(ctor.ty().clone(), EffRow::Empty), + SITE_INIT_AT, + ); + } + TypedCompKind::RefNew(value) => { + self.require_ref_node("ref-new"); + self.value(value); + self.expect_sig( + comp.sig(), + &CompSig::new(CoreType::Ref(Box::new(value.ty().clone())), EffRow::Empty), + "ref-new", + ); + } + TypedCompKind::RefGet(value) => { + self.require_ref_node("ref-get"); + self.value(value); + match value.ty() { + CoreType::Ref(inner) => self.expect_sig( + comp.sig(), + &CompSig::new(inner.as_ref().clone(), EffRow::Empty), + "ref-get", + ), + other => self.fail(Violation::NotAForm { + site: Site::At("ref-get operand"), + expected: Form::Reference, + found: other.clone(), + }), + } + } + TypedCompKind::RefSet(cell, value) => { + self.require_ref_node("ref-set"); + self.value(cell); + self.value(value); + match cell.ty() { + CoreType::Ref(inner) => { + self.expect_type(value.ty(), inner, "ref-set value"); + } + other => self.fail(Violation::NotAForm { + site: Site::At("ref-set target"), + expected: Form::Reference, + found: other.clone(), + }), + } + self.expect_sig( + comp.sig(), + &CompSig::new(CoreType::Source(Type::Unit), EffRow::Empty), + "ref-set", + ); + } + } + } + + fn check_bind_comp( + &mut self, + comp: &TypedComp, + first: &TypedComp, + binder: &TypedBinder, + rest: &TypedComp, + ) { + self.at("first", |this| this.comp(first)); + self.expect_type(binder.ty(), first.sig().result(), "bind binder"); + if binder.erasure == BinderErasure::RcSequence { + if !P::ALLOW_RC_NODES { + self.fail(Violation::PhaseIllegal { + what: Site::At(SITE_RC_SEQUENCE_WITNESS), + phase: P::NAME, + }); + } + if binder.name() != Sym::new(names::RC_SEQUENCE_BINDER) { + self.fail(Violation::RcSequence( + RcSequenceFault::WrongReservedIdentity, + )); + } + self.expect_type( + binder.ty(), + &CoreType::Source(Type::Unit), + SITE_RC_SEQUENCE_WITNESS, + ); + match first.kind() { + // The operand is the reference the operation acts on, + // so it has to be a term that reads one. A constructed + // value has no prior owner to retain and no owner to + // release, so an operation standing on one is justified + // by nothing and a later consumer asking which binding + // it discharged would have no answer. + TypedCompKind::Dup(operand) | TypedCompKind::Drop(operand) => { + if operand.referenced_binding().is_none() { + self.fail(Violation::RcSequence( + RcSequenceFault::OperandIsNotAReference, + )); + } + } + _ => self.fail(Violation::RcSequence(RcSequenceFault::NotADupOrDrop)), + } + self.check_core_type(binder.ty()); + self.at("rest", |this| this.comp(rest)); + } else { + self.at("rest", |this| { + this.scoped_binders(&[binder], |this| this.comp(rest)); + }); + } + if let Some(effects) = self.union_rows( + first.sig().effects(), + rest.sig().effects(), + "bind effect union", + ) { + self.expect_subtype_type(comp.sig().result(), rest.sig().result(), "bind"); + if !row_included(&effects, comp.sig().effects()) { + self.fail(Violation::RowMismatch { + site: Site::At("bind"), + relation: RowRelation::Includes, + actual: comp.sig().effects().clone(), + expected: effects, + }); + } + } + } + + fn check_lambda_comp(&mut self, comp: &TypedComp, params: &[TypedBinder], body: &TypedComp) { + let token_state = self.token_uses.clone(); + let shell_state = self.reuse_shells.clone(); + let Some(signature) = (match comp.sig().result() { + CoreType::Function(signature) => Some(signature.as_ref()), + other => { + self.fail(Violation::NotAForm { + site: Site::At("lambda result"), + expected: Form::Function, + found: other.clone(), + }); + None + } + }) else { + return; + }; + self.expect_row(comp.sig().effects(), &EffRow::Empty, "lambda"); + if params.len() != signature.params().len() { + self.fail(Violation::Arity { + counted: Site::At("lambda parameter"), + relation: ArityRelation::Exact, + bound: ArityBound::Witness, + found: params.len(), + expected: signature.params().len(), + }); + } + // A lambda closes over its environment: parameters bind at the deeper + // suspension depth, so a body reference to an outer binding is known + // to read a closure capture slot. + self.thunk_depth += 1; + self.scoped_quantifiers(signature.quantifiers(), |this| { + for (parameter, expected) in params.iter().zip(signature.params()) { + this.expect_type(parameter.ty(), expected, "lambda parameter"); + } + this.at("lambda", |this| { + let binders: Vec<_> = params.iter().collect(); + this.scoped_binders(&binders, |this| this.comp(body)); + }); + this.expect_subtype_sig(body.sig(), signature.body(), "lambda body"); + }); + self.thunk_depth -= 1; + if self.token_uses != token_state { + self.fail(Violation::Reuse(ReuseFault::CapturesToken(Site::At( + "a function closure", + )))); + } + if self.reuse_shells != shell_state { + self.fail(Violation::Reuse(ReuseFault::FreesShell(Site::At( + "a function closure", + )))); + } + self.token_uses = token_state; + self.reuse_shells = shell_state; + } + + fn check_if_comp( + &mut self, + comp: &TypedComp, + condition: &TypedValue, + yes: &TypedComp, + no: &TypedComp, + ) { + self.value(condition); + self.expect_source(condition.ty(), &Type::Bool, "if condition"); + let token_state = self.token_uses.clone(); + let shell_state = self.reuse_shells.clone(); + self.at("yes", |this| this.comp(yes)); + let yes_tokens = self.token_uses.clone(); + let yes_shells = self.reuse_shells.clone(); + self.token_uses = token_state; + self.reuse_shells = shell_state; + self.at("no", |this| this.comp(no)); + let no_tokens = self.token_uses.clone(); + let no_shells = self.reuse_shells.clone(); + if yes_tokens != no_tokens { + self.fail(Violation::Reuse(ReuseFault::UnequalCredits(Site::At( + "if branches", + )))); + } + self.token_uses = merge_token_states(&yes_tokens, &no_tokens); + self.reuse_shells = merge_shell_states(&yes_shells, &no_shells); + self.expect_type(yes.sig().result(), no.sig().result(), "if branch result"); + if let Some(effects) = + self.union_rows(yes.sig().effects(), no.sig().effects(), "if effect union") + { + self.expect_sig( + comp.sig(), + &CompSig::new(yes.sig().result().clone(), effects), + "if", + ); + } + } + + // Whether a type occupies exactly one GC-scanned word at a cell boundary. + // Constructor fields, boxed tuple fields, and closure captures are each + // one slot of a heap cell, and every downstream computation over those + // cells (allocation size, field offsets, reuse capacity) is a plain field + // count on that assumption. Core-private types are all single runtime + // words: a suspension, closure, or mutable cell is a pointer, a lowered + // ABI value is one word by construction, and a reuse token erases to the + // shell pointer it recycles (its linearity is policed separately). A + // source type is judged by its boundary layout under the environment's + // declaration evidence: an unboxed product stored in a slot is its boxed + // boundary form, and a nominal awaiting declaration evidence stays one + // slot, the same posture the erased pipeline takes. + fn one_boundary_word(&self, ty: &CoreType) -> bool { + match ty { + CoreType::Thunk(_) + | CoreType::Function(_) + | CoreType::Ref(_) + | CoreType::ReuseToken(_) + | CoreType::Lowered(_) => true, + CoreType::Source(ty) => { + let env = self.env; + let layout = layout_of_type_in(ty, |name| env.nominal_is_boxed(name)); + matches!(layout.abi(), AbiLayout::DeferredNominal) + || layout.abi().repr().is_some_and(|repr| repr.is_gc_value()) + } + } + } + + fn cell_slots(&mut self, slots: &[CoreType], site: &'static str) { + for ty in slots { + if !self.one_boundary_word(ty) { + self.fail(Violation::CellSlotNotOneWord { + site: Site::At(site), + ty: ty.clone(), + }); + } + } + } + + // Whether a `dup`/`drop` (or the cell a `with-reuse` frees) acts on a + // value the count can touch, judged from the layout authority with the + // environment's declaration evidence. Core-private types are heap cells or + // runtime words, so only a linear reuse token is refused among them; for a + // source type only a non-value (an effect row or a type-level natural) is + // refused. A nominal without boxing evidence and a polymorphic word stay + // accepted: both are runtime words whose count is decided dynamically by + // the tag bit. An unboxed product whose fields cannot cross the ABI is + // also accepted: the adapter that boxes it is judged where the boundary is + // introduced, and the count acts on that box. + fn check_rc_operand(&mut self, value: &TypedValue) { + match value.ty() { + CoreType::Thunk(_) + | CoreType::Function(_) + | CoreType::Ref(_) + | CoreType::Lowered(_) => {} + CoreType::ReuseToken(_) => { + self.fail(Violation::RcOperand(RcOperandFault::ReuseToken)); + } + CoreType::Source(ty) => { + let env = self.env; + let layout = layout_of_type_in(ty, |name| env.nominal_is_boxed(name)); + if layout.abi() == &AbiLayout::Invalid && layout.local() == &Repr::Any { + self.fail(Violation::RcOperand(RcOperandFault::NotAValue)); + } + } + } + } + + fn check_reuse_comp(&mut self, comp: &TypedComp, token: &TypedBinder, value: &TypedValue) { + self.require_reuse_node("reuse"); + self.value(value); + let rebuild = rebuild_arity(value); + if rebuild.is_none() { + self.fail(Violation::Reuse(ReuseFault::RebuildIsNotAllocation)); + } + let local = self.local(token.name()); + match local { + Some(local) => { + self.expect_type(token.ty(), &local, "reuse token reference"); + if let (Some(arity), Some(capacity)) = ( + rebuild, + self.token_capacities + .get(&token.name()) + .and_then(|capacities| capacities.last()) + .copied(), + ) { + if arity > capacity { + self.fail(Violation::Arity { + counted: Site::At("reuse rebuild"), + relation: ArityRelation::AtMost, + bound: ArityBound::ShellCapacity, + found: arity, + expected: capacity, + }); + } + } + if let Some(credit) = self + .token_uses + .get_mut(&token.name()) + .and_then(|credits| credits.last_mut()) + { + if *credit == 1 { + *credit = 0; + } else { + self.fail(Violation::Reuse(ReuseFault::ConsumedTwice(token.name()))); + } + } else { + self.fail(Violation::Reuse(ReuseFault::NotActive(token.name()))); + } + } + None => self.fail(Violation::Reuse(ReuseFault::OutOfScope(token.name()))), + } + self.expect_sig( + comp.sig(), + &CompSig::new(value.ty().clone(), EffRow::Empty), + "reuse", + ); + } + + fn primitive(&mut self, comp: &TypedComp, op: CoreOp, lhs: &TypedValue, rhs: &TypedValue) { + self.value(lhs); + self.value(rhs); + let (operand, result) = match op { + Add | Sub | Mul | Div | Rem => (CoreType::Source(Type::Int), Type::Int), + Addf | Subf | Mulf | Divf => (CoreType::Source(Type::Float), Type::Float), + Eqf | Nef | Ltf | Lef | Gtf | Gef => (CoreType::Source(Type::Float), Type::Bool), + Eq | Ne | Lt | Le | Gt | Ge => { + if lhs.ty() != rhs.ty() + || !matches!( + lhs.ty(), + CoreType::Source(Type::Int | Type::Bool | Type::Char) + ) + { + self.fail(Violation::LaneOperands { + lhs: lhs.ty().clone(), + rhs: rhs.ty().clone(), + }); + } + (lhs.ty().clone(), Type::Bool) + } + }; + self.expect_type(lhs.ty(), &operand, "primitive lhs"); + self.expect_type(rhs.ty(), &operand, "primitive rhs"); + self.expect_sig( + comp.sig(), + &CompSig::new(CoreType::Source(result), EffRow::Empty), + "primitive", + ); + } + + fn io(&mut self, comp: &TypedComp, op: IoOp, args: &[TypedValue]) { + if args.len() != op.arity() { + self.fail(Violation::Arity { + counted: Site::At("I/O argument"), + relation: ArityRelation::Exact, + bound: ArityBound::Expected, + found: args.len(), + expected: op.arity(), + }); + } + for (index, argument) in args.iter().enumerate() { + self.at(format!("arg[{index}]"), |this| this.value(argument)); + } + if let Some(argument) = args.first() { + match op { + // The raw printer is the lowering of `forall a. (a) -> Unit`; + // concrete Float/String sites use their specialized nodes while + // a rigid polymorphic value legitimately remains arbitrary. + IoOp::PrintF => { + self.expect_source(argument.ty(), &Type::Float, "float print argument"); + } + IoOp::PrintS => { + self.expect_source(argument.ty(), &Type::Str, "string print argument"); + } + IoOp::Srand => { + self.expect_source(argument.ty(), &Type::Int, "random seed argument"); + } + IoOp::Print | IoOp::PrintNl | IoOp::ReadInt | IoOp::ReadLine | IoOp::Rand => {} + } + } + let result = match op { + IoOp::ReadInt | IoOp::Rand => Type::Int, + IoOp::ReadLine => Type::Str, + IoOp::Print | IoOp::PrintF | IoOp::PrintS | IoOp::PrintNl | IoOp::Srand => Type::Unit, + }; + self.expect_sig( + comp.sig(), + &CompSig::new(CoreType::Source(result), EffRow::singleton(IO_EFFECT)), + SITE_IO_OPERATION, + ); + } + + fn case( + &mut self, + comp: &TypedComp, + scrutinee: &TypedValue, + arms: &[(TypedPattern, TypedComp)], + ) { + self.value(scrutinee); + if arms.is_empty() { + self.fail(Violation::CaseHasNoArms); + return; + } + let mut effects = EffRow::Empty; + let token_state = self.token_uses.clone(); + let shell_state = self.reuse_shells.clone(); + let mut merged_tokens = None; + let mut merged_shells = None; + for (index, (pattern, body)) in arms.iter().enumerate() { + self.token_uses = token_state.clone(); + self.reuse_shells = shell_state.clone(); + self.at(format!("arm[{index}]"), |this| { + let binders = this.pattern(pattern, scrutinee.ty()); + let shell = this.case_reuse_shell(scrutinee, pattern); + let pushes_shell = shell.as_ref().is_some_and(|(name, shell)| { + !this.reuse_shells.get(name).is_some_and(|shells| { + shells + .last() + .is_some_and(|active| active.binding_depth == shell.binding_depth) + }) + }); + if pushes_shell { + if let Some((name, shell)) = &shell { + this.reuse_shells + .entry(*name) + .or_default() + .push(shell.clone()); + } + } + let refs: Vec<_> = binders.iter().collect(); + this.scoped_binders(&refs, |this| this.comp(body)); + if pushes_shell { + if let Some((name, _)) = shell { + pop_scoped(&mut this.reuse_shells, name); + } + } + this.expect_subtype_type( + body.sig().result(), + comp.sig().result(), + "case arm result", + ); + }); + let arm_tokens = self.token_uses.clone(); + let arm_shells = self.reuse_shells.clone(); + if let Some(previous) = &merged_tokens { + if previous != &arm_tokens { + self.fail(Violation::Reuse(ReuseFault::UnequalCredits(Site::At( + "case arms", + )))); + } + merged_tokens = Some(merge_token_states(previous, &arm_tokens)); + } else { + merged_tokens = Some(arm_tokens); + } + merged_shells = Some(match &merged_shells { + Some(previous) => merge_shell_states(previous, &arm_shells), + None => arm_shells, + }); + if let Some(union) = + self.union_rows(&effects, body.sig().effects(), "case effect union") + { + effects = union; + } + } + self.token_uses = merged_tokens.unwrap_or(token_state); + self.reuse_shells = merged_shells.unwrap_or(shell_state); + self.expect_row(comp.sig().effects(), &effects, "case effects"); + } + + fn pattern(&mut self, pattern: &TypedPattern, scrutinee: &CoreType) -> Vec { + match pattern { + TypedPattern::Wild => Vec::new(), + TypedPattern::Var(binder) => { + self.expect_type(binder.ty(), scrutinee, "pattern binder"); + vec![binder.clone()] + } + TypedPattern::Tuple(fields) => { + let expected = match scrutinee { + CoreType::Source(Type::Tuple(types) | Type::UnboxedTuple(types)) => { + Some(types.clone()) + } + CoreType::Source(Type::UnboxedRecord(fields)) => { + Some(fields.iter().map(|(_, ty)| ty.clone()).collect()) + } + _ => None, + }; + let Some(expected) = expected else { + self.fail(Violation::TuplePatternScrutinee { + scrutinee: scrutinee.clone(), + }); + return fields.iter().filter_map(Clone::clone).collect(); + }; + self.pattern_fields(fields, &expected) + } + TypedPattern::Ctor { + name, + instantiation, + fields, + } => { + let Some(declared) = self.env.constructor(*name).cloned() else { + self.fail(Violation::UnknownName { + kind: NameKind::PatternConstructor, + name: *name, + }); + return fields.iter().filter_map(Clone::clone).collect(); + }; + let Some(instantiated) = self.instantiate_constructor(&declared, instantiation) + else { + return fields.iter().filter_map(Clone::clone).collect(); + }; + self.expect_type( + scrutinee, + &instantiated.result, + "constructor pattern result", + ); + if fields.len() != instantiated.fields.len() { + self.fail(Violation::Arity { + counted: Site::At("constructor pattern"), + relation: ArityRelation::Exact, + bound: ArityBound::Declared, + found: fields.len(), + expected: instantiated.fields.len(), + }); + } + let mut binders = Vec::new(); + for (index, binder) in fields.iter().enumerate() { + if let Some(binder) = binder { + if let Some(expected) = instantiated.fields.get(index) { + self.expect_type(binder.ty(), expected, "constructor pattern field"); + } + binders.push(binder.clone()); + } + } + binders + } + } + } + + fn pattern_fields( + &mut self, + fields: &[Option], + expected: &[Type], + ) -> Vec { + if fields.len() != expected.len() { + self.fail(Violation::Arity { + counted: Site::At("tuple pattern"), + relation: ArityRelation::Exact, + bound: ArityBound::Scrutinee, + found: fields.len(), + expected: expected.len(), + }); + } + fields + .iter() + .enumerate() + .filter_map(|(index, binder)| { + binder.as_ref().map(|binder| { + if let Some(expected) = expected.get(index) { + self.expect_type( + binder.ty(), + &lower_value_type(expected), + "tuple pattern field", + ); + } + binder.clone() + }) + }) + .collect() + } + + fn operation( + &mut self, + comp: &TypedComp, + name: Sym, + instantiation: &[CoreInstantiation], + args: &[TypedValue], + ) { + self.require_effect_node("operation"); + let Some(declared) = self.env.operation(name).cloned() else { + self.fail(Violation::UnknownName { + kind: NameKind::Operation, + name, + }); + return; + }; + let Some(instantiated) = self.instantiate_operation(&declared, instantiation) else { + return; + }; + self.values(args, &instantiated.params, "operation argument"); + self.expect_sig( + comp.sig(), + &CompSig::new( + instantiated.result, + EffRow::canonical([instantiated.effect], EffRow::Empty), + ), + "effect operation", + ); + } + + fn handle( + &mut self, + comp: &TypedComp, + body: &TypedComp, + return_binder: Option<&TypedBinder>, + return_body: Option<&TypedComp>, + handler: &TypedHandler, + ) { + self.require_effect_node("handler"); + self.at("handled", |this| this.comp(body)); + let arms = handler.arms(); + if return_binder.is_some() != return_body.is_some() { + self.fail(Violation::HandlerReturnClauseIncomplete); + } + + let mut clause_effects = + if let (Some(binder), Some(return_body)) = (return_binder, return_body) { + self.expect_type(binder.ty(), body.sig().result(), "handler return binder"); + self.at("return", |this| { + this.scoped_binders(&[binder], |this| this.comp(return_body)); + }); + self.expect_subtype_type( + return_body.sig().result(), + comp.sig().result(), + "handler return result", + ); + return_body.sig().effects().clone() + } else { + self.expect_type( + body.sig().result(), + comp.sig().result(), + "handler identity return", + ); + EffRow::Empty + }; + + let mut instantiated_arms = BTreeMap::new(); + for (index, arm) in arms.iter().enumerate() { + self.at(format!("op[{}]", arm.name()), |this| { + let Some(declared) = this.env.operations.get(&arm.name()).cloned() else { + this.fail(Violation::UnknownName { + kind: NameKind::HandledOperation, + name: arm.name(), + }); + return; + }; + let Some(operation) = this.instantiate_operation(&declared, arm.instantiation()) + else { + return; + }; + this.check_handler_arm(arm, &operation, comp.sig()); + instantiated_arms.insert(arm.name(), operation.effect.clone()); + }); + if let Some(union) = self.union_rows( + &clause_effects, + arm.body().sig().effects(), + "handler clause effect union", + ) { + clause_effects = union; + } + let _ = index; + } + + let expected_forwarding = self.residual_forwarding(&instantiated_arms); + let stored_forwarding: Vec<_> = handler + .forwarded() + .iter() + .map(|forward| (forward.operation(), forward.effect().clone())) + .collect(); + if stored_forwarding != expected_forwarding { + self.fail(Violation::ForwardingMismatch { + derived: expected_forwarding, + stored: stored_forwarding, + }); + } + + let discharged = self.exhaustively_handled_labels(body.sig().effects(), &instantiated_arms); + let residual = subtract_labels(body.sig().effects(), &discharged); + if let Some(effects) = self.union_rows(&residual, &clause_effects, "handler effect union") { + if !row_included(&effects, comp.sig().effects()) { + self.fail(Violation::HandlerResidualRow { + derived: effects, + stored: comp.sig().effects().clone(), + }); + } + } + } + + fn residual_forwarding(&self, arms: &BTreeMap) -> Vec<(Sym, Label)> { + let effects: BTreeMap = arms + .values() + .map(|label| (label.name, label.clone())) + .collect(); + self.env + .operations + .iter() + .filter_map(|(operation, declared)| { + effects + .get(&declared.effect.name) + .filter(|_| !arms.contains_key(operation)) + .cloned() + .map(|effect| (*operation, effect)) + }) + .collect() + } + + fn check_handler_arm( + &mut self, + arm: &TypedHandleOp, + operation: &MonoOperation, + outer: &CompSig, + ) { + if arm.params().len() != operation.params.len() { + self.fail(Violation::Arity { + counted: Site::At("operation arm"), + relation: ArityRelation::Exact, + bound: ArityBound::Declared, + found: arm.params().len(), + expected: operation.params.len(), + }); + } + for (binder, expected) in arm.params().iter().zip(&operation.params) { + self.expect_type(binder.ty(), expected, "operation arm parameter"); + } + let resume = CoreType::Thunk(Box::new(CompSig::new( + CoreType::Function(Box::new(CoreFnSig::new( + Vec::new(), + vec![operation.result.clone()], + outer.clone(), + ))), + EffRow::Empty, + ))); + self.expect_type(arm.resume().ty(), &resume, "operation resumption"); + let mut binders: Vec<_> = arm.params().iter().collect(); + binders.push(arm.resume()); + self.scoped_binders(&binders, |this| this.comp(arm.body())); + self.expect_subtype_type( + arm.body().sig().result(), + outer.result(), + "operation arm result", + ); + } + + fn exhaustively_handled_labels( + &self, + body: &EffRow, + arms: &BTreeMap, + ) -> BTreeSet

(body: &TypedComp) -> UncheckedTypedCore

{ + UncheckedTypedCore::new(vec![TypedCoreFn::new( + Sym::new("main"), + Vec::new(), + body.clone(), + CoreFnSig::new(Vec::new(), Vec::new(), body.sig().clone()), + 0, + )]) +} + +fn local(name: &str, ty: Type) -> TypedValue { + value( + ty, + TypedValueKind::Var { + name: Sym::new(name), + instantiation: Vec::new(), + }, + ) +} + +#[test] +fn accepts_a_closed_well_typed_program() { + let body = return_value(value(Type::Int, TypedValueKind::Int(42))); + let _core = verify(function::(&body), &VerifyEnv::new()) + .expect("closed well-typed fixture must mint elaborated authority"); +} + +#[test] +fn case_arms_may_widen_latent_effect_rows_but_not_narrow_them() { + let row_name = Sym::new("e"); + let closure = |effects| { + CoreType::Thunk(Box::new(pure(CoreType::Function(Box::new( + CoreFnSig::new( + Vec::new(), + vec![source(Type::U64)], + CompSig::new(source(Type::Int), effects), + ), + ))))) + }; + let pure_closure = closure(EffRow::Empty); + let open_closure = closure(EffRow::Var(row_name)); + let program = |arm_ty: CoreType, result_ty: CoreType| { + let choice = TypedBinder::new(Sym::new("choice"), source(Type::Bool)); + let selected = TypedBinder::new(Sym::new("selected"), arm_ty.clone()); + let arm_value = TypedValue::new( + arm_ty.clone(), + TypedValueKind::Var { + name: selected.name(), + instantiation: Vec::new(), + }, + ); + let body = TypedComp::new( + pure(result_ty.clone()), + TypedCompKind::Case( + TypedValue::new( + choice.ty().clone(), + TypedValueKind::Var { + name: choice.name(), + instantiation: Vec::new(), + }, + ), + vec![(TypedPattern::Wild, return_value(arm_value))], + ), + ); + UncheckedTypedCore::::new(vec![TypedCoreFn::new( + Sym::new("main"), + vec![choice, selected], + body, + CoreFnSig::new( + vec![CoreQuantifier::Row(row_name)], + vec![source(Type::Bool), arm_ty], + pure(result_ty), + ), + 0, + )]) + }; + + let _core = verify( + program(pure_closure.clone(), open_closure.clone()), + &VerifyEnv::new(), + ) + .expect("widening latent effects must mint elaborated authority"); + let errors = verify(program(open_closure, pure_closure), &VerifyEnv::new()).unwrap_err(); + assert!(errors.iter().any(|error| { + error.path().ends_with("body.arm[0]") + && matches!( + error.kind(), + Violation::TypeMismatch { + relation: TypeRelation::Subtype, + expected: CoreType::Thunk(_), + .. + } + ) + })); +} + +#[test] +fn rc_sequence_witness_is_confined_to_administrative_owned_binds() { + let unit = source(Type::Unit); + let unit_value = || value(Type::Unit, TypedValueKind::Unit); + // The operation acts on a reference, so every fixture below owns one: + // `held` is bound around the administrative bind under test. + let held = || local("held", Type::Unit); + let owning = |rest: TypedComp| { + TypedComp::new( + rest.sig().clone(), + TypedCompKind::Bind( + Box::new(return_value(unit_value())), + TypedBinder::new(Sym::new("held"), unit.clone()), + Box::new(rest), + ), + ) + }; + let sequence = |binder: TypedBinder, rest: TypedComp| { + owning(TypedComp::new( + rest.sig().clone(), + TypedCompKind::Bind( + Box::new(TypedComp::new( + pure(unit.clone()), + TypedCompKind::Dup(held()), + )), + binder, + Box::new(rest), + ), + )) + }; + + let valid = sequence(TypedBinder::rc_sequence(), return_value(unit_value())); + let valid_core = function::(&valid); + let valid_core = + verify(valid_core, &VerifyEnv::new()).expect("valid RC sequence must mint owned authority"); + let Comp::Bind(_, _, administrative) = &valid_core.erase().fns[0].body else { + panic!("expected the owning bind"); + }; + let Comp::Bind(_, erased_binder, _) = &**administrative else { + panic!("expected erased administrative bind"); + }; + assert_eq!(erased_binder.as_str(), "_"); + + // A retain on a value that reads no binding has no owner to act on, so + // the operation stands on nothing a later consumer could ask about. + let built_operand = owning(TypedComp::new( + pure(unit.clone()), + TypedCompKind::Bind( + Box::new(TypedComp::new( + pure(unit.clone()), + TypedCompKind::Dup(unit_value()), + )), + TypedBinder::rc_sequence(), + Box::new(return_value(unit_value())), + ), + )); + let errors = verify(function::(&built_operand), &VerifyEnv::new()).unwrap_err(); + assert!(errors.iter().any( + |error| error.kind() == &Violation::RcSequence(RcSequenceFault::OperandIsNotAReference) + )); + + let too_early = sequence(TypedBinder::rc_sequence(), return_value(unit_value())); + let errors = verify(function::(&too_early), &VerifyEnv::new()).unwrap_err(); + assert!(errors.iter().any(is_illegal_in::)); + + let ordinary_first = owning(TypedComp::new( + pure(unit.clone()), + TypedCompKind::Bind( + Box::new(return_value(unit_value())), + TypedBinder::rc_sequence(), + Box::new(return_value(unit_value())), + ), + )); + let errors = verify(function::(&ordinary_first), &VerifyEnv::new()).unwrap_err(); + assert!(errors + .iter() + .any(|error| error.kind() == &Violation::RcSequence(RcSequenceFault::NotADupOrDrop))); + + let missing_witness = sequence( + TypedBinder::new(Sym::new(names::RC_SEQUENCE_BINDER), unit.clone()), + return_value(unit_value()), + ); + let errors = verify(function::(&missing_witness), &VerifyEnv::new()).unwrap_err(); + assert!(errors.iter().any( + |error| error.kind() == &Violation::RcSequence(RcSequenceFault::MissingErasureWitness) + )); + + let wrong_name = sequence( + TypedBinder { + name: Sym::new("wrong"), + ty: unit.clone(), + erasure: BinderErasure::RcSequence, + }, + return_value(unit_value()), + ); + let errors = verify(function::(&wrong_name), &VerifyEnv::new()).unwrap_err(); + assert!(errors.iter().any( + |error| error.kind() == &Violation::RcSequence(RcSequenceFault::WrongReservedIdentity) + )); + + let wrong_type = sequence( + TypedBinder { + name: Sym::new(names::RC_SEQUENCE_BINDER), + ty: source(Type::Int), + erasure: BinderErasure::RcSequence, + }, + return_value(unit_value()), + ); + let errors = verify(function::(&wrong_type), &VerifyEnv::new()).unwrap_err(); + assert!(errors.iter().any(|error| matches!( + error.kind(), + Violation::TypeMismatch { + site: Site::At(SITE_RC_SEQUENCE_WITNESS), + .. + } + ))); + + let lambda_body = return_value(unit_value()); + let lambda = TypedComp::new( + pure(CoreType::Function(Box::new(CoreFnSig::new( + Vec::new(), + vec![unit.clone()], + lambda_body.sig().clone(), + )))), + TypedCompKind::Lam(vec![TypedBinder::rc_sequence()], Box::new(lambda_body)), + ); + let errors = verify(function::(&lambda), &VerifyEnv::new()).unwrap_err(); + assert!(errors + .iter() + .any(|error| error.kind() + == &Violation::RcSequence(RcSequenceFault::OutsideAdministrativeBind))); + + let parameter_body = return_value(unit_value()); + let parameter_core = UncheckedTypedCore::::new(vec![TypedCoreFn::new( + Sym::new("parameter"), + vec![TypedBinder::rc_sequence()], + parameter_body.clone(), + CoreFnSig::new(Vec::new(), vec![unit.clone()], parameter_body.sig().clone()), + 0, + )]); + let errors = verify(parameter_core, &VerifyEnv::new()).unwrap_err(); + assert!(errors + .iter() + .any(|error| error.kind() + == &Violation::RcSequence(RcSequenceFault::OutsideAdministrativeBind))); + + let dangling = TypedValue::new( + unit.clone(), + TypedValueKind::Var { + name: Sym::new(names::RC_SEQUENCE_BINDER), + instantiation: Vec::new(), + }, + ); + let referenced = sequence(TypedBinder::rc_sequence(), return_value(dangling)); + let errors = verify(function::(&referenced), &VerifyEnv::new()).unwrap_err(); + assert!(errors + .iter() + .any(|error| matches!(error.kind(), Violation::UnboundReference { .. }))); +} + +#[test] +fn rc_operands_must_be_countable_values() { + // A `dup` of a parameter, wrapped in the administrative bind the Owned + // phase requires, with only the parameter's type varying between cases. + let dup_of = |operand_ty: CoreType| { + let held = TypedBinder::new(Sym::new("held"), operand_ty.clone()); + let operand = TypedValue::new( + operand_ty.clone(), + TypedValueKind::Var { + name: held.name(), + instantiation: Vec::new(), + }, + ); + let rest = return_value(value(Type::Unit, TypedValueKind::Unit)); + let body = TypedComp::new( + rest.sig().clone(), + TypedCompKind::Bind( + Box::new(TypedComp::new( + pure(source(Type::Unit)), + TypedCompKind::Dup(operand), + )), + TypedBinder::rc_sequence(), + Box::new(rest), + ), + ); + UncheckedTypedCore::::new(vec![TypedCoreFn::new( + Sym::new("main"), + vec![held], + body.clone(), + CoreFnSig::new(Vec::new(), vec![operand_ty], body.sig().clone()), + 0, + )]) + }; + + // A linear reuse token is never counted. + let errors = verify( + dup_of(CoreType::ReuseToken(Box::new(source(Type::Int)))), + &VerifyEnv::new(), + ) + .unwrap_err(); + assert!(errors + .iter() + .any(|error| error.kind() == &Violation::RcOperand(RcOperandFault::ReuseToken))); + + // An effect row has no runtime value representation to count. + let errors = verify(dup_of(source(Type::Row(EffRow::Empty))), &VerifyEnv::new()).unwrap_err(); + assert!(errors + .iter() + .any(|error| error.kind() == &Violation::RcOperand(RcOperandFault::NotAValue))); + + // A nominal without declaration evidence is a runtime word whose count + // the tag bit decides dynamically, so it stays accepted. + let bare_nominal = dup_of(source(Type::Con(Sym::new("Box"), Vec::new()))); + verify(bare_nominal, &VerifyEnv::new()) + .expect("a nominal without boxing evidence must stay countable"); + + // The same nominal with boxing evidence is an allocated cell: also counted. + let mut env = VerifyEnv::new(); + env.mark_boxed_nominal(Sym::new("Box")); + verify(dup_of(source(Type::Con(Sym::new("Box"), Vec::new()))), &env) + .expect("a boxed nominal must stay countable"); +} + +#[test] +fn rejects_a_drifting_literal_witness() { + let body = return_value(value(Type::Bool, TypedValueKind::Int(42))); + let errors = verify(function::(&body), &VerifyEnv::new()).unwrap_err(); + assert!(errors.iter().any(|error| matches!( + error.kind(), + Violation::LiteralWitness { + site: Site::At(SITE_INTEGER_LITERAL), + .. + } + ))); +} + +#[test] +fn rejects_effect_row_drift() { + let body = TypedComp::new( + pure(source(Type::Int)), + TypedCompKind::Io(IoOp::ReadInt, Vec::new()), + ); + let errors = verify(function::(&body), &VerifyEnv::new()).unwrap_err(); + assert!(errors.iter().any(|error| matches!( + error.kind(), + Violation::RowMismatch { + site: Site::At(SITE_IO_OPERATION), + .. + } + ))); +} + +#[test] +fn accepts_error_with_arbitrary_well_formed_inherited_witnesses() { + let inherited = fatal_error(CompSig::new( + source(Type::Bool), + EffRow::singleton(prism_syntax::names::IO_EFFECT), + )); + let _core = verify(function::(&inherited), &VerifyEnv::new()) + .expect("well-formed inherited error witnesses must mint authority"); +} + +#[test] +fn rejects_error_with_an_unbound_result_type_witness() { + let unbound_result = Sym::new("unbound_error_result"); + let bad_result = fatal_error(pure(source(Type::Var(unbound_result)))); + let bad_result_core = UncheckedTypedCore::::new(vec![TypedCoreFn::new( + Sym::new("bad_result"), + Vec::new(), + bad_result, + CoreFnSig::new(Vec::new(), Vec::new(), pure(source(Type::Unit))), + 0, + )]); + let errors = verify(bad_result_core, &VerifyEnv::new()).unwrap_err(); + assert!(errors.iter().any(|error| { + error.kind() + == &Violation::UnboundRigid { + kind: QuantifierKind::Type, + name: unbound_result, + ty: Type::Var(unbound_result), + } + })); +} + +#[test] +fn rejects_error_with_an_unbound_effect_row_witness() { + let unbound_effects = Sym::new("unbound_error_effects"); + let bad_effects = fatal_error(CompSig::new( + source(Type::Unit), + EffRow::Var(unbound_effects), + )); + let bad_effects_core = UncheckedTypedCore::::new(vec![TypedCoreFn::new( + Sym::new("bad_effects"), + Vec::new(), + bad_effects, + CoreFnSig::new(Vec::new(), Vec::new(), pure(source(Type::Unit))), + 0, + )]); + let errors = verify(bad_effects_core, &VerifyEnv::new()).unwrap_err(); + assert!(errors.iter().any(|error| { + error.kind() + == &Violation::UnboundRigidRow { + name: unbound_effects, + } + })); +} + +#[test] +fn rejects_a_bind_that_hides_a_child_effect() { + let unit = source(Type::Unit); + let io = TypedComp::new( + CompSig::new( + unit.clone(), + EffRow::singleton(prism_syntax::names::IO_EFFECT), + ), + TypedCompKind::Io(IoOp::PrintNl, Vec::new()), + ); + let rest = return_value(value(Type::Unit, TypedValueKind::Unit)); + let hidden = TypedComp::new( + pure(unit.clone()), + TypedCompKind::Bind( + Box::new(io), + TypedBinder::new(Sym::new("ignored"), unit), + Box::new(rest), + ), + ); + let errors = verify(function::(&hidden), &VerifyEnv::new()).unwrap_err(); + assert!(errors.iter().any(|error| matches!( + error.kind(), + Violation::RowMismatch { + relation: RowRelation::Includes, + expected, + .. + } if expected == &EffRow::singleton(IO_EFFECT) + ))); +} + +#[test] +fn rejects_unknown_references_and_duplicate_binders() { + let binder = TypedBinder::new(Sym::new("x"), source(Type::Int)); + let unknown = value( + Type::Int, + TypedValueKind::Var { + name: Sym::new("missing"), + instantiation: Vec::new(), + }, + ); + let lambda_body = return_value(unknown); + let lambda_sig = CoreFnSig::new( + Vec::new(), + vec![source(Type::Int), source(Type::Int)], + lambda_body.sig().clone(), + ); + let body = TypedComp::new( + pure(CoreType::Function(Box::new(lambda_sig))), + TypedCompKind::Lam(vec![binder.clone(), binder], Box::new(lambda_body)), + ); + let errors = verify(function::(&body), &VerifyEnv::new()).unwrap_err(); + assert!(errors + .iter() + .any(|error| matches!(error.kind(), Violation::DuplicateBinder { .. }))); + assert!(errors + .iter() + .any(|error| matches!(error.kind(), Violation::UnboundReference { .. }))); +} + +#[test] +fn checks_explicit_polymorphic_call_instantiation() { + let type_parameter = Sym::new("a"); + let parameter = TypedBinder::new(Sym::new("x"), source(Type::Var(type_parameter))); + let id_body = return_value(local("x", Type::Var(type_parameter))); + let id = TypedCoreFn::new( + Sym::new("id"), + vec![parameter], + id_body.clone(), + CoreFnSig::new( + vec![CoreQuantifier::Type(type_parameter)], + vec![source(Type::Var(type_parameter))], + id_body.sig().clone(), + ), + 0, + ); + let call = TypedComp::new( + pure(source(Type::Int)), + TypedCompKind::Call { + callee: Sym::new("id"), + instantiation: vec![CoreInstantiation::Type(Type::Int)], + args: vec![value(Type::Int, TypedValueKind::Int(1))], + }, + ); + let main = TypedCoreFn::new( + Sym::new("main"), + Vec::new(), + call.clone(), + CoreFnSig::new(Vec::new(), Vec::new(), call.sig().clone()), + 0, + ); + let core = UncheckedTypedCore::::new(vec![id.clone(), main]); + let _core = verify(core, &VerifyEnv::new()) + .expect("well-kinded explicit instantiation must mint authority"); + + let bad_call = TypedComp::new( + pure(source(Type::Int)), + TypedCompKind::Call { + callee: Sym::new("id"), + instantiation: vec![CoreInstantiation::Row(EffRow::Empty)], + args: vec![value(Type::Int, TypedValueKind::Int(1))], + }, + ); + let bad_main = TypedCoreFn::new( + Sym::new("main"), + Vec::new(), + bad_call.clone(), + CoreFnSig::new(Vec::new(), Vec::new(), bad_call.sig().clone()), + 0, + ); + let errors = verify( + UncheckedTypedCore::::new(vec![id, bad_main]), + &VerifyEnv::new(), + ) + .unwrap_err(); + assert!(errors.iter().any(|error| matches!( + error.kind(), + Violation::Instantiation { + error: InstantiationError::Kind { .. }, + .. + } + ))); +} + +#[test] +fn rejects_constructor_tag_and_field_drift() { + let parameter = Sym::new("a"); + let mut env = VerifyEnv::new(); + env.insert_constructor( + Sym::new("Some"), + ConstructorSig::new( + vec![CoreQuantifier::Type(parameter)], + 7, + vec![source(Type::Var(parameter))], + source(Type::Con(Sym::new("Option"), vec![Type::Var(parameter)])), + ), + ); + let option_int = Type::Con(Sym::new("Option"), vec![Type::Int]); + let constructor = TypedValue::new( + source(option_int), + TypedValueKind::Ctor { + name: Sym::new("Some"), + tag: 8, + instantiation: vec![CoreInstantiation::Type(Type::Int)], + fields: vec![value(Type::Bool, TypedValueKind::Bool(true))], + }, + ); + let errors = verify(function::(&return_value(constructor)), &env).unwrap_err(); + assert!(errors + .iter() + .any(|error| matches!(error.kind(), Violation::ConstructorTag { declared: 7, .. }))); + assert!(errors.iter().any(|error| matches!( + error.kind(), + Violation::TypeMismatch { + site: Site::At(SITE_CONSTRUCTOR_FIELD), + .. + } + ))); +} + +#[test] +fn checks_handler_residual_rows_and_resumption_type() { + let operation_name = Sym::new("get"); + let effect_name = Sym::new("State"); + let mut env = VerifyEnv::new(); + env.insert_operation( + operation_name, + OperationSig::new( + Vec::new(), + Vec::new(), + source(Type::Int), + Label::bare(effect_name), + ), + ); + let handled = TypedComp::new( + CompSig::new(source(Type::Int), EffRow::singleton(effect_name)), + TypedCompKind::Do { + operation: operation_name, + instantiation: Vec::new(), + args: Vec::new(), + }, + ); + let outer = pure(source(Type::Int)); + let resume = TypedBinder::new( + Sym::new("resume"), + CoreType::Thunk(Box::new(pure(CoreType::Function(Box::new( + CoreFnSig::new(Vec::new(), vec![source(Type::Int)], outer.clone()), + ))))), + ); + let arm = TypedHandleOp::new( + operation_name, + Vec::new(), + Vec::new(), + resume, + return_value(value(Type::Int, TypedValueKind::Int(0))), + ); + let clauses = TypedHandler::new(vec![arm]).unwrap(); + let body = TypedComp::new( + outer, + TypedCompKind::Handle { + body: Box::new(handled.clone()), + return_binder: None, + return_body: None, + ops: clauses, + }, + ); + let _core = verify(function::(&body), &env) + .expect("fully handled effects must mint elaborated authority"); + + env.insert_operation( + Sym::new("put"), + OperationSig::new( + Vec::new(), + vec![source(Type::Int)], + source(Type::Unit), + Label::bare(effect_name), + ), + ); + let residual = CompSig::new(source(Type::Int), EffRow::singleton(effect_name)); + let resume = TypedBinder::new( + Sym::new("resume_partial"), + CoreType::Thunk(Box::new(pure(CoreType::Function(Box::new( + CoreFnSig::new(Vec::new(), vec![source(Type::Int)], residual.clone()), + ))))), + ); + let arm = TypedHandleOp::new( + operation_name, + Vec::new(), + Vec::new(), + resume, + return_value(value(Type::Int, TypedValueKind::Int(0))), + ); + let partial = TypedComp::new( + residual, + TypedCompKind::Handle { + body: Box::new(handled), + return_binder: None, + return_body: None, + ops: TypedHandler::new(vec![arm]) + .unwrap() + .with_forwarded(vec![TypedForward::new( + Sym::new("put"), + Label::bare(effect_name), + )]), + }, + ); + let _core = verify(function::(&partial), &env) + .expect("forwarded residual effects must mint elaborated authority"); +} + +#[test] +fn rejects_nodes_outside_their_phase() { + let integer = value(Type::Int, TypedValueKind::Int(1)); + let ref_new = TypedComp::new( + pure(CoreType::Ref(Box::new(source(Type::Int)))), + TypedCompKind::RefNew(integer), + ); + let elaborated_errors = + verify(function::(&ref_new), &VerifyEnv::new()).unwrap_err(); + assert!(elaborated_errors.iter().any(is_illegal_in::)); + + let returned = return_value(value(Type::Int, TypedValueKind::Int(1))); + let mask = TypedComp::new( + returned.sig().clone(), + TypedCompKind::Mask(Vec::new(), Box::new(returned)), + ); + let lowered_errors = verify(function::(&mask), &VerifyEnv::new()).unwrap_err(); + assert!(lowered_errors.iter().any(is_illegal_in::)); +} + +// `init_at` is the proof that a cell an allocator handed out now holds a +// constructor. Each premise of that claim is independent, so each is pinned: +// the phase it may appear in, that the cell is the declared `alloc` result, +// that the payload is something a cell can hold, and that the node's own +// witness is the constructor's. +#[test] +fn init_at_checks_every_premise_of_its_claim() { + let boxed = Type::Con(Sym::new("Boxed"), Vec::new()); + let cell = Type::Con(Sym::new("Arena.Cell"), Vec::new()); + let mut env = VerifyEnv::new(); + env.insert_constructor( + Sym::new("Boxed"), + ConstructorSig::new(Vec::new(), 0, Vec::new(), source(boxed.clone())), + ); + env.insert_operation( + Sym::new(ALLOC_OP), + OperationSig::new( + Vec::new(), + vec![source(Type::Int)], + source(cell.clone()), + Label::bare(ALLOC_EFFECT), + ), + ); + let ctor = || { + TypedValue::new( + source(boxed.clone()), + TypedValueKind::Ctor { + name: Sym::new("Boxed"), + tag: 0, + instantiation: Vec::new(), + fields: Vec::new(), + }, + ) + }; + let init_at = |cell_value: TypedValue, payload: TypedValue, result: Type| { + TypedComp::new( + pure(source(result)), + TypedCompKind::InitAt(cell_value, payload), + ) + }; + let good = || init_at(local("c", cell.clone()), ctor(), boxed.clone()); + let in_scope = |body: &TypedComp| { + TypedComp::new( + CompSig::new(body.sig().result().clone(), EffRow::singleton(ALLOC_EFFECT)), + TypedCompKind::Bind( + Box::new(TypedComp::new( + CompSig::new(source(cell.clone()), EffRow::singleton(ALLOC_EFFECT)), + TypedCompKind::Do { + operation: Sym::new(ALLOC_OP), + instantiation: Vec::new(), + args: vec![value(Type::Int, TypedValueKind::Int(0))], + }, + )), + TypedBinder::new(Sym::new("c"), source(cell.clone())), + Box::new(body.clone()), + ), + ) + }; + + // Legal once an arena has been prepared, and never before. + let _core = verify(function::(&in_scope(&good())), &env) + .expect("valid init-at must mint arena-prepared authority"); + let too_early = verify(function::(&in_scope(&good())), &env).unwrap_err(); + assert!(too_early.iter().any(is_illegal_in::)); + + // The cell must be what this allocator hands out. + let wrong_cell = in_scope(&init_at(local("c", Type::Int), ctor(), boxed.clone())); + let errors = verify(function::(&wrong_cell), &env).unwrap_err(); + assert!(errors.iter().any(|error| matches!( + error.kind(), + Violation::TypeMismatch { + site: Site::At(SITE_INIT_AT_CELL), + .. + } + ))); + + // A cell holds a constructor, not an arbitrary value. + let not_a_ctor = in_scope(&init_at( + local("c", cell.clone()), + value(Type::Int, TypedValueKind::Int(1)), + Type::Int, + )); + let errors = verify(function::(¬_a_ctor), &env).unwrap_err(); + assert!(errors + .iter() + .any(|error| error.kind() == &Violation::InitAtPayloadIsNotAllocation)); + + // The node returns the constructor it wrote, purely. + let drifting = in_scope(&init_at(local("c", cell.clone()), ctor(), Type::Int)); + let errors = verify(function::(&drifting), &env).unwrap_err(); + assert!(errors.iter().any(|error| matches!( + error.kind(), + Violation::TypeMismatch { + site: Site::At(SITE_INIT_AT), + .. + } + ))); +} + +#[test] +fn reference_count_operations_return_unit() { + let dup = TypedComp::new( + pure(source(Type::Unit)), + TypedCompKind::Dup(value(Type::Int, TypedValueKind::Int(1))), + ); + let _core = verify(function::(&dup), &VerifyEnv::new()) + .expect("unit-returning dup must mint owned authority"); + + let drifting = TypedComp::new( + pure(source(Type::Int)), + TypedCompKind::Dup(value(Type::Int, TypedValueKind::Int(1))), + ); + let errors = verify(function::(&drifting), &VerifyEnv::new()).unwrap_err(); + assert!(errors.iter().any(|error| matches!( + error.kind(), + Violation::TypeMismatch { + site: Site::At(SITE_DUP), + .. + } + ))); +} + +#[test] +fn row_instantiation_recanonicalizes_duplicate_labels() { + let row_parameter = Sym::new("e"); + let signature = CoreFnSig::new( + vec![CoreQuantifier::Row(row_parameter)], + Vec::new(), + CompSig::new( + source(Type::Unit), + EffRow::Extend(Label::bare(IO_EFFECT), Box::new(EffRow::Var(row_parameter))), + ), + ); + let instantiated = instantiate_fn( + &signature, + &[CoreInstantiation::Row(EffRow::singleton(IO_EFFECT))], + ) + .unwrap(); + assert_eq!(instantiated.body().effects(), &EffRow::singleton(IO_EFFECT)); +} + +#[test] +fn scheme_instantiation_is_simultaneous() { + let first = Sym::new("a"); + let second = Sym::new("b"); + let signature = CoreFnSig::new( + vec![CoreQuantifier::Type(first), CoreQuantifier::Type(second)], + vec![source(Type::Var(first))], + pure(source(Type::Var(second))), + ); + let instantiated = instantiate_fn( + &signature, + &[ + CoreInstantiation::Type(Type::Var(second)), + CoreInstantiation::Type(Type::Int), + ], + ) + .unwrap(); + assert_eq!(instantiated.params(), &[source(Type::Var(second))]); + assert_eq!(instantiated.body().result(), &source(Type::Int)); +} + +#[test] +fn canonical_builtin_signatures_are_checked_without_inference() { + let sqrt = TypedComp::new( + pure(source(Type::Float)), + TypedCompKind::FloatBuiltin( + FloatOp::Sqrt, + value(Type::Float, TypedValueKind::Float(4.0)), + ), + ); + let _core = verify(function::(&sqrt), &VerifyEnv::new()) + .expect("canonical float builtin must mint elaborated authority"); + + let array_int = Type::Con(Sym::new("Array"), vec![Type::Int]); + let get = TypedComp::new( + pure(source(Type::Int)), + TypedCompKind::StrBuiltin { + op: Builtin::ArrayGet, + instantiation: vec![CoreInstantiation::Type(Type::Int)], + args: vec![ + local("array", array_int.clone()), + value(Type::Int, TypedValueKind::Int(0)), + ], + }, + ); + let array = TypedBinder::new(Sym::new("array"), source(array_int.clone())); + let core = UncheckedTypedCore::::new(vec![TypedCoreFn::new( + Sym::new("main"), + vec![array], + get.clone(), + CoreFnSig::new(Vec::new(), vec![source(array_int)], get.sig().clone()), + 0, + )]); + let _core = verify(core, &VerifyEnv::new()) + .expect("canonical array builtin must mint elaborated authority"); +} + +#[test] +fn reuse_credit_must_be_consumed_once_on_every_branch() { + let boxed = Type::Con(Sym::new("Boxed"), Vec::new()); + let mut env = VerifyEnv::new(); + env.insert_constructor( + Sym::new("Boxed"), + ConstructorSig::new(Vec::new(), 0, Vec::new(), source(boxed.clone())), + ); + let old = TypedBinder::new(Sym::new("old"), source(boxed.clone())); + let token = TypedBinder::new( + Sym::new("token"), + CoreType::ReuseToken(Box::new(source(boxed.clone()))), + ); + let rebuild = || { + TypedValue::new( + source(boxed.clone()), + TypedValueKind::Ctor { + name: Sym::new("Boxed"), + tag: 0, + instantiation: Vec::new(), + fields: Vec::new(), + }, + ) + }; + let reuse = || { + TypedComp::new( + pure(source(boxed.clone())), + TypedCompKind::Reuse(token.clone(), rebuild()), + ) + }; + let branches = TypedComp::new( + pure(source(boxed.clone())), + TypedCompKind::If( + value(Type::Bool, TypedValueKind::Bool(true)), + Box::new(reuse()), + Box::new(reuse()), + ), + ); + let body = TypedComp::new( + branches.sig().clone(), + TypedCompKind::WithReuse { + token: token.clone(), + freed: local("old", boxed.clone()), + body: Box::new(branches), + }, + ); + let make_program = |body: TypedComp| { + let body = TypedComp::new( + body.sig().clone(), + TypedCompKind::Case( + local("old", boxed.clone()), + vec![( + TypedPattern::Ctor { + name: Sym::new("Boxed"), + instantiation: Vec::new(), + fields: Vec::new(), + }, + body, + )], + ), + ); + UncheckedTypedCore::::new(vec![TypedCoreFn::new( + Sym::new("main"), + vec![old.clone()], + body.clone(), + CoreFnSig::new(Vec::new(), vec![source(boxed.clone())], body.sig().clone()), + 0, + )]) + }; + let _core = verify(make_program(body), &env) + .expect("balanced reuse credits must mint reuse-lowered authority"); + + let unbalanced = TypedComp::new( + pure(source(boxed.clone())), + TypedCompKind::If( + value(Type::Bool, TypedValueKind::Bool(true)), + Box::new(reuse()), + Box::new(return_value(rebuild())), + ), + ); + let unbalanced = TypedComp::new( + unbalanced.sig().clone(), + TypedCompKind::WithReuse { + token, + freed: local("old", boxed.clone()), + body: Box::new(unbalanced), + }, + ); + let errors = verify(make_program(unbalanced), &env).unwrap_err(); + assert!(errors.iter().any(|error| matches!( + error.kind(), + Violation::Reuse(ReuseFault::UnequalCredits(_)) + ))); +} + +// The wired nullable frees no cell when matched, so an arm on its +// constructors supplies no shell authority to a with-reuse claim. +#[test] +fn or_null_arm_grants_no_reuse_shell() { + let element = Type::Int; + let or_null = Type::OrNull(Box::new(element.clone())); + let boxed = Type::Con(Sym::new("Boxed"), Vec::new()); + let mut env = VerifyEnv::new(); + env.insert_constructor( + Sym::new("Boxed"), + ConstructorSig::new(Vec::new(), 0, Vec::new(), source(boxed.clone())), + ); + env.insert_constructor( + Sym::from(kw::CTOR_THIS), + ConstructorSig::new( + Vec::new(), + kw::OR_THIS_TAG, + vec![source(element.clone())], + source(or_null.clone()), + ), + ); + let old = TypedBinder::new(Sym::new("old"), source(or_null.clone())); + let token = TypedBinder::new( + Sym::new("token"), + CoreType::ReuseToken(Box::new(source(or_null.clone()))), + ); + let rebuild = value( + boxed.clone(), + TypedValueKind::Ctor { + name: Sym::new("Boxed"), + tag: 0, + instantiation: Vec::new(), + fields: Vec::new(), + }, + ); + let spend = TypedComp::new( + pure(source(boxed)), + TypedCompKind::Reuse(token.clone(), rebuild), + ); + let claim = TypedComp::new( + spend.sig().clone(), + TypedCompKind::WithReuse { + token, + freed: local("old", or_null.clone()), + body: Box::new(spend), + }, + ); + let body = TypedComp::new( + claim.sig().clone(), + TypedCompKind::Case( + local("old", or_null.clone()), + vec![( + TypedPattern::Ctor { + name: Sym::from(kw::CTOR_THIS), + instantiation: Vec::new(), + fields: vec![Some(TypedBinder::new(Sym::new("x"), source(element)))], + }, + claim, + )], + ), + ); + let program = UncheckedTypedCore::::new(vec![TypedCoreFn::new( + Sym::new("main"), + vec![old], + body.clone(), + CoreFnSig::new(Vec::new(), vec![source(or_null)], body.sig().clone()), + 0, + )]); + let errors = verify(program, &env).unwrap_err(); + assert!(errors.iter().any(|error| matches!( + error.kind(), + Violation::Reuse(ReuseFault::ScrutineeNotActive) + ))); +} + +// The wired nullable allocates no cell, so rebuilding one can never spend a +// reuse token, even inside an otherwise valid shell. +#[test] +fn or_null_rebuild_is_not_an_allocation() { + let element = Type::Int; + let or_null = Type::OrNull(Box::new(element.clone())); + let boxed = Type::Con(Sym::new("Boxed"), Vec::new()); + let mut env = VerifyEnv::new(); + env.insert_constructor( + Sym::new("Boxed"), + ConstructorSig::new( + Vec::new(), + 0, + vec![source(element.clone())], + source(boxed.clone()), + ), + ); + env.insert_constructor( + Sym::from(kw::CTOR_THIS), + ConstructorSig::new( + Vec::new(), + kw::OR_THIS_TAG, + vec![source(element.clone())], + source(or_null.clone()), + ), + ); + let old = TypedBinder::new(Sym::new("old"), source(boxed.clone())); + let token = TypedBinder::new( + Sym::new("token"), + CoreType::ReuseToken(Box::new(source(boxed.clone()))), + ); + let rebuild = value( + or_null.clone(), + TypedValueKind::Ctor { + name: Sym::from(kw::CTOR_THIS), + tag: kw::OR_THIS_TAG, + instantiation: Vec::new(), + fields: vec![value(element, TypedValueKind::Int(7))], + }, + ); + let spend = TypedComp::new( + pure(source(or_null)), + TypedCompKind::Reuse(token.clone(), rebuild), + ); + let claim = TypedComp::new( + spend.sig().clone(), + TypedCompKind::WithReuse { + token, + freed: local("old", boxed.clone()), + body: Box::new(spend), + }, + ); + let body = TypedComp::new( + claim.sig().clone(), + TypedCompKind::Case( + local("old", boxed.clone()), + vec![( + TypedPattern::Ctor { + name: Sym::new("Boxed"), + instantiation: Vec::new(), + fields: vec![None], + }, + claim, + )], + ), + ); + let program = UncheckedTypedCore::::new(vec![TypedCoreFn::new( + Sym::new("main"), + vec![old], + body.clone(), + CoreFnSig::new(Vec::new(), vec![source(boxed)], body.sig().clone()), + 0, + )]); + let errors = verify(program, &env).unwrap_err(); + assert!(errors.iter().any(|error| matches!( + error.kind(), + Violation::Reuse(ReuseFault::RebuildIsNotAllocation) + ))); +} + +#[test] +fn polymorphic_function_subtyping_is_alpha_invariant() { + let a = Sym::new("a"); + let renamed = Sym::new("a$typedq0"); + let function = |name| { + CoreType::Function(Box::new(CoreFnSig::new( + vec![CoreQuantifier::Type(name)], + vec![source(Type::Var(name))], + pure(source(Type::Var(name))), + ))) + }; + assert!(core_subtype(&function(a), &function(renamed))); + assert!(core_subtype(&function(renamed), &function(a))); +} + +#[test] +fn alpha_alignment_does_not_capture_a_free_type_variable() { + let bound = Sym::new("bound"); + let other_bound = Sym::new("other_bound"); + let free = Sym::new("free"); + let actual = CoreType::Function(Box::new(CoreFnSig::new( + vec![CoreQuantifier::Type(bound)], + vec![source(Type::Var(bound)), source(Type::Var(free))], + pure(source(Type::Var(bound))), + ))); + let expected = CoreType::Function(Box::new(CoreFnSig::new( + vec![CoreQuantifier::Type(other_bound)], + vec![ + source(Type::Var(other_bound)), + source(Type::Var(other_bound)), + ], + pure(source(Type::Var(other_bound))), + ))); + assert!(!core_subtype(&actual, &expected)); +} diff --git a/crates/prism-core/src/core/typed/violation.rs b/crates/prism-core/src/core/typed/violation.rs new file mode 100644 index 00000000..7351d48e --- /dev/null +++ b/crates/prism-core/src/core/typed/violation.rs @@ -0,0 +1,1569 @@ +//! The named failures of the typed-Core judgments. +//! +//! Every way the typed prefix can refuse a term has a variant here. The proof +//! checker in `super::verify` reports these, the builder in `super::build` +//! reports [`BuildError`], and the substitution helpers both share report the +//! leaf errors ([`InstantiationError`], [`RowUnionError`], [`SchemeError`]). +//! +//! Three properties are the point of naming them. +//! +//! A failure is *classifiable*. A caller that wants to know whether a +//! verification failed because a row was too small or because a node was +//! illegal in its phase can match on the variant. Before, that question could +//! only be answered by substring-matching a sentence, which is a contract no +//! compiler should have with itself: renaming a noun in a message silently +//! changed which failures a downstream test believed it was catching. +//! +//! A failure's *operands are typed*. The types, rows, and names a judgment +//! disagreed about are carried as themselves rather than pre-rendered, so the +//! decision about how to show a type to a person is made once, at the boundary, +//! by the same printers the checker's own diagnostics use. That is what stops a +//! `Debug` dump of an internal signature from reaching a user. +//! +//! A failure is *total*. Adding a way to fail means adding a variant, which the +//! `Display` match then forces the author to give a sentence to. A free-form +//! string had no such obligation, so the wording of a new failure was decided +//! in whichever pass happened to introduce it. + +use std::fmt; + +use prism_common::sym::Sym; + +use crate::core::builtins::Builtin; +use crate::core::typed::{CoreFnSig, CoreType, LoweredType}; +use crate::core::IoOp; +use crate::types::ty::{EffRow, Label}; +use crate::types::Type; + +/// The position a judgment was made at. +/// +/// This names *where* in a node the checker was looking ("bind binder", "if +/// condition", "operation argument"), and pairs with a [`Violation`] variant +/// naming *what* it found wrong. Almost every position is a fixed label, and +/// the `&'static str` is the constraint on those: a site is a compile-time +/// constant chosen by the checker, never assembled from the program under test, +/// so no operand can be smuggled into a position label where it would escape +/// both typing and the structured rendering. The one position that genuinely +/// varies with the program carries its name as a `Sym`, for the same reason. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Site { + /// A fixed position within a node. + At(&'static str), + /// The use of a named local binder. + LocalReference(Sym), +} + +impl fmt::Display for Site { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::At(label) => f.write_str(label), + Self::LocalReference(name) => write!(f, "local reference `{name}`"), + } + } +} + +impl From<&'static str> for Site { + fn from(label: &'static str) -> Self { + Self::At(label) + } +} + +/// Which of the two kinds of Core quantifier a name was bound at. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum QuantifierKind { + /// A type- or natural-kinded variable. + Type, + /// An effect-row-kinded variable. + Row, +} + +impl fmt::Display for QuantifierKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Type => "type", + Self::Row => "row", + }) + } +} + +/// How a counted operand list must relate to the count its witness declares. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ArityRelation { + /// The two counts must be equal. + Exact, + /// The found count must not exceed the declared one. + AtMost, +} + +impl fmt::Display for ArityRelation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Exact => "does not match", + Self::AtMost => "exceeds", + }) + } +} + +/// The declared count an operand list was measured against, named for the +/// witness it was read from. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ArityBound { + /// The enclosing function's parameter list. + Parameter, + /// The function's declared signature. + Signature, + /// The type witness stored on the node. + Witness, + /// The count a declaration fixes (a constructor, an operation). + Declared, + /// The count the checked position expects. + Expected, + /// The scrutinee a pattern is matched against. + Scrutinee, + /// The field capacity of a reuse shell. + ShellCapacity, +} + +impl fmt::Display for ArityBound { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Parameter => "parameter arity", + Self::Signature => "signature arity", + Self::Witness => "witness arity", + Self::Declared => "declared arity", + Self::Expected => "expected arity", + Self::Scrutinee => "scrutinee arity", + Self::ShellCapacity => "shell capacity", + }) + } +} + +/// The syntactic form a position requires of its operand's type. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Form { + /// A suspended computation. + Thunk, + /// A callable closure. + Function, + /// A mutable cell. + Reference, + /// A constructor application or a boxed tuple. + Allocation, +} + +impl fmt::Display for Form { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Thunk => "a thunk", + Self::Function => "a function", + Self::Reference => "a reference", + Self::Allocation => "a constructor or boxed tuple", + }) + } +} + +/// The sort of name a reference failed to resolve to. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NameKind { + /// A data constructor named by a term. + Constructor, + /// A data constructor named by a pattern. + PatternConstructor, + /// A data constructor named by a representation coercion. + CoercionConstructor, + /// A local value reference. + ValueReference, + /// An effect operation a handler clause answers. + HandledOperation, + /// A top-level function named by a direct call. + Function, + /// An effect operation. + Operation, +} + +impl fmt::Display for NameKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Constructor => "unknown constructor", + Self::PatternConstructor => "pattern names unknown constructor", + Self::CoercionConstructor => "representation coercion names unknown constructor", + Self::ValueReference => "unknown value reference", + Self::HandledOperation => "handler names unknown operation", + Self::Function => "call targets unknown function", + Self::Operation => "unknown effect operation", + }) + } +} + +/// How a stored type must relate to the type its position derives. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TypeRelation { + /// The stored type must equal the expected one. + Equal, + /// The stored type may refine the expected one. + Subtype, +} + +/// How a stored effect row must relate to the row its position derives. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RowRelation { + /// The stored row must equal the expected one. + Equal, + /// The stored row must be contained in the expected one. + Subrow, + /// The stored row must contain every derived effect: a node never sheds an + /// effect it observes. + Includes, +} + +/// A failure of the linear reuse-token discipline. +/// +/// A reuse token is the shell of a dead cell, and it must be consumed exactly +/// once on every path that creates it. These are the ways a term can violate +/// that, all of which are unsoundness rather than a typing disagreement: a +/// token consumed twice writes two constructors into one cell. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ReuseFault { + /// A token was mentioned somewhere other than the reuse operand it belongs + /// to, so a use could outlive the shell. + Escapes(Sym), + /// Some path through the body leaves the token unconsumed. + NotConsumedOnce(Sym), + /// One path consumes the token twice. + ConsumedTwice(Sym), + /// The name is bound, but not to a token that is live here. + NotActive(Sym), + /// The name is not bound at this point at all. + OutOfScope(Sym), + /// A closure captured an enclosing token, which would let the shell be + /// consumed once per invocation. + CapturesToken(Site), + /// A closure frees an enclosing shell, so the shell's lifetime is no longer + /// the activation's. + FreesShell(Site), + /// Two arms of a branch leave different numbers of tokens live, so the join + /// has no single credit. + UnequalCredits(Site), + /// A rebuild's payload is not something a shell can hold. + RebuildIsNotAllocation, + /// A `with-reuse` frees a value that is not the case scrutinee whose shell + /// is live here. + ScrutineeNotActive, + /// The live scrutinee's shell was already freed on this path. + ScrutineeFreedTwice, +} + +impl fmt::Display for ReuseFault { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Escapes(name) => { + write!(f, "reuse token {name} escapes its dedicated reuse operand") + } + Self::NotConsumedOnce(name) => write!( + f, + "reuse token {name} is not consumed exactly once on every path" + ), + Self::ConsumedTwice(name) => { + write!( + f, + "reuse token {name} is consumed more than once on one path" + ) + } + Self::NotActive(name) => write!(f, "{name} is not an active reuse token"), + Self::OutOfScope(name) => write!(f, "reuse token {name} is out of scope"), + Self::CapturesToken(site) => { + write!(f, "{site} consumes an enclosing reuse token") + } + Self::FreesShell(site) => write!(f, "{site} frees an enclosing reuse shell"), + Self::UnequalCredits(site) => { + write!(f, "{site} consume different reuse-token credits") + } + Self::RebuildIsNotAllocation => { + write!(f, "reuse rebuild is not a constructor or boxed tuple") + } + Self::ScrutineeNotActive => write!( + f, + "with-reuse does not free the active boxed case scrutinee" + ), + Self::ScrutineeFreedTwice => write!( + f, + "the active boxed case scrutinee is freed more than once on one path" + ), + } + } +} + +/// A reference-count operation acting on a value that cannot be counted. +/// +/// `dup`/`drop` (and the cell a `with-reuse` frees) may act on any value that +/// is one runtime word at run time; the layout authority decides that from the +/// operand's type. These are the operand shapes it refuses. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RcOperandFault { + /// The operand is a linear reuse token, which the count never touches. + ReuseToken, + /// The operand's source type has no runtime value representation at all + /// (an effect row or a type-level natural). + NotAValue, +} + +impl fmt::Display for RcOperandFault { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ReuseToken => write!(f, "RC operation acts on a linear reuse token"), + Self::NotAValue => write!( + f, + "RC operation acts on a type with no runtime value representation" + ), + } + } +} + +/// A failure of the erased-RC-sequencing discipline. +/// +/// After reference counting runs, a `dup`/`drop` pair is sequenced through a +/// binder with a reserved identity, and erasure recognises that identity to +/// drop the bind. These are the ways that agreement can be broken. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RcSequenceFault { + /// The reserved witness appeared on a bind that is not administrative. + OutsideAdministrativeBind, + /// The reserved identity was used without the witness erasure depends on. + MissingErasureWitness, + /// The witness carries an identity other than the reserved one. + WrongReservedIdentity, + /// The witness sequences something other than a `dup` or a `drop`. + NotADupOrDrop, + /// The operation acts on a value that reads no binding. + OperandIsNotAReference, +} + +impl fmt::Display for RcSequenceFault { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::OutsideAdministrativeBind => write!( + f, + "RC sequence witness used outside an administrative dup/drop bind" + ), + Self::MissingErasureWitness => { + write!(f, "reserved RC sequence identity lacks its erasure witness") + } + Self::WrongReservedIdentity => { + write!(f, "RC sequence witness has the wrong reserved identity") + } + Self::NotADupOrDrop => { + write!(f, "RC sequence witness does not sequence a dup or drop") + } + Self::OperandIsNotAReference => { + write!(f, "RC operation acts on a value that reads no binding") + } + } + } +} + +/// A scheme did not peel down to the function type an operation required. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SchemeError { + /// What the scheme's quantifier prefix bottomed out in. + pub found: Type, +} + +impl fmt::Display for SchemeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "expected a function scheme, got {}", self.found.show()) + } +} + +impl std::error::Error for SchemeError {} + +/// Explicit instantiation arguments did not match the scheme they instantiate. +/// +/// Typed Core never searches for an instantiation: a polymorphic use site +/// carries its arguments, and the checker substitutes and compares. So these +/// are the only two ways instantiation can fail, and both are structural. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum InstantiationError { + /// The scheme is not a function scheme, so it quantifies nothing to apply. + Scheme(SchemeError), + /// The argument list and the quantifier prefix differ in length. + Count { + /// Arguments supplied at the use site. + found: usize, + /// Quantifiers the scheme declares. + quantifiers: usize, + }, + /// An argument is of the other kind than the quantifier it fills. + Kind { + /// Position in the argument list. + index: usize, + /// The kind the quantifier at that position demands. + expected: QuantifierKind, + }, +} + +impl fmt::Display for InstantiationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Scheme(error) => write!(f, "{error}"), + Self::Count { found, quantifiers } => write!( + f, + "argument count {found} does not match quantifier count {quantifiers}" + ), + Self::Kind { index, expected } => { + write!(f, "argument {index} is not a {expected} argument") + } + } + } +} + +impl std::error::Error for InstantiationError {} + +impl From for InstantiationError { + fn from(error: SchemeError) -> Self { + Self::Scheme(error) + } +} + +/// Two effect rows have no join the checker can prove. +/// +/// A row union is not a set union: two rows with distinct open tails have a +/// join only if one of the unknown remainders is known to contain the other, +/// and nothing here knows that. The same holds for one effect label carried at +/// two different argument lists. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RowUnionError { + /// The two rows end in distinct open tails. + OpenTails { + /// The left row's tail. + left: EffRow, + /// The right row's tail. + right: EffRow, + }, + /// One effect name occurs on both sides at incompatible arguments. + Labels { + /// The occurrence already absorbed. + left: Label, + /// The occurrence that could not join it. + right: Label, + }, +} + +impl fmt::Display for RowUnionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::OpenTails { left, right } => write!( + f, + "cannot prove union of distinct open tails {} and {}", + left.show(), + right.show() + ), + Self::Labels { left, right } => write!( + f, + "cannot prove union of effect labels {} and {}", + left.show(), + right.show() + ), + } + } +} + +impl std::error::Error for RowUnionError {} + +/// What a polymorphic use site was instantiating. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum InstantiationSubject { + /// A reference to a local binder, named. + Local(Sym), + /// A fixed position in a node. + At(Site), +} + +impl fmt::Display for InstantiationSubject { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Local(name) => write!(f, "local {name}"), + Self::At(site) => write!(f, "{site}"), + } + } +} + +/// One failed typed-Core judgment, named. +/// +/// The variants are grouped by the invariant they belong to rather than by the +/// node that happened to detect them, so that a family (arity, rows, the reuse +/// discipline, phase legality) can be recognised without reading prose. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Violation { + /// One name is bound twice in a single quantifier prefix. + DuplicateQuantifier { + /// Which kind of quantifier bound it. + kind: QuantifierKind, + /// Whether the prefix is an inner (rank-N) one rather than the + /// declaration's own. + nested: bool, + /// The name bound twice. + name: Sym, + }, + /// One binder identity is introduced twice in one binding group, so a + /// reference to it has no unique referent. + DuplicateBinder { + /// The duplicated identity. + name: Sym, + }, + /// Two functions in the program share one global identity, so a call has no + /// unique callee. + DuplicateGlobal, + /// A record term names a field the witness does not name at that position. + RecordField { + /// The name the term carries. + found: Sym, + /// The name the witness fixes. + expected: Sym, + }, + /// A counted operand list disagrees with the count its witness declares. + Arity { + /// What was counted. + counted: Site, + /// How the counts must relate. + relation: ArityRelation, + /// The witness the declared count was read from. + bound: ArityBound, + /// The count found. + found: usize, + /// The count declared. + expected: usize, + }, + /// A newtype constructor is not the single-field constructor a + /// representation coercion requires. + NewtypeFieldCount { + /// The constructor named by the coercion. + constructor: Sym, + /// How many fields it actually declares. + found: usize, + }, + /// A stored type disagrees with the type its position derives. + TypeMismatch { + /// Where the judgment was made. + site: Site, + /// How the two must relate. + relation: TypeRelation, + /// The type stored on the node. + actual: CoreType, + /// The type the position derives. + expected: CoreType, + }, + /// A stored effect row disagrees with the row its position derives. + RowMismatch { + /// Where the judgment was made. + site: Site, + /// How the two must relate. + relation: RowRelation, + /// The row stored on the node. + actual: EffRow, + /// The row the position derives. + expected: EffRow, + }, + /// An operand's type is not the form its position requires. + NotAForm { + /// The operand position. + site: Site, + /// The form required. + expected: Form, + /// The type found instead. + found: CoreType, + }, + /// A literal's stored witness is not one this literal can carry. + LiteralWitness { + /// The literal form. + site: Site, + /// The witness stored on it. + witness: CoreType, + }, + /// A reference resolves to neither a local binder nor a global. + UnboundReference { + /// The unresolved name. + name: Sym, + }, + /// A name does not resolve in the declaration environment. + UnknownName { + /// The sort of name, which also fixes how the failure reads. + kind: NameKind, + /// The unresolved name. + name: Sym, + }, + /// An elaborator-only builtin reached the checker with no signature to + /// check it against. + MissingBuiltinSignature { + /// The builtin's name. + builtin: Sym, + }, + /// A constructor occurrence carries a tag other than the one its + /// declaration fixes, so the runtime match would take a different arm. + ConstructorTag { + /// The constructor. + name: Sym, + /// The tag stored at the occurrence. + found: usize, + /// The tag the declaration fixes. + declared: usize, + }, + /// A product term's shape does not match the product its witness names. + ProductShape { + /// The witness stored on the term. + witness: CoreType, + }, + /// A runtime cell slot holds a type whose boundary layout is not exactly + /// one GC-scanned word. + /// + /// Constructor fields, boxed tuple fields, and closure captures each occupy + /// one word of a heap cell: allocation sizes, field offsets, and reuse + /// capacities are all computed as plain field counts on that assumption. A + /// slot type that widens, vanishes, or admits a non-value bit pattern would + /// silently corrupt every one of those computations, so it is rejected + /// here, where the type is still attached to the slot. + CellSlotNotOneWord { + /// The slot position. + site: Site, + /// The slot's type. + ty: CoreType, + }, + /// An unboxed record term carries a witness that is not a record type. + UnboxedRecordWitness { + /// The witness stored on the term. + witness: CoreType, + }, + /// An error node's payload has a witness the error representation cannot + /// carry. + ErrorArgumentWitness { + /// The witness stored on the payload. + witness: CoreType, + }, + /// A field projection names a field the operand's record does not have. + AbsentField { + /// The projected field. + field: Sym, + /// The operand's record type. + operand: CoreType, + }, + /// An integer-lane comparison was given operands that are not both in that + /// lane, so the primitive has no meaning at these types. + LaneOperands { + /// The left operand's type. + lhs: CoreType, + /// The right operand's type. + rhs: CoreType, + }, + /// A tuple pattern was matched against a scrutinee that is not a product. + TuplePatternScrutinee { + /// The scrutinee's type. + scrutinee: CoreType, + }, + /// A case has no arms, so it denotes no computation. + CaseHasNoArms, + /// An `init-at` appears with no allocation for it to initialise. + InitAtWithoutAlloc, + /// An `init-at` payload is not something a cell can hold. + InitAtPayloadIsNotAllocation, + /// A handler declares one half of its return clause. + HandlerReturnClauseIncomplete, + /// The forwarding a handler stores is not the forwarding its arms derive, + /// so an operation would be forwarded that the handler discharges (or the + /// reverse). + ForwardingMismatch { + /// The forwarding the arms derive. + derived: Vec<(Sym, Label)>, + /// The forwarding stored on the handler. + stored: Vec<(Sym, Label)>, + }, + /// A handler's stored residual row does not cover what its body leaves + /// undischarged together with what its clauses perform. + HandlerResidualRow { + /// The residual the handler derives. + derived: EffRow, + /// The upper bound the handler stores. + stored: EffRow, + }, + /// A representation coercion names a constructor that is not a newtype, so + /// there is no representation identity to appeal to. + NotANewtype { + /// The constructor named. + constructor: Sym, + }, + /// A representation-preserving coercion relates two types with different + /// representations. + ReprCoercionIllegal { + /// The operand's type. + from: CoreType, + /// The result's type. + to: CoreType, + }, + /// A lowered-representation conversion relates two types the effect-runtime + /// ABI does not identify. + ReprConversionIllegal { + /// The operand's type. + from: CoreType, + /// The result's type. + to: CoreType, + }, + /// A newtype coercion's operand and result do not stand at the two ends of + /// the constructor it names. + NewtypeCoercionDisconnected { + /// The constructor named. + constructor: Sym, + /// The constructor's single field type. + field: CoreType, + /// The constructor's result type. + result: CoreType, + /// The operand's type. + inner: CoreType, + /// The result's type. + outer: CoreType, + }, + /// A node or witness appears in a phase whose Core does not admit it. + /// + /// This is what keeps the phases genuinely distinct: each typed pass runs + /// the same checker at a different phase marker, and a node the phase + /// forbids is rejected there rather than surviving to a backend that cannot + /// lower it. + PhaseIllegal { + /// The node or witness form. + what: Site, + /// The phase that forbids it. + phase: &'static str, + }, + /// A failure of the linear reuse-token discipline. + Reuse(ReuseFault), + /// A failure of the erased-RC-sequencing discipline. + RcSequence(RcSequenceFault), + /// A reference-count operation acting on a value that cannot be counted. + RcOperand(RcOperandFault), + /// A polymorphic use site's explicit instantiation does not fit its scheme. + Instantiation { + /// What was being instantiated. + subject: InstantiationSubject, + /// Why it did not fit. + error: InstantiationError, + }, + /// A signature the checker holds for a builtin does not peel down to a + /// function type. + CanonicalSignature { + /// The registry the signature came from. + site: Site, + /// Why it did not peel down. + error: SchemeError, + }, + /// A signature the checker holds for a builtin does not parse. + /// + /// The parse error is rendered here rather than carried: it is a foreign + /// diagnostic type with no value semantics, and this is the one boundary a + /// violation crosses to reach it. + CanonicalSignatureParse { + /// The registry the signature came from. + site: Site, + /// The rendered parse failure. + error: String, + }, + /// Two rows a node must join have no provable union. + RowUnion { + /// The position whose row is the join. + site: Site, + /// Why the join is not provable. + error: RowUnionError, + }, + /// Metavariables survive into a type that must be ground by now. + UnsolvedMeta { + /// Which kind of metavariable survived. + kind: QuantifierKind, + /// The type they survive in. + ty: Type, + }, + /// Metavariables survive into an effect row that must be ground by now. + UnsolvedRowMeta { + /// The row they survive in. + row: EffRow, + }, + /// A type mentions a rigid variable no enclosing quantifier binds. + UnboundRigid { + /// Which kind of variable. + kind: QuantifierKind, + /// The unbound name. + name: Sym, + /// The type mentioning it. + ty: Type, + }, + /// An effect row mentions a rigid tail variable no enclosing quantifier + /// binds. + UnboundRigidRow { + /// The unbound name. + name: Sym, + }, + /// An effect row is not in the canonical form every comparison assumes. + /// + /// Rows are compared structurally, so a row that duplicates a label or + /// orders one differently would compare unequal to a row denoting the same + /// effects, and the checker would reject a valid program. + RowNotCanonical { + /// The row as stored. + row: EffRow, + }, + /// A lowered ABI type appears in a phase before the ABI exists. + LoweredAbiIllegal { + /// The phase that forbids it. + phase: &'static str, + /// The ABI type found. + found: LoweredType, + }, +} + +impl fmt::Display for Violation { + #[allow(clippy::too_many_lines)] // One arm per variant; splitting hides the total. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::DuplicateQuantifier { kind, nested, name } => { + let nested = if *nested { "nested " } else { "" }; + write!(f, "duplicate {nested}{kind} quantifier {name}") + } + Self::DuplicateBinder { name } => { + write!( + f, + "binder identity {name} is duplicated in one binding group" + ) + } + Self::DuplicateGlobal => f.write_str("duplicate global function identity"), + Self::RecordField { found, expected } => write!( + f, + "record field {found} does not match witness field {expected}" + ), + Self::Arity { + counted, + relation, + bound, + found, + expected, + } => write!(f, "{counted} arity {found} {relation} {bound} {expected}"), + Self::NewtypeFieldCount { constructor, found } => write!( + f, + "newtype constructor {constructor} has {found} fields rather than one" + ), + Self::TypeMismatch { + site, + relation, + actual, + expected, + } => { + let relation = match relation { + TypeRelation::Equal => "expected", + TypeRelation::Subtype => "expected a subtype of", + }; + write!( + f, + "{site} type mismatch: stored {actual}, {relation} {expected}" + ) + } + Self::RowMismatch { + site, + relation, + actual, + expected, + } => { + let actual = actual.show(); + let expected = expected.show(); + match relation { + RowRelation::Equal => { + write!( + f, + "{site} row mismatch: stored {actual}, expected {expected}" + ) + } + RowRelation::Subrow => write!( + f, + "{site} row mismatch: stored {actual}, expected a subrow of {expected}" + ), + RowRelation::Includes => write!( + f, + "{site} row mismatch: stored {actual}, does not include derived {expected}" + ), + } + } + Self::NotAForm { + site, + expected, + found, + } => write!(f, "{site} is not {expected}: {found}"), + Self::LiteralWitness { site, witness } => { + write!(f, "{site} has witness {witness}") + } + Self::UnboundReference { name } => { + write!(f, "reference {name} is neither local nor global") + } + Self::UnknownName { kind, name } => write!(f, "{kind} {name}"), + Self::MissingBuiltinSignature { builtin } => write!( + f, + "elaborator-only builtin {builtin} has no verifier signature override" + ), + Self::ConstructorTag { + name, + found, + declared, + } => write!( + f, + "constructor {name} tag {found} does not match declared tag {declared}" + ), + Self::ProductShape { witness } => { + write!(f, "product shape does not match witness {witness}") + } + Self::CellSlotNotOneWord { site, ty } => { + write!(f, "{site} of type {ty} is not one runtime word") + } + Self::UnboxedRecordWitness { witness } => { + write!(f, "unboxed record has non-record witness {witness}") + } + Self::ErrorArgumentWitness { witness } => { + write!(f, "error argument has unsupported witness {witness}") + } + Self::AbsentField { field, operand } => write!( + f, + "field {field} is absent from unboxed-record operand {operand}" + ), + Self::LaneOperands { lhs, rhs } => { + write!(f, "integer-lane comparison has operands {lhs} and {rhs}") + } + Self::TuplePatternScrutinee { scrutinee } => { + write!(f, "tuple pattern has non-product scrutinee {scrutinee}") + } + Self::CaseHasNoArms => write!(f, "case has no arms"), + Self::InitAtWithoutAlloc => { + write!(f, "init-at without a declared alloc operation") + } + Self::InitAtPayloadIsNotAllocation => { + write!(f, "init-at payload is not a constructor or boxed tuple") + } + Self::HandlerReturnClauseIncomplete => write!( + f, + "handler return binder and return body must appear together" + ), + Self::ForwardingMismatch { derived, stored } => write!( + f, + "handler residual-forwarding witness mismatch: derived {}, stored {}", + show_forwarding(derived), + show_forwarding(stored) + ), + Self::HandlerResidualRow { derived, stored } => write!( + f, + "handler residual effects row mismatch: derived {}, stored upper bound {}", + derived.show(), + stored.show() + ), + Self::NotANewtype { constructor } => write!( + f, + "representation coercion names non-newtype constructor {constructor}" + ), + Self::ReprCoercionIllegal { from, to } => write!( + f, + "illegal representation-preserving coercion {from} to {to}" + ), + Self::ReprConversionIllegal { from, to } => write!( + f, + "illegal lowered representation conversion {from} to {to}" + ), + Self::NewtypeCoercionDisconnected { + constructor, + field, + result, + inner, + outer, + } => write!( + f, + "newtype representation coercion for {constructor} does not connect field {field} \ + and result {result}: inner {inner}, outer {outer}" + ), + Self::PhaseIllegal { what, phase } => { + write!(f, "{what} is illegal in {phase} Core") + } + Self::Reuse(fault) => write!(f, "{fault}"), + Self::RcSequence(fault) => write!(f, "{fault}"), + Self::RcOperand(fault) => write!(f, "{fault}"), + Self::Instantiation { subject, error } => { + write!(f, "invalid {subject} instantiation: {error}") + } + Self::CanonicalSignature { site, error } => { + write!(f, "invalid canonical {site} signature: {error}") + } + Self::CanonicalSignatureParse { site, error } => { + write!(f, "cannot parse canonical {site} signature: {error}") + } + Self::RowUnion { site, error } => write!(f, "{site}: {error}"), + Self::UnsolvedMeta { kind, ty } => { + write!(f, "unsolved {kind} metavariables survive in {}", ty.show()) + } + Self::UnsolvedRowMeta { row } => write!( + f, + "unsolved effect-row metavariables survive in {}", + row.show() + ), + Self::UnboundRigid { kind, name, ty } => { + write!(f, "unbound rigid {kind} variable {name} in {}", ty.show()) + } + Self::UnboundRigidRow { name } => { + write!(f, "unbound rigid effect-row variable {name}") + } + Self::RowNotCanonical { row } => { + write!(f, "effect row is not canonical: {}", row.show()) + } + Self::LoweredAbiIllegal { phase, found } => { + write!(f, "lowered ABI type {found} is illegal in {phase} Core") + } + } + } +} + +/// The nouns the solver's failed judgments are phrased over. One home, because +/// the same noun appears under several relations and a second spelling of it +/// would read as a second kind of witness. +const CORE_TYPE: &str = "Core type"; +const SOURCE_TYPE: &str = "source type"; +const FUNCTION_SIGNATURE: &str = "function signature"; +const EFFECT_ROW: &str = "effect row"; +const EFFECT_ROW_TAIL: &str = "effect-row tail"; +const EFFECT_LABEL: &str = "effect label"; + +/// The judgment the Core solver was making when two witnesses disagreed. +/// +/// The solver relates witnesses in exactly these four ways, and which one was +/// being made is what decides whether a failure is a genuine disagreement or +/// merely a direction the checker cannot prove. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SolveRelation { + /// The two witnesses must be made equal. + Unify, + /// The left witness must refine the right one. + Subtype, + /// The left row's effects must all appear in the right row. + Subrow, + /// The two witnesses must have a least upper bound. + Join, +} + +impl SolveRelation { + /// Phrase this relation over one noun and the two operands it related. + fn describe( + self, + f: &mut fmt::Formatter<'_>, + noun: &str, + left: &dyn fmt::Display, + right: &dyn fmt::Display, + ) -> fmt::Result { + match self { + Self::Unify => write!(f, "cannot unify {noun}s {left} and {right}"), + Self::Join => write!(f, "cannot join {noun}s {left} and {right}"), + Self::Subtype => write!(f, "{noun} {left} is not a subtype of {right}"), + Self::Subrow => write!(f, "{noun} {left} is not included in {right}"), + } + } +} + +/// Which substitution table a metavariable belongs to. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MetaKind { + /// A placeholder for a CBPV shape with no source-language spelling. + Core, + /// A placeholder for a source type. + Source, + /// A placeholder for an effect row. + Row, +} + +/// One solver metavariable, named the way the solver's tables name it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct MetaVar { + /// The table it lives in. + pub kind: MetaKind, + /// Its identity within that table. + pub id: u32, +} + +impl fmt::Display for MetaVar { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let id = self.id; + match self.kind { + MetaKind::Core => write!(f, "Core metavariable ?{id}"), + MetaKind::Source => write!(f, "type metavariable ?{id}"), + MetaKind::Row => write!(f, "row metavariable ?r{id}"), + } + } +} + +/// The witness a metavariable was about to be bound to when it was found to +/// contain that same variable. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Within { + /// A Core type reached by unification. + Core(CoreType), + /// A Core type reached by joining two lower bounds. + Joined(CoreType), + /// A source type. + Source(Type), + /// An effect row. + Row(EffRow), +} + +impl fmt::Display for Within { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Core(ty) => write!(f, "{ty}"), + Self::Joined(ty) => write!(f, "joined type {ty}"), + Self::Source(ty) => f.write_str(&ty.show()), + Self::Row(row) => f.write_str(&row.show()), + } + } +} + +/// A failure of the Core solver: two witnesses that could not be related. +/// +/// The solver runs under the builder rather than over the source language, so +/// none of these is a user-facing type error. Each one says that reconstructed +/// evidence disagreed with declared evidence, which is a compiler fault, and +/// naming them is what lets the fault be classified without reading its prose. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum SolveError { + /// A metavariable would have to be bound to a witness containing itself. + Occurs { + /// The variable being solved. + meta: MetaVar, + /// The witness it occurs in. + within: Within, + }, + /// Two Core types do not stand in the required relation. + Core { + /// The judgment that failed. + relation: SolveRelation, + /// The two operands, left and right. + operands: Box<(CoreType, CoreType)>, + }, + /// Two source types cannot be unified. + Source { + /// The two operands, left and right. + operands: Box<(Type, Type)>, + }, + /// Two calling conventions do not stand in the required relation. + Signature { + /// The judgment that failed. + relation: SolveRelation, + /// The two operands, left and right. + operands: Box<(CoreFnSig, CoreFnSig)>, + }, + /// Two effect rows do not stand in the required relation. + Row { + /// The judgment that failed. + relation: SolveRelation, + /// The left operand. + left: EffRow, + /// The right operand. + right: EffRow, + }, + /// Two rows end in open tails that cannot be identified. + RowTails { + /// The left tail. + left: EffRow, + /// The right tail. + right: EffRow, + }, + /// An effect performed on the left is absent from the row on the right. + LabelNotIncluded { + /// The effect that has nowhere to go. + label: Label, + /// The row that does not carry it. + row: EffRow, + }, + /// Two rows have no join the checker can prove. + Union(RowUnionError), + /// Two unboxed records disagree on a field name at the same position. + RecordField { + /// The left field name. + left: Sym, + /// The right field name. + right: Sym, + }, + /// A join widened past the type its position had already fixed. + JoinExceeds { + /// The least upper bound of the observed lower bounds, and the type the + /// position expects. + operands: Box<(CoreType, CoreType)>, + }, + /// A failure found under a named position of a compound witness. + In { + /// Where in the compound the operands were taken from. + site: Site, + /// What went wrong there. + error: Box, + }, +} + +impl SolveError { + /// Record that this failure was found under `site` of a compound witness. + #[must_use] + pub fn at(self, site: impl Into) -> Self { + Self::In { + site: site.into(), + error: Box::new(self), + } + } +} + +impl fmt::Display for SolveError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Occurs { meta, within } => write!(f, "recursive {meta} in {within}"), + Self::Core { relation, operands } => { + relation.describe(f, CORE_TYPE, &operands.0, &operands.1) + } + Self::Source { operands } => SolveRelation::Unify.describe( + f, + SOURCE_TYPE, + &operands.0.show(), + &operands.1.show(), + ), + Self::Signature { relation, operands } => { + relation.describe(f, FUNCTION_SIGNATURE, &operands.0, &operands.1) + } + Self::Row { + relation, + left, + right, + } => relation.describe(f, EFFECT_ROW, &left.show(), &right.show()), + Self::RowTails { left, right } => { + SolveRelation::Unify.describe(f, EFFECT_ROW_TAIL, &left.show(), &right.show()) + } + Self::LabelNotIncluded { label, row } => { + SolveRelation::Subrow.describe(f, EFFECT_LABEL, &label.show(), &row.show()) + } + Self::Union(error) => write!(f, "{error}"), + Self::RecordField { left, right } => { + write!(f, "record field mismatch {left} and {right}") + } + Self::JoinExceeds { operands } => write!( + f, + "joined result {} exceeds expected type {}", + operands.0, operands.1 + ), + Self::In { site, error } => write!(f, "{site}: {error}"), + } + } +} + +impl std::error::Error for SolveError {} + +impl From for SolveError { + fn from(error: RowUnionError) -> Self { + Self::Union(error) + } +} + +/// Which part of a `Bind` a failure was found under. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BindPart { + /// The bound computation. + First, + /// The continuation. + Rest, + /// The continuation, checked against the exact expected signature rather + /// than a relaxed one. + ExactRest, +} + +impl fmt::Display for BindPart { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::First => "first", + Self::Rest => "rest", + Self::ExactRest => "exact rest", + }) + } +} + +/// The position a builder failure was found under. +/// +/// This is the builder's analogue of [`Site`], widened because reconstructing a +/// witness fails inside a term rather than at a fixed slot of a node: the +/// positions that matter carry the binder, the argument index, or the rows a +/// handler derived, and those are what make a report locatable. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum BuildContext { + /// A fixed position within a node. + At(Site), + /// One half of a sequencing node, named by its binder. + Binding { + /// The name bound. + binder: Sym, + /// Which half. + part: BindPart, + }, + /// One argument of a computed application. + Argument { + /// Position in the argument list. + index: usize, + /// The parameter type it was checked against. + expected: CoreType, + }, + /// The body of one handler clause. + HandlerOperationBody(Sym), + /// A handler's derived row against the row its position expects. + HandlerEffects { + /// The row the handler's own clauses derived. + derived: EffRow, + /// The row the enclosing position fixes, if it fixes one. + expected: Option, + }, +} + +impl fmt::Display for BuildContext { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::At(site) => write!(f, "{site}"), + Self::Binding { binder, part } => write!(f, "bind {binder} {part}"), + Self::Argument { index, expected } => { + write!( + f, + "computed application argument {index} against {expected}" + ) + } + Self::HandlerOperationBody(name) => write!(f, "handler operation {name} body"), + Self::HandlerEffects { derived, expected } => { + let expected = expected + .as_ref() + .map_or_else(|| "an unconstrained row".to_string(), EffRow::show); + write!( + f, + "handler effects derived {}, expected {expected}", + derived.show() + ) + } + } + } +} + +impl From<&'static str> for BuildContext { + fn from(label: &'static str) -> Self { + Self::At(Site::At(label)) + } +} + +/// What a miscounted operand list belonged to. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BuildSubject { + /// A data constructor, named. + Constructor(Sym), + /// An application whose callee is a value rather than a known function. + ComputedApplication, + /// A direct call to a top-level function, named. + Call(Sym), + /// A builtin I/O operation. + Io(IoOp), + /// A compiler builtin. + Builtin(Builtin), + /// An effect operation, named. + Operation(Sym), + /// A constructor pattern, named. + Pattern(Sym), + /// A handler clause answering an operation, named. + HandlerOperation(Sym), +} + +impl fmt::Display for BuildSubject { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Constructor(name) => write!(f, "constructor {name}"), + Self::ComputedApplication => f.write_str("computed application"), + Self::Call(name) => write!(f, "call {name}"), + Self::Io(op) => write!(f, "I/O {}", op.kind()), + Self::Builtin(builtin) => write!(f, "builtin {}", builtin.name()), + Self::Operation(name) => write!(f, "operation {name}"), + Self::Pattern(name) => write!(f, "pattern {name}"), + Self::HandlerOperation(name) => write!(f, "handler operation {name}"), + } + } +} + +/// A failure to reconstruct a typed witness at the elaboration boundary. +/// +/// The builder is not an inference pass: every witness it needs is already +/// determined by a checked declaration, so each of these says that the Core the +/// elaborator produced and the schemes the checker recorded describe different +/// programs. That is always a compiler fault, never a user's type error, and +/// naming the faults is what keeps them distinguishable once they are wrapped +/// into a construction failure and rendered. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum BuildError { + /// The solver could not relate two witnesses. + Solve(Box), + /// A declared scheme is not a function scheme. + Scheme(SchemeError), + /// An explicit instantiation did not match the scheme it instantiates. + Instantiation(InstantiationError), + /// A signature the builder itself writes did not parse. + /// + /// The parser's error has no value semantics, so this is the one place the + /// family carries rendered text rather than an operand. + SignatureParse { + /// What the signature was written for. + item: &'static str, + /// The parse failure, rendered. + error: String, + }, + /// A name the term uses is absent from the environment the checker built. + UnknownName { + /// What sort of name failed to resolve. + kind: NameKind, + /// The name. + name: Sym, + }, + /// A builtin the elaborator emitted has no Core calling convention, so it + /// was meant to have been erased before this point. + MissingBuiltinSignature { + /// The builtin with no signature. + builtin: Builtin, + }, + /// A handler carries no return clause at all. + HandlerWithoutResultClause, + /// A Core type has no source-language value type to read back out of it. + NoSourceType { + /// The type with no inverse. + found: CoreType, + }, + /// A constructor use carries a different tag than its declaration. + ConstructorTag { + /// The constructor named. + name: Sym, + /// The tag the term carries. + found: usize, + /// The tag the declaration fixes. + expected: usize, + }, + /// An operand list is the wrong length for what it fills. + Arity { + /// What the operands belong to. + subject: BuildSubject, + /// Operands supplied. + found: usize, + /// Operands the declaration fixes. + expected: usize, + }, + /// A position requires a form its operand's type does not have. + NotAForm { + /// Where the operand sits. + site: Site, + /// The form the position requires. + expected: Form, + /// The type found instead. + found: CoreType, + }, + /// A node that only exists after effect lowering reached the builder. + RuntimeNode, + /// A case with no arms fixes no result type. + CaseWithoutArms, + /// A handler carries half of a return clause. + IncompleteHandlerReturn, + /// Two clauses of one handler answer the same operation. + DuplicateHandlerOperation(Sym), + /// A failure found under a named position of the term. + In { + /// Where in the term the builder was working. + context: BuildContext, + /// What went wrong there. + error: Box, + }, +} + +impl BuildError { + /// Record that this failure was found under `context`. + #[must_use] + pub fn at(self, context: impl Into) -> Self { + Self::In { + context: context.into(), + error: Box::new(self), + } + } +} + +impl fmt::Display for BuildError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Solve(error) => write!(f, "{error}"), + Self::Scheme(error) => write!(f, "{error}"), + Self::Instantiation(error) => write!(f, "{error}"), + Self::SignatureParse { item, error } => { + write!(f, "cannot parse the {item} signature: {error}") + } + Self::UnknownName { kind, name } => write!(f, "{kind} {name}"), + Self::MissingBuiltinSignature { builtin } => write!( + f, + "elaborator-only builtin {} has no signature", + builtin.name() + ), + Self::HandlerWithoutResultClause => f.write_str("handler has no result clause"), + Self::NoSourceType { found } => { + write!(f, "{found} has no source-language value type") + } + Self::ConstructorTag { + name, + found, + expected, + } => write!( + f, + "constructor {name} carries tag {found} rather than {expected}" + ), + Self::Arity { + subject, + found, + expected, + } => write!(f, "{subject} arity {found} does not match {expected}"), + Self::NotAForm { + site, + expected, + found, + } => write!(f, "{site} is not {expected}: {found}"), + Self::RuntimeNode => f.write_str("runtime node reached the typed builder"), + Self::CaseWithoutArms => f.write_str("case has no arms"), + Self::IncompleteHandlerReturn => f.write_str("incomplete handler return clause"), + Self::DuplicateHandlerOperation(name) => { + write!(f, "duplicate handler operation {name}") + } + Self::In { context, error } => write!(f, "{context}: {error}"), + } + } +} + +impl std::error::Error for BuildError {} + +impl From for BuildError { + fn from(error: SolveError) -> Self { + Self::Solve(Box::new(error)) + } +} + +impl From for BuildError { + fn from(error: RowUnionError) -> Self { + Self::Solve(Box::new(SolveError::Union(error))) + } +} + +impl From for BuildError { + fn from(error: SchemeError) -> Self { + Self::Scheme(error) + } +} + +impl From for BuildError { + fn from(error: InstantiationError) -> Self { + Self::Instantiation(error) + } +} + +/// The operation-to-effect pairs a handler forwards, as one readable list. +fn show_forwarding(pairs: &[(Sym, Label)]) -> String { + let shown: Vec = pairs + .iter() + .map(|(operation, effect)| format!("{operation}: {}", effect.show())) + .collect(); + format!("[{}]", shown.join(", ")) +} diff --git a/crates/prism-core/src/core/work.rs b/crates/prism-core/src/core/work.rs new file mode 100644 index 00000000..713e2526 --- /dev/null +++ b/crates/prism-core/src/core/work.rs @@ -0,0 +1,176 @@ +//! Compiler-work counters: how much structural work a phase actually did. +//! +//! Wall time answers "how long", which a loaded machine can answer wrongly. These +//! counters answer "how much", which it cannot: they are sums and maxima over the +//! nodes a pass touched, so they are a property of the compilation and not of the +//! box it ran on. That is what makes them usable as evidence rather than as a +//! reading, and it is why a receipt quotes them. +//! +//! Descents charge in two disciplines: +//! +//! - a read-only walk enters a node and reconstructs nothing; +//! - a rewrite reconstructs every node it descends. +//! +//! So `visits` counts every node either discipline entered, and `rebuilt` counts +//! only the nodes a rewrite rebuilt. The pair separates analysis from +//! transformation without comparing input to output, which would cost more than +//! the pass being measured. +//! +//! **What is counted, exactly.** Three shared descents, and only those: the +//! untyped [`Rewrite`](super::traverse::Rewrite) and +//! [`Visit`](super::traverse::Visit), plus the typed-Core rewrite the optimizer +//! and effect lowering are built on. That leaves real work uncounted, and the +//! omissions are not a rounding error: +//! +//! - a pass that overrides a variant and handles it without recursing charges +//! nothing for that node; +//! - a hand-rolled walk charges nothing at all, which covers the typed analyses +//! (there is no shared typed read-only discipline to instrument) and the +//! frame-local `tailrec` recursion; +//! - the front end works on the AST, which has no instrumented descent, so +//! parsing, resolution, and typechecking charge nothing however hard they +//! worked. +//! +//! Elaboration is the awkward case and worth naming: it charges, but only for +//! the free-variable scans that classify each handler's resume shape. That is a +//! count of handlers, not of the program, so a one-line program still reports a +//! few hundred visits for the prelude's handlers. The number is real and it is +//! not a measure of elaboration. +//! +//! A zero is therefore "no instrumented descent happened", never "no work +//! happened", and a non-zero is "this much instrumented descent happened", +//! never "this is what the phase cost". These counters are a lower bound on +//! structural work: sound to compare between two runs of the same compiler, +//! unsound to read as a share of a phase's total cost. +//! +//! **Determinism.** `visits` and `rebuilt` are sums and `max_depth` is a maximum, +//! all over the same set of nodes regardless of the order threads reach them, so +//! a parallel compilation reports the same counts as a serial one. That is the +//! property that lets a receipt commit to them. Wall time and peak memory do not +//! have it and are therefore not recorded here. +//! +//! Counting is off unless [`enable`] is called, and off is the default: with the +//! flag clear every entry point is one relaxed load and a predicted branch. + +use std::cell::Cell; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; + +// Counting is opt-in, so the hot descent pays one relaxed load when it is off. +static ENABLED: AtomicBool = AtomicBool::new(false); + +// Every Core node entered by either descent discipline. +static VISITS: AtomicU64 = AtomicU64::new(0); +// Every Core node reconstructed by a rewrite descent. +static REBUILT: AtomicU64 = AtomicU64::new(0); +// The deepest descent any thread reached, as a maximum over per-thread depths. +static MAX_DEPTH: AtomicU64 = AtomicU64::new(0); + +thread_local! { + // Descent depth is per-stack, so it is per-thread; the global above keeps the + // maximum across threads. + static DEPTH: Cell = const { Cell::new(0) }; +} + +/// One phase's structural work. +/// +/// Every field is order-independent, so the same compilation reports the same +/// counts whether it ran on one thread or many. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct WorkCounts { + /// Core nodes entered by either descent discipline. + pub visits: u64, + /// Core nodes reconstructed by a rewrite descent. + pub rebuilt: u64, + /// The deepest descent observed. + pub max_depth: u64, +} + +impl WorkCounts { + /// Whether any structural work was recorded at all. + /// + /// This is a fact about the counters, not a verdict. Only a phase built on + /// an instrumented descent owes a non-zero count: the front end works on the + /// AST and legitimately records nothing here, so reading this as vacuity for + /// every phase would condemn `parse` for doing its job. The caller that knows + /// which descent a phase uses is the one that may draw the conclusion. + #[must_use] + pub const fn is_silent(self) -> bool { + self.visits == 0 + } +} + +/// Start counting. Idempotent, and never called from a hot path. +pub fn enable() { + ENABLED.store(true, Ordering::Relaxed); +} + +/// Whether counting is on. +#[must_use] +pub fn enabled() -> bool { + ENABLED.load(Ordering::Relaxed) +} + +/// Read the counters without disturbing them. +#[must_use] +pub fn snapshot() -> WorkCounts { + WorkCounts { + visits: VISITS.load(Ordering::Relaxed), + rebuilt: REBUILT.load(Ordering::Relaxed), + max_depth: MAX_DEPTH.load(Ordering::Relaxed), + } +} + +/// Read the counters and clear them, so the next read is attributed to whatever +/// runs next. The driver calls this at a phase boundary. +pub fn take() -> WorkCounts { + WorkCounts { + visits: VISITS.swap(0, Ordering::Relaxed), + rebuilt: REBUILT.swap(0, Ordering::Relaxed), + max_depth: MAX_DEPTH.swap(0, Ordering::Relaxed), + } +} + +/// Charge one visited node. +pub fn visit() { + if enabled() { + VISITS.fetch_add(1, Ordering::Relaxed); + } +} + +/// Charge one visited node that a rewrite also reconstructed. +pub fn rebuild() { + if enabled() { + VISITS.fetch_add(1, Ordering::Relaxed); + REBUILT.fetch_add(1, Ordering::Relaxed); + } +} + +/// Enter one descent level, returning a guard that leaves it on drop. +/// +/// The guard is what keeps depth honest under the early returns and `?` inside a +/// descent: the level is left however the frame exits. +#[must_use] +pub fn frame() -> Frame { + if !enabled() { + return Frame(false); + } + let depth = DEPTH.with(|d| { + let next = d.get() + 1; + d.set(next); + next + }); + MAX_DEPTH.fetch_max(depth, Ordering::Relaxed); + Frame(true) +} + +/// Holds one descent level open; see [`frame`]. +#[derive(Debug)] +pub struct Frame(bool); + +impl Drop for Frame { + fn drop(&mut self) { + if self.0 { + DEPTH.with(|d| d.set(d.get().saturating_sub(1))); + } + } +} diff --git a/crates/prism-core/src/flags.rs b/crates/prism-core/src/flags.rs index d0f6b629..ec79e652 100644 --- a/crates/prism-core/src/flags.rs +++ b/crates/prism-core/src/flags.rs @@ -257,7 +257,7 @@ pub struct DynFlags { /// lint between. Setting this therefore forces the uncached route, which is /// why it belongs on runs that already disable the cache (the whole-corpus /// optimizer-equivalence sweep, the example compile) and not on a whole test - /// suite, where it would quietly stand down the cache under test. + /// suite, where it would disable the cache under test. pub core_lint: bool, /// `PRISM_RT_CHECKS` (default off): compile the native C runtime with /// `-DPRISM_RT_DEBUG`, inserting a cheap validity check at every cell @@ -268,9 +268,8 @@ pub struct DynFlags { pub rt_checks: bool, /// `PRISM_NATIVE_KONT_FRAMES` (default off): ask the native link step to /// preserve frame pointers, unwind tables, and non-mandatory call frames for - /// experimental native kont frame capture. This is not native resume; it is - /// the build-mode backstop that makes return-PC capture less dependent on the - /// platform optimizer's defaults. + /// experimental native kont frame capture. This build-mode backstop makes + /// return-PC capture less dependent on the platform optimizer's defaults. pub native_kont_frames: bool, /// `PRISM_DUMP_CORE` (default none): sink for the per-pass Core dump. /// `stdout`/`stderr` stream a banner plus the block; an off spelling (`0`, @@ -337,6 +336,18 @@ pub struct DynFlags { /// interpreter, and the optimizer-equivalence sweep runs `-O2` against /// `--no-fuse` over the whole corpus. pub fuse: bool, + /// `PRISM_BORROW_INFER` (default on): infer per-parameter borrow masks for + /// provably pure functions, so a read-only argument is loaned across the + /// call instead of threading a retain and release pair. Augments the + /// declared `borrow` annotations at the reference-count insertion boundary + /// only; the inferred masks are a pure function of the checked source, so + /// they are a cost decision like a lowering tier, never part of a + /// definition's identity, and the output must not reveal the setting. The + /// inserted reference-count operations do move the linked bytes, so like + /// the effect tier the flag joins the artifact identity that keys + /// byte-level artifact caches. `PRISM_BORROW_INFER=0` disables inference, + /// leaving only the declared annotations. + pub borrow_infer: bool, /// `PRISM_SCHEDULER` (default cooperative/FIFO): which shipped cooperative /// scheduler `run_cooperative` binds to when the CLI does not pass /// `--scheduler`. @@ -368,6 +379,12 @@ pub struct DynFlags { /// back to a user-wide cache directory, then `target/prism-store`; see the /// store's `disk::resolve_store_path`. pub store_path: Option, + /// `PRISM_TOOL_PACKAGES_ROOT` (default none): load compiler-owned Prism tool + /// packages from this `packages/` directory while developing them. The + /// installed default remains the sources embedded in the binary. This is an + /// explicit developer input, never inferred from the checked project's + /// manifest, so a project cannot select the checker or linter that judges it. + pub tool_packages_root: Option, /// `PRISM_SOLVER_TIMEOUT_MS` (default none): the per-obligation wall-clock /// budget `prism verify` gives an external solver before it kills the process /// and records an infrastructure timeout. Physical policy, never part of the @@ -433,12 +450,14 @@ impl Default for DynFlags { backend_opt: BackendOpt::default(), no_specialize: false, fuse: false, + borrow_infer: true, scheduler: Scheduler::default(), effect_tier: EffectTier::default(), erasures: true, compiler_cache: true, store: false, store_path: None, + tool_packages_root: None, solver_timeout_ms: None, sign_mode: SignMode::default(), sign_key: None, @@ -493,6 +512,7 @@ impl DynFlags { backend_opt: backend_opt_from_env(base.backend_opt), no_specialize: base.no_specialize || env_present("PRISM_NO_SPECIALIZE"), fuse: env_bool("PRISM_FUSE", base.fuse), + borrow_infer: env_bool("PRISM_BORROW_INFER", base.borrow_infer), scheduler: std::env::var("PRISM_SCHEDULER") .ok() .and_then(|s| Scheduler::parse(&s)) @@ -504,6 +524,9 @@ impl DynFlags { store_path: std::env::var_os("PRISM_STORE_PATH") .map(PathBuf::from) .or_else(|| base.store_path.clone()), + tool_packages_root: std::env::var_os("PRISM_TOOL_PACKAGES_ROOT") + .map(PathBuf::from) + .or_else(|| base.tool_packages_root.clone()), solver_timeout_ms: std::env::var("PRISM_SOLVER_TIMEOUT_MS") .ok() .and_then(|s| s.trim().parse().ok()) @@ -564,6 +587,7 @@ impl DynFlags { "verbose" => self.verbose = toml_bool(key, val)?, "no-specialize" => self.no_specialize = toml_bool(key, val)?, "fuse" => self.fuse = toml_bool(key, val)?, + "borrow-infer" => self.borrow_infer = toml_bool(key, val)?, "compiler-cache" => self.compiler_cache = toml_bool(key, val)?, "store" => self.store = toml_bool(key, val)?, "query-threads" => self.query_threads = toml_pos_int(key, val)?, @@ -884,7 +908,7 @@ impl Scheduler { /// A backend optimization level clang accepts via `-O`. /// -/// The single source of truth shared by the `--backend-opt` flag and the +/// Parsed representation shared by the `--backend-opt` flag and the /// `PRISM_BACKEND_OPT` env knob. An invalid level is unrepresentable; every /// spelling flows through [`BackendOpt::as_str`], so the `-O` argument handed to /// `cc` and the artifact-identity label can never drift apart or off the set. diff --git a/crates/prism-core/src/types/deps.rs b/crates/prism-core/src/types/deps.rs index 63af5cbd..fd23d853 100644 --- a/crates/prism-core/src/types/deps.rs +++ b/crates/prism-core/src/types/deps.rs @@ -26,7 +26,7 @@ use prism_syntax::ast::{Core, Decl, Expr, HandlerArm, Pattern, Program, S}; /// Members within a component are returned in declaration order. References are /// collected with lexical scope: a name bound by a parameter, lambda, `let`, /// match pattern, or handler clause shadows the same-named top-level function, so -/// it is not a dependency. This matters for principal inference, not just +/// it is not a dependency. This affects principal inference as well as /// performance: a spurious edge would merge a callee into its caller's component, /// switching it from generalize-then-instantiate to monomorphic mutual recursion /// and so changing the inferred (effect) type. diff --git a/crates/prism-core/src/types/mod.rs b/crates/prism-core/src/types/mod.rs index 36c072cf..3b8c474f 100644 --- a/crates/prism-core/src/types/mod.rs +++ b/crates/prism-core/src/types/mod.rs @@ -11,7 +11,10 @@ pub mod repr; pub mod sig; pub mod ty; -pub use repr::{is_or_null_element, repr_of_type, Repr}; +pub use repr::{ + is_or_null_element, is_or_null_element_in, layout_of_type, layout_of_type_in, repr_of_type, + scalar_plan, AbiLayout, LiteralCell, RcBehavior, Repr, ScalarPlan, TypeLayout, ZeroPossibility, +}; pub use ty::{ show_effects, show_type_with_effects, Effects, Type, ARBITRARY_CLASS, BUF, CANONICAL, CONS, DIV_CLASS, EQ_CLASS, F32X4, F64X2, FLOAT_BUF, FROM_JSON_CLASS, HASH_CLASS, I32X4, I64X2, @@ -45,6 +48,13 @@ pub struct DeclInfo { pub params: Vec, pub ty: Type, pub effects: Effects, + /// Provably pure: the body's principal effect row solved empty and closed, + /// recorded from the pre-generalization witness (generalization re-opens a + /// pure row for context fit, so this fact cannot be read off `ty`). This is + /// the precondition the `borrow` calling convention requires; borrow + /// inference consumes it, and a rehydrated interface conservatively reports + /// `false` because the witness is not serialized. + pub pure: bool, } /// One effect operation's checked signature facts. diff --git a/crates/prism-core/src/types/repr.rs b/crates/prism-core/src/types/repr.rs index 3d3490d7..a5718930 100644 --- a/crates/prism-core/src/types/repr.rs +++ b/crates/prism-core/src/types/repr.rs @@ -1,33 +1,28 @@ -//! Runtime representation facts (`Repr`). +//! Runtime representation authority. //! -//! A `Repr` records how a value is laid out and moved at runtime, independent of -//! its `Kind` (which classifies it at the type level). Every existing Prism value -//! is `Repr::Value`; the unboxed-values work (behind `PRISM_UNBOXED`) introduces -//! the non-`Value` reprs and the types that carry them. This module is the fact -//! table and the `Type -> Repr` query; nothing here yet drives lowering, so until -//! the unboxed front end lands every program observes `repr_of_type == Value` (or -//! `Immediate`) and behaves and hashes exactly as before. +//! [`TypeLayout`] keeps facts that must not be inferred from one overloaded +//! enum separate: local storage, boundary ABI adaptation, zero-word behavior, +//! and reference-count behavior. In particular, `Unit` and `Bool` are both +//! immediate words but only `Unit` is zero, while an unboxed product is +//! multiword locally and boxed when it crosses the current native ABI. use std::fmt; use super::ty::Type; +use prism_common::sym::Sym; -/// How a value is represented at runtime. -/// -/// A lattice ordered from the most general boxed form (`Value`) down to concrete -/// unboxed payloads, with `Any` the internal top used for abstract declarations -/// and signatures. +const REPR_MIN_STACK: usize = 64 * 1024; +const REPR_GROW_STACK: usize = 2 * 1024 * 1024; + +/// How a value is stored in its local representation. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum Repr { - /// An ordinary boxed Prism value (a heap cell or a tagged immediate). The - /// default for everything until a type opts into an unboxed representation. + /// An ordinary runtime value word whose nullability is not known from the + /// storage class alone. Value, - /// A `Value` that is guaranteed not to be the null word, so it may back an - /// `OrNull` slot. Heap pointers and tagged immediates qualify; `OrNull` does - /// not. + /// A runtime value word guaranteed not to be the machine zero word. NonNullValue, - /// A non-pointer word that `dup`/`drop` treat as a no-op (`Unit`, `Bool`, - /// `Char`, the fixed-width `I64`/`U64`). + /// A tagged or reserved non-pointer word. Immediate, /// An unboxed 64-bit payload with no GC traversal. Bits64, @@ -35,160 +30,616 @@ pub enum Repr { Float64, /// An unboxed 128-bit SIMD payload (two words). Vec128, - /// An unboxed product whose fields are carried as their component reprs. + /// An unboxed product whose fields retain their component layouts. Product(Vec), - /// The internal upper bound for signatures and abstract declarations. Not - /// executable: a value of `Any` cannot be bound, passed, returned, matched, - /// or stored until its concrete representation is known. + /// An unresolved or non-value layout. It is never executable. Any, } impl Repr { - /// Whether this repr occupies an ordinary GC-scanned value slot. True for the - /// boxed and immediate forms; false for the raw unboxed payloads and `Any`. + /// Whether this representation occupies one ordinary runtime value word. #[must_use] pub const fn is_gc_value(&self) -> bool { matches!(self, Self::Value | Self::NonNullValue | Self::Immediate) } - /// Whether a value of this repr can be the null word. Only the plain boxed - /// `Value` form is nullable; `NonNullValue` and the unboxed forms are not. + /// Whether this representation names a concrete, finite local layout. #[must_use] - pub const fn is_nullable(&self) -> bool { - matches!(self, Self::Value) + pub fn is_representable(&self) -> bool { + self.field_width_words().is_some() } - /// Whether this repr names a concrete runtime layout. Everything except `Any` - /// is representable; `Any` must be resolved before a value can exist. + /// Storage width in machine words. + /// + /// Returns `None` for `Any`, for a product containing `Any`, or if the sum + /// overflows. The iterative walk also avoids recursive stack growth for an + /// adversarially deep product. #[must_use] - pub fn is_representable(&self) -> bool { - match self { - Self::Any => false, - Self::Product(fields) => fields.iter().all(Self::is_representable), - _ => true, + pub fn field_width_words(&self) -> Option { + let mut width = 0usize; + let mut pending = vec![self]; + while let Some(repr) = pending.pop() { + match repr { + Self::Any => return None, + Self::Vec128 => width = width.checked_add(2)?, + Self::Product(fields) => pending.extend(fields), + _ => width = width.checked_add(1)?, + } } + Some(width) } - /// The storage width in machine words. Word-sized forms are one word, `Vec128` - /// is two, and a `Product` is the sum of its fields. `Any` has no defined - /// layout: asking is a caller bug the representability check must catch - /// first, so it trips a debug assertion (a hard failure everywhere tests - /// run) rather than silently shaping an ABI around a placeholder width. + /// Required alignment in machine words. + /// + /// Products use the strictest field alignment. Undefined layouts return + /// `None` rather than silently acquiring word alignment. #[must_use] - pub fn field_width_words(&self) -> usize { - debug_assert!( - !matches!(self, Self::Any), - "field_width_words on Repr::Any: check is_representable first" - ); - match self { - Self::Vec128 => 2, - Self::Product(fields) => fields.iter().map(Self::field_width_words).sum(), - _ => 1, + pub fn alignment_words(&self) -> Option { + let mut alignment = 1usize; + let mut pending = vec![self]; + while let Some(repr) = pending.pop() { + match repr { + Self::Any => return None, + Self::Vec128 => alignment = alignment.max(2), + Self::Product(fields) => pending.extend(fields), + _ => {} + } } + Some(alignment) } } impl fmt::Display for Repr { - /// A user-facing name for a representation, for diagnostics ("expected a boxed - /// value, found an unboxed product"). fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Value => f.write_str("boxed value"), - Self::NonNullValue => f.write_str("non-null value"), + Self::Value => f.write_str("runtime value word"), + Self::NonNullValue => f.write_str("non-null value word"), Self::Immediate => f.write_str("immediate word"), Self::Bits64 => f.write_str("unboxed i64"), Self::Float64 => f.write_str("unboxed f64"), Self::Vec128 => f.write_str("128-bit vector"), Self::Product(_) => f.write_str("unboxed product"), - Self::Any => f.write_str("abstract representation"), + Self::Any => f.write_str("unknown representation"), + } + } +} + +/// How a local representation crosses the current function ABI. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum AbiLayout { + /// Local and boundary representations are identical. + Direct(Repr), + /// A locally unboxed product is allocated as one non-null value word. + BoxedProduct, + /// A polymorphic boundary requires an explicit opaque value-word contract. + OpaqueWord, + /// A nominal value needs declaration evidence before its ABI is known. + /// + /// Mandatory newtype erasure may make the source wrapper transparent, so + /// type syntax alone cannot decide whether the boundary carries a cell or + /// the wrapped value. + DeferredNominal, + /// Rows, type-level naturals, or unresolved product fields cannot cross. + Invalid, +} + +impl AbiLayout { + /// Concrete boundary representation, if this layout may cross the ABI. + #[must_use] + pub fn repr(&self) -> Option { + match self { + Self::Direct(repr) => Some(repr.clone()), + Self::BoxedProduct => Some(Repr::NonNullValue), + Self::OpaqueWord => Some(Repr::Value), + Self::DeferredNominal | Self::Invalid => None, + } + } +} + +/// Whether the machine zero word can encode a value. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ZeroPossibility { + Always, + Never, + Maybe, + NotAWord, + Unknown, +} + +/// Reference-count action required by a local value. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RcBehavior { + /// The value cannot own a heap cell. + Trivial, + /// The value is always a managed heap cell. + Managed, + /// The runtime word may be immediate, null, or a managed cell. + RuntimeWord, + /// Ownership is the composition of product fields. + Fields, + /// No executable ownership fact is available. + Unknown, +} + +/// Where a literal of the type keeps its backing cell, when it has one. +/// +/// The axis a scalar-literal consumer needs beyond storage, zero, and +/// ownership: a managed cell class alone cannot say whether a literal mints a +/// cell per use or shares one interned into the program image. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum LiteralCell { + /// The literal is a machine word; no cell exists. + NoCell, + /// One static cell interned into the program image, shared by every use. + Interned, + /// A fresh cell allocated at each materialized literal. + Boxed, + /// No literal encoding fact is available. + Unknown, +} + +/// Authoritative representation facts for one semantic type. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TypeLayout { + local: Repr, + abi: AbiLayout, + zero: ZeroPossibility, + rc: RcBehavior, + literal: LiteralCell, +} + +impl TypeLayout { + /// Representation used inside a function body. + #[must_use] + pub const fn local(&self) -> &Repr { + &self.local + } + + /// Representation plan at a function boundary. + #[must_use] + pub const fn abi(&self) -> &AbiLayout { + &self.abi + } + + /// Local zero-word behavior. + #[must_use] + pub const fn zero(&self) -> ZeroPossibility { + self.zero + } + + /// Local reference-count behavior. + #[must_use] + pub const fn rc(&self) -> RcBehavior { + self.rc + } + + /// Where a literal of the type keeps its backing cell. + #[must_use] + pub const fn literal(&self) -> LiteralCell { + self.literal + } + + /// True only when a value is exactly one known non-zero local word. + #[must_use] + pub fn is_non_zero_word(&self) -> bool { + self.local.is_gc_value() && self.zero == ZeroPossibility::Never + } +} + +fn direct(local: Repr, zero: ZeroPossibility, rc: RcBehavior, literal: LiteralCell) -> TypeLayout { + TypeLayout { + abi: AbiLayout::Direct(local.clone()), + local, + zero, + rc, + literal, + } +} + +fn product_layout(fields: Vec) -> TypeLayout { + let local = Repr::Product(fields); + let concrete = local.is_representable(); + TypeLayout { + local, + abi: if concrete { + AbiLayout::BoxedProduct + } else { + AbiLayout::Invalid + }, + zero: ZeroPossibility::NotAWord, + rc: if concrete { + RcBehavior::Fields + } else { + RcBehavior::Unknown + }, + literal: LiteralCell::Unknown, + } +} + +fn layout_inner(ty: &Type) -> TypeLayout { + match ty { + Type::Unit => direct( + Repr::Immediate, + ZeroPossibility::Always, + RcBehavior::Trivial, + LiteralCell::NoCell, + ), + Type::Bool | Type::Char => direct( + Repr::Immediate, + ZeroPossibility::Never, + RcBehavior::Trivial, + LiteralCell::NoCell, + ), + // These values are allocated cells. In particular, fixed-width integer + // and float literals are boxed by both native emitters, not immediates. + Type::I64 | Type::U64 | Type::Float | Type::Fun(..) | Type::Tuple(_) => direct( + Repr::NonNullValue, + ZeroPossibility::Never, + RcBehavior::Managed, + LiteralCell::Boxed, + ), + // `Str` shares the managed-cell class but its literals are interned + // into the program image, one static cell shared by every use. + Type::Str => direct( + Repr::NonNullValue, + ZeroPossibility::Never, + RcBehavior::Managed, + LiteralCell::Interned, + ), + // `Int` is either a tagged immediate or a boxed bignum, but never zero. + // Its literals are range-checked and encoded as tagged words. + Type::Int => direct( + Repr::NonNullValue, + ZeroPossibility::Never, + RcBehavior::RuntimeWord, + LiteralCell::NoCell, + ), + // A nominal source type may become representation-transparent during + // mandatory newtype erasure. Type syntax alone cannot distinguish that + // from an allocated datatype, so its ABI is deferred until declaration + // evidence arrives. + Type::Con(..) => TypeLayout { + local: Repr::Any, + abi: AbiLayout::DeferredNominal, + zero: ZeroPossibility::Unknown, + rc: RcBehavior::Unknown, + literal: LiteralCell::Unknown, + }, + // Rows and naturals are not value types at all. + Type::Row(_) | Type::Nat(_) => TypeLayout { + local: Repr::Any, + abi: AbiLayout::Invalid, + zero: ZeroPossibility::Unknown, + rc: RcBehavior::Unknown, + literal: LiteralCell::Unknown, + }, + Type::OrNull(_) => direct( + Repr::Value, + ZeroPossibility::Maybe, + RcBehavior::RuntimeWord, + LiteralCell::Unknown, + ), + Type::Forall(_, inner) | Type::RowForall(_, inner) | Type::Coeffect(inner, _) => { + layout_of_type(inner) } + Type::UnboxedTuple(fields) => product_layout( + fields + .iter() + .map(|field| layout_of_type(field).local) + .collect(), + ), + Type::UnboxedRecord(fields) => product_layout( + fields + .iter() + .map(|(_, field)| layout_of_type(field).local) + .collect(), + ), + // A flexible head might later become a multiword unboxed product. The + // local layout therefore stays unknown; a boundary may use a boxed-word + // convention only as an explicit ABI decision. + Type::Var(_) | Type::Exist(_) | Type::App(..) => TypeLayout { + local: Repr::Any, + abi: AbiLayout::OpaqueWord, + zero: ZeroPossibility::Unknown, + rc: RcBehavior::Unknown, + literal: LiteralCell::Unknown, + }, } } -/// The runtime representation of a type. +/// Compute the authoritative representation facts for `ty`. /// -/// Reads scalar built-ins and unboxed products; everything else is the boxed -/// `Value` (or `Immediate` for non-pointer words). Type variables and abstract -/// heads default to `Value`. +/// Recursive type syntax re-enters through this grown-stack boundary, while +/// width and alignment queries use iterative walks. +#[must_use] +pub fn layout_of_type(ty: &Type) -> TypeLayout { + stacker::maybe_grow(REPR_MIN_STACK, REPR_GROW_STACK, || layout_inner(ty)) +} + +/// Compute representation facts with declaration evidence for nominal types. +/// +/// The callback may claim an allocated wrapper only when that wrapper survives +/// mandatory representation passes. A false answer retains the context-free, +/// fail-closed nominal layout. The evidence is threaded through schemes, +/// coeffects, and product fields rather than being consulted only at the head. +#[must_use] +pub fn layout_of_type_in(ty: &Type, nominal_is_boxed: impl Fn(Sym) -> bool) -> TypeLayout { + fn with_evidence(ty: &Type, nominal_is_boxed: &F) -> TypeLayout + where + F: Fn(Sym) -> bool, + { + stacker::maybe_grow(REPR_MIN_STACK, REPR_GROW_STACK, || match ty { + // A nominal type has no scalar literal, so no literal-cell fact is + // claimed even once the wrapper is known to be boxed. + Type::Con(name, _) if nominal_is_boxed(*name) => direct( + Repr::NonNullValue, + ZeroPossibility::Never, + RcBehavior::Managed, + LiteralCell::Unknown, + ), + Type::Forall(_, inner) | Type::RowForall(_, inner) | Type::Coeffect(inner, _) => { + with_evidence(inner, nominal_is_boxed) + } + Type::UnboxedTuple(fields) => product_layout( + fields + .iter() + .map(|field| with_evidence(field, nominal_is_boxed).local) + .collect(), + ), + Type::UnboxedRecord(fields) => product_layout( + fields + .iter() + .map(|(_, field)| with_evidence(field, nominal_is_boxed).local) + .collect(), + ), + _ => layout_inner(ty), + }) + } + + with_evidence(ty, &nominal_is_boxed) +} + +/// Local runtime representation of a type. +/// +/// Kept as the compact compatibility query for existing verifier callers; new +/// consumers should use [`layout_of_type`] so they do not conflate local and ABI +/// layouts or infer zero/ownership behavior from storage alone. #[must_use] pub fn repr_of_type(ty: &Type) -> Repr { - match ty { - // Non-pointer words: dup/drop no-ops. - Type::Unit | Type::Bool | Type::Char | Type::I64 | Type::U64 => Repr::Immediate, - // A scheme's representation is its body's. - Type::Forall(_, inner) | Type::RowForall(_, inner) => repr_of_type(inner), - // Unboxed products carry their fields as component reprs. - Type::UnboxedTuple(fields) => Repr::Product(fields.iter().map(repr_of_type).collect()), - Type::UnboxedRecord(fields) => { - Repr::Product(fields.iter().map(|(_, t)| repr_of_type(t)).collect()) + let layout = layout_of_type(ty); + match layout.abi { + // The question this query answers is boundary word-ness, asked before + // erasure. A variable/application crosses polymorphically as one opaque + // word, while a nominal source value occupies one Core slot pending + // declaration-aware erasure. Reading both from explicit ABI states + // keeps the compatibility query from inventing another layout table. + AbiLayout::OpaqueWord | AbiLayout::DeferredNominal => Repr::Value, + _ => layout.local, + } +} + +/// How a scalar literal is encoded as its one runtime word. +/// +/// A derived view of the layout facts: the storage, zero, and ownership axes +/// pick the word shape, and [`LiteralCell`] says where a cell-carrying +/// literal's cell lives. `Str` literals are interned into the program image as +/// static cells, so no use site owns one and no release ever frees one, while +/// fixed-width integer and float literals allocate a fresh box at each +/// materialization. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ScalarPlan { + /// The machine zero word. + ZeroWord, + /// A tagged non-pointer word: the payload is stored as `(n << 1) | 1`. + TaggedImmediate, + /// One static cell in the program image, shared by every use. + StaticCell, + /// A freshly allocated cell per materialized literal. + FreshCell, +} + +impl ScalarPlan { + /// Whether a literal with this plan materializes a fresh heap cell its + /// use site must own. Zero and tagged words carry no cell at all, and a + /// static cell is owned by the program image, not the use site. + #[must_use] + pub const fn owns_fresh_cell(self) -> bool { + matches!(self, Self::FreshCell) + } +} + +/// Encoding plan for a scalar literal of type `ty`, derived from the layout +/// facts rather than re-decided inside each consumer. +/// +/// Fail-closed: layout facts that match no known scalar encoding are an +/// error, never a guess. `Int` reads as tagged because the native emitters +/// range-check each literal first; a wider literal reaches codegen boxed as +/// `I64` or a bignum. +/// +/// # Errors +/// +/// Returns the offending layout facts when they name no known scalar +/// encoding, so a consumer refuses the type instead of guessing a width. +pub fn scalar_plan(ty: &Type) -> Result { + let layout = layout_of_type(ty); + match (layout.local(), layout.zero(), layout.rc(), layout.literal()) { + (Repr::Immediate, ZeroPossibility::Always, RcBehavior::Trivial, LiteralCell::NoCell) => { + Ok(ScalarPlan::ZeroWord) + } + (Repr::Immediate, ZeroPossibility::Never, RcBehavior::Trivial, LiteralCell::NoCell) + | ( + Repr::NonNullValue, + ZeroPossibility::Never, + RcBehavior::RuntimeWord, + LiteralCell::NoCell, + ) => Ok(ScalarPlan::TaggedImmediate), + ( + Repr::NonNullValue, + ZeroPossibility::Never, + RcBehavior::Managed, + LiteralCell::Interned, + ) => Ok(ScalarPlan::StaticCell), + (Repr::NonNullValue, ZeroPossibility::Never, RcBehavior::Managed, LiteralCell::Boxed) => { + Ok(ScalarPlan::FreshCell) } - // Rows and type-level nats are not value types; asking for their runtime - // representation is a category error, so answer the non-executable top. - Type::Row(_) | Type::Nat(_) => Repr::Any, - // Everything else is an ordinary boxed value: arbitrary-precision `Int`, - // `Float`, `Str`, functions, datatypes, and the existing boxed `Tuple`. A - // non-allocating nullable (`OrNull`) also lands here: it sits in a value - // slot but may hold the null word, so it is the plain (nullable) `Value`. - // Unannotated variables and abstract heads default here too. - _ => Repr::Value, + (local, zero, rc, literal) => Err(format!( + "no scalar literal encoding for a layout of {local} / {zero:?} / {rc:?} / {literal:?}" + )), } } -/// Whether `a` is a sound element type for `OrNull(a)`: its runtime word is a -/// single value slot that is never the machine zero word. +/// Whether `a` is admitted as an `OrNull(a)` element. /// -/// So `Null` (the zero word) can never be confused with a present `This(v)`. Heap -/// pointers are non-zero by construction and tagged immediates are odd, so both -/// qualify. `Unit` is the zero word, `Float` and `Char` are excluded, an unboxed -/// product spans multiple words, and a bare type variable may instantiate to -/// `Unit`; only concrete, single-word, non-zero types are admitted. Nested -/// `OrNull` is rejected because the null word would be ambiguous. +/// This deliberately combines source-language policy with the physical proof: +/// the type must be on the supported policy list and the representation authority +/// must prove it is exactly one non-zero word. A flexible application is not +/// guessed to be boxed. #[must_use] -pub const fn is_or_null_element(a: &Type) -> bool { +pub fn is_or_null_element(a: &Type) -> bool { + is_or_null_element_in(a, |_| false) +} + +/// Declaration-aware `OrNull` element check. +/// +/// `nominal_is_boxed` must return true only when the named type is known to keep +/// an allocated, non-zero wrapper after mandatory representation passes. This +/// proof is required because a `Type::Con` may name a transparent newtype. +#[must_use] +pub fn is_or_null_element_in(a: &Type, nominal_is_boxed: impl Fn(Sym) -> bool) -> bool { matches!( a, - Type::Int - | Type::Bool - | Type::I64 - | Type::U64 - | Type::Str - | Type::Con(..) - | Type::App(..) - | Type::Tuple(_) - ) + Type::Con(..) | Type::Int | Type::Bool | Type::I64 | Type::U64 | Type::Str | Type::Tuple(_) + ) && layout_of_type_in(a, nominal_is_boxed).is_non_zero_word() } #[cfg(test)] mod tests { - use super::{repr_of_type, Repr}; + use super::{ + is_or_null_element, is_or_null_element_in, layout_of_type, layout_of_type_in, repr_of_type, + scalar_plan, AbiLayout, LiteralCell, RcBehavior, Repr, ScalarPlan, ZeroPossibility, + }; use crate::types::Type; + use prism_common::sym::Sym; #[test] - fn scalars_and_boxed_have_expected_reprs() { - assert_eq!(repr_of_type(&Type::Unit), Repr::Immediate); - assert_eq!(repr_of_type(&Type::Bool), Repr::Immediate); - assert_eq!(repr_of_type(&Type::I64), Repr::Immediate); - // Arbitrary-precision `Int` and the boxed `Tuple` stay boxed values. - assert_eq!(repr_of_type(&Type::Int), Repr::Value); - assert_eq!(repr_of_type(&Type::Tuple(vec![Type::Int])), Repr::Value); - // A scheme reports its body's representation. + fn scalar_facts_distinguish_storage_zero_and_ownership() { + let unit = layout_of_type(&Type::Unit); + let boolean = layout_of_type(&Type::Bool); + let i64_layout = layout_of_type(&Type::I64); + + assert_eq!(unit.local(), &Repr::Immediate); + assert_eq!(unit.zero(), ZeroPossibility::Always); + assert_eq!(boolean.local(), &Repr::Immediate); + assert_eq!(boolean.zero(), ZeroPossibility::Never); + assert_eq!(i64_layout.local(), &Repr::NonNullValue); + assert_eq!(i64_layout.rc(), RcBehavior::Managed); + assert_eq!(repr_of_type(&Type::U64), Repr::NonNullValue); + + // The literal-cell axis is what separates the managed scalars: same + // storage/zero/ownership triple, different literal homes. + assert_eq!(i64_layout.literal(), LiteralCell::Boxed); + assert_eq!(layout_of_type(&Type::Str).literal(), LiteralCell::Interned); + assert_eq!(layout_of_type(&Type::Int).literal(), LiteralCell::NoCell); assert_eq!( - repr_of_type(&Type::Forall("a".into(), Box::new(Type::Bool))), - Repr::Immediate + layout_of_type(&Type::Con(Sym::from("Box"), vec![])).literal(), + LiteralCell::Unknown ); } #[test] - fn predicates() { - assert!(Repr::Value.is_gc_value() && Repr::Value.is_nullable()); - assert!(Repr::Immediate.is_gc_value() && !Repr::Immediate.is_nullable()); - assert!(!Repr::Bits64.is_gc_value()); + fn local_products_and_boundary_products_are_distinct() { + let layout = layout_of_type(&Type::UnboxedTuple(vec![Type::Int, Type::Bool])); + assert_eq!( + layout.local(), + &Repr::Product(vec![Repr::NonNullValue, Repr::Immediate]) + ); + assert_eq!(layout.local().field_width_words(), Some(2)); + assert_eq!(layout.abi(), &AbiLayout::BoxedProduct); + assert_eq!(layout.abi().repr(), Some(Repr::NonNullValue)); + } + + #[test] + fn undefined_layouts_fail_closed() { + assert_eq!(Repr::Any.field_width_words(), None); + assert_eq!(Repr::Any.alignment_words(), None); assert!(!Repr::Any.is_representable()); - assert!(Repr::Product(vec![Repr::Bits64, Repr::Float64]).is_representable()); - assert_eq!(Repr::Vec128.field_width_words(), 2); + + let unresolved = layout_of_type(&Type::Var(Sym::from("a"))); + assert_eq!(unresolved.local(), &Repr::Any); + assert_eq!(unresolved.abi(), &AbiLayout::OpaqueWord); + + let product = layout_of_type(&Type::UnboxedTuple(vec![Type::Exist(1)])); + assert_eq!(product.abi(), &AbiLayout::Invalid); + assert_eq!(product.local().field_width_words(), None); + + let type_level_nat = layout_of_type(&Type::Nat(1)); + assert_eq!(type_level_nat.abi(), &AbiLayout::Invalid); + } + + #[test] + fn or_null_requires_policy_and_a_non_zero_word_proof() { + assert!(is_or_null_element(&Type::I64)); + assert!(is_or_null_element(&Type::Tuple(vec![Type::Unit]))); + assert!(!is_or_null_element(&Type::Unit)); + assert!(!is_or_null_element(&Type::Float)); + assert!(!is_or_null_element(&Type::App( + Box::new(Type::Var(Sym::from("f"))), + Box::new(Type::Int), + ))); + + let nominal = Type::Con(Sym::from("Box"), vec![Type::Unit]); + assert!(!is_or_null_element(&nominal)); + assert!(is_or_null_element_in(&nominal, |_| true)); + assert_eq!(layout_of_type(&nominal).local(), &Repr::Any); + assert_eq!(layout_of_type(&nominal).abi(), &AbiLayout::DeferredNominal); + assert_eq!(layout_of_type(&nominal).abi().repr(), None); + assert_eq!(repr_of_type(&nominal), Repr::Value); assert_eq!( - Repr::Product(vec![Repr::Bits64, Repr::Vec128]).field_width_words(), - 3 + layout_of_type_in(&nominal, |_| true).local(), + &Repr::NonNullValue ); + + let quantified = Type::Forall(Sym::from("a"), Box::new(nominal.clone())); + assert_eq!( + layout_of_type_in(&quantified, |_| true).local(), + &Repr::NonNullValue + ); + let product = Type::UnboxedTuple(vec![nominal]); + assert_eq!( + layout_of_type_in(&product, |_| true).abi(), + &AbiLayout::BoxedProduct + ); + } + + #[test] + fn scalar_plans_follow_the_layout_facts() { + assert_eq!(scalar_plan(&Type::Unit), Ok(ScalarPlan::ZeroWord)); + for ty in [Type::Int, Type::Bool, Type::Char] { + assert_eq!(scalar_plan(&ty), Ok(ScalarPlan::TaggedImmediate)); + } + assert_eq!(scalar_plan(&Type::Str), Ok(ScalarPlan::StaticCell)); + for ty in [Type::I64, Type::U64, Type::Float] { + assert_eq!(scalar_plan(&ty), Ok(ScalarPlan::FreshCell)); + } + // Only a fresh cell must be owned by the site that materializes it. + assert!(ScalarPlan::FreshCell.owns_fresh_cell()); + for plan in [ + ScalarPlan::ZeroWord, + ScalarPlan::TaggedImmediate, + ScalarPlan::StaticCell, + ] { + assert!(!plan.owns_fresh_cell()); + } + // No scalar encoding exists for a non-word or flexible layout; the + // plan must refuse rather than guess. + assert!(scalar_plan(&Type::Var(Sym::from("a"))).is_err()); + assert!(scalar_plan(&Type::UnboxedTuple(vec![Type::Int, Type::Bool])).is_err()); + } + + #[test] + fn widths_and_alignment_are_checked_iteratively() { + let repr = Repr::Product(vec![Repr::Bits64, Repr::Vec128]); + assert_eq!(repr.field_width_words(), Some(3)); + assert_eq!(repr.alignment_words(), Some(2)); } } diff --git a/crates/prism-lineage/src/verify.rs b/crates/prism-lineage/src/verify.rs index 01762ae1..babaf1f0 100644 --- a/crates/prism-lineage/src/verify.rs +++ b/crates/prism-lineage/src/verify.rs @@ -99,9 +99,8 @@ pub fn verify(graph: &LineageGraph, base_dir: &Path) -> Result Option { let prefix = format!("#define {name} "); let raw = header.lines().find_map(|l| l.strip_prefix(&prefix))?.trim(); + if let Some(inner) = raw.strip_prefix('(').and_then(|r| r.strip_suffix(')')) { + let (base, shift) = inner.split_once("<<")?; + let base = base.trim(); + let base = base.strip_suffix(C_LONG_SUFFIX).unwrap_or(base); + return base + .parse::() + .ok()? + .checked_shl(shift.trim().parse().ok()?); + } let raw = raw.strip_suffix(C_LONG_SUFFIX).unwrap_or(raw); raw.strip_prefix(C_HEX_PREFIX).map_or_else( || raw.parse().ok(), @@ -159,6 +170,7 @@ fn runtime_abi(manifest_dir: &str) -> String { let arity_word = required_c_define(&header, ABI_ARITY_WORD); let header_words = required_c_define(&header, ABI_HEADER_WORDS); let word_bytes = required_c_define(&header, ABI_WORD_BYTES); + let static_cell = required_c_define(&header, ABI_STATIC_CELL); let tag_offset = tag_word * word_bytes; let header_bytes = header_words * word_bytes; assert_eq!( @@ -202,6 +214,15 @@ fn runtime_abi(manifest_dir: &str) -> String { writeln!(out, "pub(crate) const WORD_BYTES: i64 = {word_bytes};").unwrap(); writeln!(out, "pub(crate) const TAG_OFF: i64 = {tag_offset};").unwrap(); writeln!(out, "pub(crate) const HDR_BYTES: i64 = {header_bytes};").unwrap(); + writeln!( + out, + "/// The rc-word marker for a cell baked into the executable image:\n\ + /// codegen writes it into a static cell's refcount word so the runtime\n\ + /// treats the cell as count-inert and never writes the (read-only) word.\n\ + pub(crate) const STATIC_CELL: i64 = {};", + rust_hex(static_cell) + ) + .unwrap(); out } diff --git a/crates/prism-native/src/codegen/abi.rs b/crates/prism-native/src/codegen/abi.rs index 90d1d66c..216183cf 100644 --- a/crates/prism-native/src/codegen/abi.rs +++ b/crates/prism-native/src/codegen/abi.rs @@ -1,8 +1,13 @@ use std::collections::BTreeMap; -use prism_core::types::CtorInfo; +use prism_core::types::{CtorInfo, Type}; use prism_syntax::kw; +// The scalar-literal encoding plan is a fact of the representation authority, +// shared with the reference-count and borrow passes; codegen re-exports it so +// emission arms and their guard have one import home. +pub(crate) use prism_core::types::{scalar_plan, ScalarPlan}; + // The runtime's cell layout and reserved heap-tag family, generated by // build.rs from runtime/prism_internal.h so this mirror cannot drift from the // C: TAG_OFF, HDR_BYTES, WORD_BYTES, one named constant per PRISM_*_TAG, and @@ -28,6 +33,34 @@ pub(crate) fn idx64(n: usize) -> i64 { i64::try_from(n).unwrap_or(i64::MAX) } +/// Byte offset of field slot `index` inside a heap cell: the header, then one +/// word per preceding slot. Constructor fields, boxed tuple fields, and +/// closure captures each occupy exactly one runtime word; the typed verifier +/// checks that invariant on every cell slot, so this fixed stride never needs +/// a per-slot layout consult. +pub(crate) fn field_slot_off(index: usize) -> i64 { + HDR_BYTES + idx64(index) * WORD_BYTES +} + +/// Guard one literal arm against representation drift: the arm names the +/// encoding it emits, and emission fails closed if the authority now plans a +/// different one. +pub(crate) fn require_scalar_plan( + ty: &Type, + expected: ScalarPlan, + what: &str, +) -> Result<(), String> { + let plan = scalar_plan(ty)?; + if plan == expected { + Ok(()) + } else { + Err(format!( + "codegen: {what} literal emits the {expected:?} encoding but the \ + layout authority plans {plan:?}" + )) + } +} + /// The runtime tag a `Case` dispatches on for constructor `name`: the wired-in /// nullable's `Null`/`This` tags, otherwise the datatype table's tag. Keeps the /// tag source the single `kw` constant instead of a literal re-typed in codegen. @@ -38,3 +71,87 @@ pub(crate) fn ctor_tag(ctors: &BTreeMap, name: &str) -> Option _ => ctors.get(name).map(|info| info.tag), } } + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use prism_common::sym::Sym; + use prism_core::core::{Comp, Core, CoreFn, LoweredCore, Value}; + use prism_core::types::{layout_of_type, AbiLayout, RcBehavior, Repr, Type}; + + fn lowered_value(value: Value) -> LoweredCore { + let core = Core { + fns: vec![CoreFn { + name: Sym::new("main"), + params: Vec::new(), + body: Comp::Return(value), + dict_arity: 0, + }], + }; + LoweredCore::validate(core) + .map_err(|violations| violations.join("\n")) + .expect("test Core is structurally lowered") + } + + fn emit_value(value: Value) -> String { + crate::emit_llvm(&lowered_value(value), &BTreeMap::new()).expect("LLVM emission succeeds") + } + + #[cfg(feature = "mlir")] + fn emit_value_mlir(value: Value) -> String { + crate::emit_mlir(&lowered_value(value), &BTreeMap::new()).expect("MLIR emission succeeds") + } + + #[test] + fn scalar_layout_matches_native_boxing() { + for ty in [Type::I64, Type::U64, Type::Float] { + let layout = layout_of_type(&ty); + assert_eq!(layout.local(), &Repr::NonNullValue); + assert_eq!(layout.abi(), &AbiLayout::Direct(Repr::NonNullValue)); + assert_eq!(layout.rc(), RcBehavior::Managed); + } + + for (name, value) in [ + ("I64", Value::I64(7)), + ("U64", Value::U64(9)), + ("Float", Value::Float(1.5)), + ] { + let ir = emit_value(value.clone()); + assert!( + ir.lines() + .any(|line| line.contains("call i64 @prism_box(i64")), + "{name} emission must allocate the box promised by its layout" + ); + #[cfg(feature = "mlir")] + assert!( + emit_value_mlir(value) + .lines() + .any(|line| line.contains("llvm.call @prism_box(")), + "MLIR {name} emission must allocate the same box" + ); + } + } + + #[test] + fn escaping_unboxed_product_uses_the_boxed_boundary_plan() { + let layout = layout_of_type(&Type::UnboxedTuple(vec![Type::Int, Type::Bool])); + assert!(matches!(layout.local(), Repr::Product(_))); + assert_eq!(layout.abi(), &AbiLayout::BoxedProduct); + assert_eq!(layout.abi().repr(), Some(Repr::NonNullValue)); + + let ir = emit_value(Value::UnboxedTuple(vec![Value::Int(1), Value::Bool(true)])); + assert!( + ir.lines() + .any(|line| line.contains("call ptr @prism_alloc(i64 2)")), + "an escaping unboxed product must follow the boxed boundary plan" + ); + #[cfg(feature = "mlir")] + assert!( + emit_value_mlir(Value::UnboxedTuple(vec![Value::Int(1), Value::Bool(true)])) + .lines() + .any(|line| line.contains("llvm.call @prism_alloc(")), + "MLIR products must follow the same boxed boundary plan" + ); + } +} diff --git a/crates/prism-native/src/codegen/dispatch.rs b/crates/prism-native/src/codegen/dispatch.rs index a1f63cd8..2e389e25 100644 --- a/crates/prism-native/src/codegen/dispatch.rs +++ b/crates/prism-native/src/codegen/dispatch.rs @@ -9,7 +9,7 @@ use std::cmp::Ordering; use std::collections::BTreeSet; use std::slice; -use super::abi::{idx64, HDR_BYTES, TAG_OFF, WORD_BYTES}; +use super::abi::{field_slot_off, idx64, TAG_OFF}; use super::emit::{Cg, LamBody, LamInfo}; use super::isa::{Buf, Isa}; use super::rt; @@ -136,7 +136,7 @@ impl Cg<'_, I> { for i in 0..fvs { let fp = format!("%_fp{tag}_{i}"); let fv = format!("%_fv{tag}_{i}"); - let off = HDR_BYTES + idx64(i) * WORD_BYTES; + let off = field_slot_off(i); self.isa.gep(&mut b, &fp, "%_cp", off); self.isa.load(&mut b, &fv, &fp); captured.push(fv); @@ -171,7 +171,7 @@ impl Cg<'_, I> { let tv = self.isa.const_int(&mut b, idx64(self.lams[adapter].tag)); self.isa.store(&mut b, &tv, &tp); for (i, fld) in fields.iter().enumerate() { - let off = HDR_BYTES + idx64(i) * WORD_BYTES; + let off = field_slot_off(i); let fp = format!("%_afp{tag}_{i}"); self.isa.gep(&mut b, &fp, &cp, off); self.isa.store(&mut b, fld, &fp); diff --git a/crates/prism-native/src/codegen/emit.rs b/crates/prism-native/src/codegen/emit.rs index 49e5378f..2579f40f 100644 --- a/crates/prism-native/src/codegen/emit.rs +++ b/crates/prism-native/src/codegen/emit.rs @@ -23,7 +23,10 @@ const CLOSURE_TAG_MASK: u64 = i64::MAX.cast_unsigned(); // forking the two tiers; such a literal must reach codegen boxed as I64/bignum. const TAGGED_INT_VALUE_BITS: u32 = i64::BITS - 2; -use super::abi::{ctor_tag, idx64, HDR_BYTES, NULL_WORD, RESERVED_HEAP_TAGS, TAG_OFF, WORD_BYTES}; +use super::abi::{ + ctor_tag, field_slot_off, idx64, require_scalar_plan, ScalarPlan, HDR_BYTES, NULL_WORD, + RESERVED_HEAP_TAGS, TAG_OFF, +}; use super::dispatch::partial_app_body; use super::isa::{Buf, Cmp, FloatBinOp, FloatIntrinsic, IntOp, Isa}; use super::rt; @@ -34,11 +37,13 @@ use prism_common::{ASCII_PRINTABLE_HI, ASCII_PRINTABLE_LO}; use prism_core::core::builtins::BUILTINS; use prism_core::core::builtins::{builtin, AbiArg, AbiResult, Builtin, BuiltinKind, FloatOp}; use prism_core::core::effect_abi::{is_free_monad_driver, EOP}; -use prism_core::core::tailrec::{reassoc, trmc_mode, trmc_shape, TrmcMode, TrmcShape}; +use prism_core::core::tailrec::{ + loops_as_tail_call, reassoc, trmc_mode, trmc_shape, TrmcMode, TrmcShape, +}; use prism_core::core::{ fv, reachable_fns, Comp, Core, CoreFn, CoreOp, CorePat, IoOp, LoweredCore, NegLane, Value, }; -use prism_core::types::CtorInfo; +use prism_core::types::{CtorInfo, Type}; use prism_syntax::kw; use prism_syntax::names::{closure_cap, generated_param}; @@ -119,6 +124,10 @@ pub(crate) struct Cg<'a, I> { // planning and emission agree on adapter tags. pub adapters: BTreeMap<(usize, usize), usize>, strs: Vec, + // Distinct literal spellings only: every mention of the same bytes resolves + // to the same static cell, so sharing is part of the emitted layout, not a + // size optimization the backend may skip. + str_index: BTreeMap, used_rt: BTreeMap, // Owned-libm transcendentals actually called (the `prism_m_*` symbols). These // take and return `f64` rather than the `i64` of `used_rt`, so they carry a @@ -149,6 +158,7 @@ impl<'a, I: Isa> Cg<'a, I> { lams: Vec::new(), adapters: BTreeMap::new(), strs: Vec::new(), + str_index: BTreeMap::new(), used_rt: BTreeMap::new(), used_fcall: BTreeSet::new(), used_apply: BTreeSet::new(), @@ -206,7 +216,7 @@ impl<'a, I: Isa> Cg<'a, I> { let tv = self.isa.const_int(&mut self.b, tag); self.isa.store(&mut self.b, &tv, &tag_ptr); for (i, fv) in fields.iter().enumerate() { - let off = HDR_BYTES + idx64(i) * WORD_BYTES; + let off = field_slot_off(i); let fp = self.dst(|i, b, d| i.gep(b, d, ptr, off)); self.isa.store(&mut self.b, fv, &fp); } @@ -239,7 +249,10 @@ impl<'a, I: Isa> Cg<'a, I> { body: &Comp, ) -> Result { // `Sym` orders by intern id, so sort captures by name to keep the closure - // cell layout (and `_fv` numbering) byte-stable across runs. + // cell layout (and `_fv` numbering) byte-stable across runs. Each capture + // fills one word of the cell, an invariant the typed verifier checks on + // every binding referenced across a suspension, so the capture count + // sizes the cell exactly. let mut free_vars: Vec = fv::comp_without(body, params).into_iter().collect(); free_vars.sort_by_cached_key(|s| s.as_str()); @@ -322,7 +335,7 @@ impl<'a, I: Isa> Cg<'a, I> { .get(x) .cloned() .ok_or_else(|| format!("codegen: unbound {x}")), - // Checked in every profile, not just under `debug_assertions`: the + // Checked in every profile. The // shift is what makes a literal an immediate, and out of range it // wraps into a different value in silence. Two comparisons per // literal buy a hard failure instead of a wrong program, in the @@ -335,26 +348,40 @@ impl<'a, I: Isa> Cg<'a, I> { elaborator must box a wider literal as I64/bignum before codegen" )); } + require_scalar_plan(&Type::Int, ScalarPlan::TaggedImmediate, "Int")?; Ok(self.isa.const_int(&mut self.b, n.wrapping_shl(1) | 1)) } Value::I64(n) => { + require_scalar_plan(&Type::I64, ScalarPlan::FreshCell, "I64")?; let c = self.isa.const_int(&mut self.b, *n); Ok(self.box_i64(&c)) } Value::U64(n) => { + require_scalar_plan(&Type::U64, ScalarPlan::FreshCell, "U64")?; let c = self.isa.const_int(&mut self.b, n.cast_signed()); Ok(self.box_i64(&c)) } - Value::Bool(b) => Ok(self.isa.const_int(&mut self.b, (i64::from(*b) << 1) | 1)), - Value::Unit => Ok(self.isa.const_int(&mut self.b, 0)), + Value::Bool(b) => { + require_scalar_plan(&Type::Bool, ScalarPlan::TaggedImmediate, "Bool")?; + Ok(self.isa.const_int(&mut self.b, (i64::from(*b) << 1) | 1)) + } + Value::Unit => { + require_scalar_plan(&Type::Unit, ScalarPlan::ZeroWord, "Unit")?; + Ok(self.isa.const_int(&mut self.b, 0)) + } Value::Float(f) => { + require_scalar_plan(&Type::Float, ScalarPlan::FreshCell, "Float")?; let ft = self.isa.const_float(&mut self.b, *f); Ok(self.f_out(&ft)) } Value::Str(s) => { - let idx = self.strs.len(); + require_scalar_plan(&Type::Str, ScalarPlan::StaticCell, "Str")?; + let next = self.strs.len(); + let idx = *self.str_index.entry(s.clone()).or_insert(next); + if idx == next { + self.strs.push(s.clone()); + } let nbytes = s.len(); - self.strs.push(s.clone()); Ok(self.dst(|i, b, d| i.str_lit(b, d, idx, nbytes))) } Value::Thunk(inner) => match inner.as_ref() { @@ -784,7 +811,7 @@ impl<'a, I: Isa> Cg<'a, I> { return Ok(()); } let arity = self.fn_arity(name.as_str())?; - if args.len() == arity && arity == self.cur_arity { + if loops_as_tail_call(args.len(), arity, self.cur_arity) { let avs: Vec = args .iter() .map(|a| self.value(regs, a)) @@ -868,7 +895,7 @@ impl<'a, I: Isa> Cg<'a, I> { None => self.dst(|i, b, d| i.call_ptr(b, d, rt::ALLOC, slice::from_ref(&n))), }; let cell = self.fill_obj(&ptr, *tag, &fvs); - let off = HDR_BYTES + idx64(*hole) * WORD_BYTES; + let off = field_slot_off(*hole); let hp = self.dst(|i, b, d| i.gep(b, d, &ptr, off)); let hole_int = self.dst(|i, b, d| i.ptrtoint(b, d, &hp)); let parent = self.dst(|i, b, d| i.inttoptr(b, d, &t.extra)); @@ -999,7 +1026,7 @@ impl<'a, I: Isa> Cg<'a, I> { CorePat::Ctor(_, fields) | CorePat::Tuple(fields) => { for (fi, sub) in fields.iter().enumerate() { if let Some(vname) = sub { - let off = HDR_BYTES + idx64(fi) * WORD_BYTES; + let off = field_slot_off(fi); let fp = self.dst(|i, b, d| i.gep(b, d, &ptr, off)); let fv = self.dst(|i, b, d| i.load(b, d, &fp)); arm_regs.insert(*vname, fv); @@ -1760,7 +1787,16 @@ mod tests { .flat_map(|(_, body, _)| body.lines()) .find(|l| l.starts_with(&prefix)) .unwrap_or_else(|| panic!("{name} not defined in any runtime/ module")); - let val = line[prefix.len()..].trim_end().trim_end_matches('L'); + let val = line[prefix.len()..].trim_end(); + if let Some(inner) = val.strip_prefix('(').and_then(|v| v.strip_suffix(')')) { + // The parenthesized shift form `(1L << n)` the rc-word markers use. + let (base, shift) = inner + .split_once("<<") + .unwrap_or_else(|| panic!("{name}: unsupported parenthesized define")); + let base: i64 = base.trim().trim_end_matches('L').parse().unwrap(); + return base << shift.trim().parse::().unwrap(); + } + let val = val.trim_end_matches('L'); val.strip_prefix("0x").map_or_else( || val.parse().unwrap(), |hex| i64::from_str_radix(hex, 16).unwrap(), @@ -1789,16 +1825,47 @@ mod tests { .count(), "the runtime defines a PRISM_*_TAG the generated mirror missed" ); - assert_eq!(c_def("PRISM_TAG_W") * super::WORD_BYTES, super::TAG_OFF); - assert_eq!( - c_def("PRISM_HDR_WORDS") * super::WORD_BYTES, - super::HDR_BYTES - ); + assert_eq!(c_def("PRISM_TAG_W") * abi::WORD_BYTES, abi::TAG_OFF); + assert_eq!(c_def("PRISM_HDR_WORDS") * abi::WORD_BYTES, abi::HDR_BYTES); assert_eq!(c_def("PRISM_RC_W"), 0); - assert_eq!(c_def("PRISM_WORD_BYTES"), super::WORD_BYTES); + assert_eq!(c_def("PRISM_WORD_BYTES"), abi::WORD_BYTES); assert_eq!( - c_def("PRISM_ARITY_W") * super::WORD_BYTES + super::WORD_BYTES, - super::HDR_BYTES + c_def("PRISM_ARITY_W") * abi::WORD_BYTES + abi::WORD_BYTES, + abi::HDR_BYTES + ); + // Every stored slot (constructor field, closure capture, TRMC hole) is + // one runtime word wide, starting right after the header: the fixed + // stride `field_slot_off` bakes in and the C runtime walks with. + assert_eq!(abi::field_slot_off(0), abi::HDR_BYTES); + for index in 1..4 { + assert_eq!( + abi::field_slot_off(index) - abi::field_slot_off(index - 1), + abi::WORD_BYTES + ); + } + } + + #[test] + fn static_cell_matches_runtime() { + // The rc word codegen writes into every static string cell, checked + // against the embedded header by the independent parser above: the + // marker is the header's bit, is a single bit, and shares no bit with + // the other rc-word markers, so no rc-word test can conflate the + // image-owned, region-owned, and forwarded cases. + assert_eq!(c_def("PRISM_STATIC_CELL"), abi::STATIC_CELL); + assert_eq!(abi::STATIC_CELL.count_ones(), 1); + assert_eq!(abi::STATIC_CELL & c_def("PRISM_ARENA_OWNED"), 0); + assert_eq!(abi::STATIC_CELL & c_def("PRISM_ARENA_FORWARDED"), 0); + // The inert mask stays exactly the two ownership markers: a future + // inert class must extend that one definition, where every rc-writing + // site already looks, rather than patching sites by hand. + let inert = "#define PRISM_RC_INERT (PRISM_ARENA_OWNED | PRISM_STATIC_CELL)"; + assert!( + super::rt::RUNTIME_FILES + .iter() + .flat_map(|(_, body, _)| body.lines()) + .any(|l| l.trim_end() == inert), + "PRISM_RC_INERT changed shape in the runtime header" ); } diff --git a/crates/prism-native/src/codegen/llvm.rs b/crates/prism-native/src/codegen/llvm.rs index d78b7878..4df260c1 100644 --- a/crates/prism-native/src/codegen/llvm.rs +++ b/crates/prism-native/src/codegen/llvm.rs @@ -16,7 +16,7 @@ use inkwell::values::{ }; use inkwell::{AddressSpace, FloatPredicate, IntPredicate, OptimizationLevel}; -use super::abi::idx64; +use super::abi::{idx64, STATIC_CELL, STR_TAG}; use super::emit::{ closure_summary_with_isa, emit_closure_adapters_with_isa, emit_closure_dispatch_with_isa, emit_lowered_with_isa, emit_selected_plan_with_isa, emit_selected_with_isa, @@ -261,15 +261,30 @@ impl<'ctx> Inkwell<'ctx> { let _ = self.call_direct(f, args, ""); } - fn str_gl(&self, idx: usize, size: usize) -> GlobalValue<'ctx> { + // The static string cell for literal `idx`, get-or-added so a mention may + // precede the initializer pass: a struct global with the exact heap-cell + // shape `{ rc, tag, byte_len, bytes }`, 8-aligned so the address is a valid + // cell word (tag bit clear). `nbytes` excludes the NUL the array carries. + fn str_gl(&self, idx: usize, nbytes: usize) -> GlobalValue<'ctx> { let name = format!(".str{idx}"); self.module.get_global(&name).unwrap_or_else(|| { - let len = u32::try_from(size).unwrap_or_else(|_| { + let len = u32::try_from(nbytes + 1).unwrap_or_else(|_| { self.ice("string literal exceeds u32 length"); u32::MAX }); - let ty = self.ctx.i8_type().array_type(len); - self.module.add_global(ty, None, &name) + let word = self.i64t(); + let cell = self.ctx.struct_type( + &[ + word.into(), + word.into(), + word.into(), + self.ctx.i8_type().array_type(len).into(), + ], + false, + ); + let g = self.module.add_global(cell, None, &name); + g.set_alignment(8); + g }) } @@ -515,15 +530,12 @@ impl Isa for Inkwell<'_> { } fn str_lit(&self, _b: &mut Buf, dst: &str, idx: usize, len: usize) { - let g = self.str_gl(idx, len + 1); - let f = self.decl( - rt::STR_LIT, - self.i64t() - .fn_type(&[self.ptr_t().into(), self.i64t().into()], false), - ); - let n = self.i64t().const_int(idx64(len).cast_unsigned(), false); - let cs = self.call_direct(f, &[g.as_pointer_value().into(), n.into()], nm(dst)); - self.set(dst, self.cs_basic(cs)); + let g = self.str_gl(idx, len); + let r = self + .builder + .build_ptr_to_int(g.as_pointer_value(), self.i64t(), nm(dst)) + .unwrap_or_else(|e| self.pint("str_lit", &e)); + self.set(dst, r.into()); } fn bin(&self, _b: &mut Buf, dst: &str, op: IntOp, x: &str, y: &str) { @@ -894,8 +906,18 @@ impl Isa for Inkwell<'_> { } fn str_global(&self, _out: &mut String, idx: usize, s: &str) { - let g = self.str_gl(idx, s.len() + 1); - g.set_initializer(&self.ctx.const_string(s.as_bytes(), true)); + let g = self.str_gl(idx, s.len()); + let word = self.i64t(); + let init = self.ctx.const_struct( + &[ + word.const_int(STATIC_CELL.cast_unsigned(), false).into(), + word.const_int(STR_TAG.cast_unsigned(), false).into(), + word.const_int(idx64(s.len()).cast_unsigned(), false).into(), + self.ctx.const_string(s.as_bytes(), true).into(), + ], + false, + ); + g.set_initializer(&init); g.set_constant(true); g.set_linkage(Linkage::Private); } diff --git a/crates/prism-native/src/codegen/mlir.rs b/crates/prism-native/src/codegen/mlir.rs index d7910a9e..45650832 100644 --- a/crates/prism-native/src/codegen/mlir.rs +++ b/crates/prism-native/src/codegen/mlir.rs @@ -1,7 +1,7 @@ use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Write; -use super::abi::idx64; +use super::abi::{idx64, STATIC_CELL, STR_TAG}; use super::emit::{emit_with_isa, escape_str, str_builtin_decls}; use super::isa::{Buf, Cmp, FloatBinOp, FloatIntrinsic, IntOp, Isa}; use prism_core::core::LoweredCore; @@ -37,13 +37,10 @@ impl Isa for MlirText { self.const_int(b, 0) } - fn str_lit(&self, b: &mut Buf, dst: &str, idx: usize, len: usize) { + fn str_lit(&self, b: &mut Buf, dst: &str, idx: usize, _len: usize) { let p = b.tmp(); b.line(&format!("{p} = llvm.mlir.addressof @str{idx} : !llvm.ptr")); - let n = self.const_int(b, idx64(len)); - b.line(&format!( - "{dst} = llvm.call @prism_str_lit({p}, {n}) : (!llvm.ptr, i64) -> i64" - )); + b.line(&format!("{dst} = llvm.ptrtoint {p} : !llvm.ptr to i64")); } fn bin(&self, b: &mut Buf, dst: &str, op: IntOp, x: &str, y: &str) { @@ -286,7 +283,6 @@ impl Isa for MlirText { "llvm.func @prism_prim_read_line() -> i64", "llvm.func @prism_prim_rand() -> i64", "llvm.func @prism_srand(i64)", - "llvm.func @prism_str_lit(!llvm.ptr, i64) -> i64", // The saturating fp->int intrinsic, called by `fptosi_sat`. Quoted // because the symbol has dots; `mlir-translate` binds it to the real // `@llvm.fptosi.sat.i64.f64` (an unused declaration is harmless). @@ -316,8 +312,40 @@ impl Isa for MlirText { } } + // A string literal's static cell: the exact heap str-cell shape + // `{ rc, tag, byte_len, bytes }` with the static rc marker, 8-aligned so + // the address is a valid cell word. Globals with a non-string element + // type need an initializer region in the llvm dialect, built here as an + // insertvalue chain over an undef aggregate. fn str_global(&self, out: &mut String, idx: usize, s: &str) { - Self::fmt_global(out, &format!("str{idx}"), s); + let arr = s.len() + 1; + let cell = format!("!llvm.struct<(i64, i64, i64, !llvm.array<{arr} x i8>)>"); + writeln!( + out, + "llvm.mlir.global internal constant @str{idx}() {{alignment = 8 : i64}} : {cell} {{" + ) + .unwrap(); + writeln!(out, " %v = llvm.mlir.undef : {cell}").unwrap(); + writeln!(out, " %rc = llvm.mlir.constant({STATIC_CELL} : i64) : i64").unwrap(); + writeln!(out, " %tag = llvm.mlir.constant({STR_TAG} : i64) : i64").unwrap(); + writeln!( + out, + " %len = llvm.mlir.constant({} : i64) : i64", + idx64(s.len()) + ) + .unwrap(); + writeln!( + out, + " %bs = llvm.mlir.constant(\"{}\\00\") : !llvm.array<{arr} x i8>", + escape_str(s) + ) + .unwrap(); + writeln!(out, " %v0 = llvm.insertvalue %rc, %v[0] : {cell}").unwrap(); + writeln!(out, " %v1 = llvm.insertvalue %tag, %v0[1] : {cell}").unwrap(); + writeln!(out, " %v2 = llvm.insertvalue %len, %v1[2] : {cell}").unwrap(); + writeln!(out, " %v3 = llvm.insertvalue %bs, %v2[3] : {cell}").unwrap(); + writeln!(out, " llvm.return %v3 : {cell}").unwrap(); + writeln!(out, "}}").unwrap(); } } diff --git a/crates/prism-native/src/codegen/rt.rs b/crates/prism-native/src/codegen/rt.rs index 641fd0bb..11e8147e 100644 --- a/crates/prism-native/src/codegen/rt.rs +++ b/crates/prism-native/src/codegen/rt.rs @@ -1,4 +1,4 @@ -//! Single source of truth for the C runtime symbols codegen calls by name. +//! Canonical C runtime symbols referenced by codegen. //! //! These are the `prism_*` intrinsics defined in the `runtime/` C modules that the //! emitter references directly: allocation, reference counting, boxing, the IO @@ -318,10 +318,12 @@ pub const REUSE_TOKEN: &str = "prism_reuse_token"; pub const RC_INC: &str = "prism_rc_inc"; pub const RC_DEC: &str = "prism_rc_dec"; -// Tagging: box/unbox a 63-bit payload, and the interned string-literal cell. +// Tagging: box/unbox a 63-bit payload. String literals need no runtime call: +// codegen bakes each distinct spelling into the image as a static cell and a +// mention is the global's address. The C runtime keeps its own prism_str_lit +// for the strings it builds internally. pub const BOX: &str = "prism_box"; pub const UNBOX: &str = "prism_unbox"; -pub const STR_LIT: &str = "prism_str_lit"; // IO intrinsics (the lowered forms of `Comp::Io`). pub const PRINT_INT: &str = "prism_print_int"; @@ -376,7 +378,6 @@ const ALL: &[&str] = &[ RC_DEC, BOX, UNBOX, - STR_LIT, PRINT_INT, PRINT_FLOAT, PRINT_NL, diff --git a/crates/prism-store/src/disk/census.rs b/crates/prism-store/src/disk/census.rs new file mode 100644 index 00000000..ace42946 --- /dev/null +++ b/crates/prism-store/src/disk/census.rs @@ -0,0 +1,247 @@ +//! A fast whole-store census: how many files each layer holds right now. +//! +//! The census opens every `store gc` run (the report the sweep is measured +//! against) and decides when a layer is so far past its budget that the sweep +//! retires it wholesale instead of walking it. It must therefore stay cheap on +//! exactly the stores that need it most, the ones holding millions of files: +//! on macOS each shard directory's entry count is one filesystem metadata read +//! (see `fast_entry_count`), and everywhere else the count reads directory +//! entries without ever opening or stating the files themselves. + +use std::fs; +use std::io; +use std::path::Path; + +#[cfg(target_os = "macos")] +use std::ffi::CString; +#[cfg(target_os = "macos")] +use std::os::raw::{c_char, c_int, c_void}; +#[cfg(target_os = "macos")] +use std::os::unix::ffi::OsStrExt; + +use super::{ + CERTS_DIR, DECISIONS_DIR, INDEX_DIR, META_DIR, OBJECTS_DIR, QUERIES_DIR, RETIRED_PREFIX, + VERIFIED_DIR, +}; + +// The label the census aggregates every retired tree under; the trees' +// on-disk names are unique per rename and carry no reportable identity. +const RETIRED_LABEL: &str = "retired"; + +/// One layer's file population, as `store gc` reports it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LayerCensus { + /// The layer as the user sees it: `objects`, `meta`, `queries/`, + /// `index`, or `retired` for trees a bulk sweep has renamed aside. + pub name: String, + /// Files currently on disk under the layer. + pub files: u64, +} + +/// Per-layer file populations for a whole store. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct StoreCensus { + /// One row per layer (and per query kind), in report order. + pub layers: Vec, +} + +impl StoreCensus { + /// Total files across every layer. + #[must_use] + pub fn total(&self) -> u64 { + self.layers.iter().map(|layer| layer.files).sum() + } + + /// The population recorded for `name`, zero when the layer is absent. + #[must_use] + pub fn files(&self, name: &str) -> u64 { + self.layers + .iter() + .find(|layer| layer.name == name) + .map_or(0, |layer| layer.files) + } +} + +/// Count the files under every layer of the store rooted at `root`. +/// +/// # Errors +/// Fails on a filesystem error other than a layer being absent. +pub(super) fn take(root: &Path) -> io::Result { + let mut layers = Vec::new(); + for layer in [OBJECTS_DIR, META_DIR] { + layers.push(LayerCensus { + name: layer.to_string(), + files: count_tree(&root.join(layer))?, + }); + } + let queries_root = root.join(QUERIES_DIR); + for kind in child_dirs(&queries_root)? { + layers.push(LayerCensus { + name: format!("{QUERIES_DIR}/{kind}"), + files: count_tree(&queries_root.join(&kind))?, + }); + } + for layer in [INDEX_DIR, DECISIONS_DIR, VERIFIED_DIR, CERTS_DIR] { + layers.push(LayerCensus { + name: layer.to_string(), + files: count_tree(&root.join(layer))?, + }); + } + let mut retired = 0u64; + for entry in fs::read_dir(root)? { + let entry = entry?; + let name = entry.file_name(); + let Some(name) = name.to_str() else { + continue; + }; + if name.starts_with(RETIRED_PREFIX) && entry.file_type()?.is_dir() { + retired += count_tree(&entry.path())?; + } + } + if retired > 0 { + layers.push(LayerCensus { + name: RETIRED_LABEL.to_string(), + files: retired, + }); + } + Ok(StoreCensus { layers }) +} + +// Count the files under `dir`: fast-count each child directory plus the plain +// files at the root (layout stamps, index files). Depth one is the store's +// whole shape: every layer is either flat or one level of shard or kind +// directories. An absent directory reads as empty. +fn count_tree(dir: &Path) -> io::Result { + let entries = match fs::read_dir(dir) { + Ok(rd) => rd, + Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(0), + Err(e) => return Err(e), + }; + let mut files = 0u64; + for entry in entries { + let entry = entry?; + if entry.file_type()?.is_dir() { + files += entry_count(&entry.path())?; + } else { + files += 1; + } + } + Ok(files) +} + +// The names of `dir`'s immediate subdirectories, empty when it is absent. +fn child_dirs(dir: &Path) -> io::Result> { + let entries = match fs::read_dir(dir) { + Ok(rd) => rd, + Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()), + Err(e) => return Err(e), + }; + let mut names = Vec::new(); + for entry in entries { + let entry = entry?; + if entry.file_type()?.is_dir() { + if let Some(name) = entry.file_name().to_str() { + names.push(name.to_string()); + } + } + } + names.sort(); + Ok(names) +} + +// One directory's entry count. The fast path asks the filesystem for the +// directory's own record; the portable path reads the directory. Neither +// opens or stats any file inside, and a directory that vanishes mid-census +// (a racing sweep) reads as empty. +fn entry_count(dir: &Path) -> io::Result { + if let Some(count) = fast_entry_count(dir) { + return Ok(count); + } + match fs::read_dir(dir) { + Ok(rd) => Ok(rd.count() as u64), + Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(0), + Err(e) => Err(e), + } +} + +// The `attrlist` request structure from the platform's `sys/attr.h`; the +// layout is fixed kernel ABI. +#[cfg(target_os = "macos")] +#[repr(C)] +struct AttrList { + bitmapcount: u16, + reserved: u16, + commonattr: u32, + volattr: u32, + dirattr: u32, + fileattr: u32, + forkattr: u32, +} + +#[cfg(target_os = "macos")] +const ATTR_BIT_MAP_COUNT: u16 = 5; +#[cfg(target_os = "macos")] +const ATTR_DIR_ENTRYCOUNT: u32 = 0x0000_0002; +// Returned buffer layout: a u32 total length, then the requested u32 count. +#[cfg(target_os = "macos")] +const ATTR_OUT_LEN: usize = 8; + +// The store crate carries no FFI dependency, so the one platform call it +// wants is declared directly; libSystem is always linked. +#[cfg(target_os = "macos")] +#[allow(unsafe_code)] +extern "C" { + fn getattrlist( + path: *const c_char, + attr_list: *mut c_void, + attr_buf: *mut c_void, + attr_buf_size: usize, + options: u32, + ) -> c_int; +} + +// On macOS `getattrlist(ATTR_DIR_ENTRYCOUNT)` reads the entry count straight +// from the directory's catalog record: one call whether the directory holds +// ten entries or a million. Any failure falls back to the portable count. +#[cfg(target_os = "macos")] +#[allow(unsafe_code)] +fn fast_entry_count(dir: &Path) -> Option { + let path = CString::new(dir.as_os_str().as_bytes()).ok()?; + let mut list = AttrList { + bitmapcount: ATTR_BIT_MAP_COUNT, + reserved: 0, + commonattr: 0, + volattr: 0, + dirattr: ATTR_DIR_ENTRYCOUNT, + fileattr: 0, + forkattr: 0, + }; + let mut buf = [0u8; ATTR_OUT_LEN]; + // SAFETY: `path` is NUL-terminated by CString; the attribute list requests + // exactly one u32 attribute, so the out-buffer (a u32 length header plus + // the u32 payload) is large enough, and the kernel writes at most + // `attr_buf_size` bytes into it. + let rc = unsafe { + getattrlist( + path.as_ptr(), + (&raw mut list).cast(), + buf.as_mut_ptr().cast(), + buf.len(), + 0, + ) + }; + if rc != 0 { + return None; + } + let used = u32::from_ne_bytes(buf[0..4].try_into().ok()?) as usize; + if used < ATTR_OUT_LEN { + return None; + } + Some(u64::from(u32::from_ne_bytes(buf[4..8].try_into().ok()?))) +} + +// Everywhere else there is no catalog record to ask, so the caller always walks. +#[cfg(not(target_os = "macos"))] +const fn fast_entry_count(_dir: &Path) -> Option { + None +} diff --git a/crates/prism-store/src/disk/decisions.rs b/crates/prism-store/src/disk/decisions.rs index 0c248bff..fdf7b90c 100644 --- a/crates/prism-store/src/disk/decisions.rs +++ b/crates/prism-store/src/disk/decisions.rs @@ -2,9 +2,8 @@ use std::fs; use std::io; use std::path::{Path, PathBuf}; -use super::atomic_write; +use super::{atomic_write, DECISIONS_DIR}; -const DECISIONS_DIR: &str = "decisions"; const DECISION_FORMAT: &str = "prism-query-decision-v1"; fn path(root: &Path, kind: &str, locator: &str) -> io::Result { diff --git a/crates/prism-store/src/disk/gc.rs b/crates/prism-store/src/disk/gc.rs new file mode 100644 index 00000000..9fdf01e7 --- /dev/null +++ b/crates/prism-store/src/disk/gc.rs @@ -0,0 +1,550 @@ +//! Mark-and-sweep garbage collection over the anonymous object and metadata +//! layers, with a bulk mode for trees that have outgrown in-place sweeping. +//! +//! The store's own contract ("everything is a cache") holds for the query and +//! index layers, whose entries are the only durable references into +//! `objects/`/`meta/` this crate can see: an object still bound by a +//! surviving query output, or still pointed at by `names`/`deps`/`canonical`/ +//! `refs`, survives the sweep. Content addressed only by something outside the +//! store (a `prism.lock` pin in another project, for example) is invisible +//! here; a caller that must protect such content records a `refs` entry for it +//! first (see [`super::Store::set_ref`]). +//! +//! The age cutoff is the safety margin for exactly that blind spot, and for +//! the ordinary race between an in-progress write and this sweep: an object +//! newer than the cutoff survives even when unreferenced, so a commit that has +//! not yet reached its query/index binding is never swept out from under it. +//! +//! A layer (or a single query kind) holding vastly more files than eviction's +//! ceiling predates per-shard bounding or lost its eviction path; walking it +//! in place would grind for hours. The sweep instead retires the whole tree: +//! one rename moves it to a dot-prefixed sibling at the store root (readers +//! only open exact live paths, so the layer is healthy immediately), a +//! manifest written inside the tree before the rename records its origin as +//! data, and the drain then salvages what the live set still references or +//! what was written recently enough to be an in-flight commit, removing the +//! rest. A crash mid-drain leaves the tree in place; the next sweep finds it +//! by prefix, reads the manifest, and resumes. + +use std::collections::BTreeSet; +use std::fs; +use std::io; +use std::num::NonZeroUsize; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::thread; +use std::time::{Duration, SystemTime}; + +use super::{ + atomic_write, census, index, queries, refresh_entry_age, unique_name, META_DIR, OBJECTS_DIR, + OBJECT_SHARD_BUDGET, QUERIES_DIR, QUERY_SHARD_BUDGET, RETIRED_FORMAT, RETIRED_MANIFEST, + RETIRED_PREFIX, SHARD_COUNT, SHARD_HEX, TEMP_PREFIX, +}; + +// The sweep fans out across a tree's shard subdirectories, one disjoint chunk +// of shards per worker. Both the scan (a readdir plus a stat per file) and +// the unlinks themselves parallelize across independent directories +// (concurrent per-directory removals scale well on the filesystems that +// matter here, degrading only when few directories remain), so the cap exists +// to leave the machine usable during a sweep, not because the filesystem +// serializes the work. +const GC_MAX_WORKERS: usize = 8; + +// A layer or query kind whose census exceeds its eviction ceiling (every +// shard at its cap) by this factor is retired wholesale rather than swept in +// place. +const RUNAWAY_FACTOR: u64 = 4; + +// What a drain keeps besides live-set members: anything written within this +// margin of the sweep's start, covering an in-flight commit racing the +// retirement. Deliberately not the sweep's age cutoff: bulk mode exists for +// trees whose bulk is unreferenced churn, and a multi-day margin would +// salvage nearly all of it back. +const RETIRE_FRESH_MARGIN: Duration = Duration::from_hours(1); + +/// What one garbage-collection pass did, or, under `dry_run`, would do. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct GcStats { + /// Stale query bindings removed. + pub queries_removed: u64, + /// Anonymous objects removed. + pub objects_removed: u64, + /// Metadata blobs removed. + pub meta_removed: u64, + /// Bytes reclaimed from `objects/`, the layer that dominates store size; + /// `meta/` blobs are small and not separately tracked. + pub bytes_removed: u64, + /// Files a bulk retirement salvaged back into a live layer instead of + /// removing (still referenced, or fresh enough to be an in-flight write). + pub salvaged: u64, +} + +/// One progress beat from a sweep, carrying the running totals for the named +/// phase. Beats fire once per finished shard directory, from the sweep's +/// worker threads, so the callback must be `Sync`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GcProgress { + /// Which part of the sweep is reporting, as shown to the user. + pub phase: String, + /// Shard directories finished within the phase. + pub done: u64, + /// Shard directories the phase will touch; zero when unknown up front. + pub total: u64, + /// Files removed so far within the phase. + pub removed: u64, + /// Bytes reclaimed so far within the phase. + pub bytes: u64, + /// Files salvaged back into a live layer so far within the phase. + pub salvaged: u64, +} + +/// The callback a sweep reports [`GcProgress`] beats through. +pub type GcProgressFn<'a> = &'a (dyn Fn(&GcProgress) + Sync); + +/// Sweep the store rooted at `root`: prune query bindings older than +/// `cutoff`, then remove any object or metadata blob older than `cutoff` that +/// no surviving query output or index entry references. `dry_run` computes +/// what would be removed without touching the filesystem. +/// +/// # Errors +/// Fails on a filesystem error or a malformed query/index entry. +pub(super) fn sweep( + root: &Path, + cutoff: SystemTime, + dry_run: bool, + progress: GcProgressFn<'_>, +) -> io::Result { + let mut stats = GcStats::default(); + + // Bulk mode fires before any walk, so a runaway tree is renamed aside + // (and the live store healthy) without first paying a full crawl of it. + // Dry runs never rename; they fall through to the in-place walk, which + // reports exact counts at walking cost. + if !dry_run { + retire_runaway(root)?; + } + + stats.queries_removed = queries::sweep_stale(root, cutoff, dry_run, progress)?.removed; + + let mut live = queries::live_outputs(root, cutoff)?; + live.extend(index::all_referenced_hashes(root)?); + + let fresh_cutoff = SystemTime::now() + .checked_sub(RETIRE_FRESH_MARGIN) + .unwrap_or(SystemTime::UNIX_EPOCH); + drain_retired(root, &live, fresh_cutoff, dry_run, progress, &mut stats)?; + + let (objects_removed, object_bytes) = sweep_layer( + &root.join(OBJECTS_DIR), + &live, + cutoff, + dry_run, + OBJECTS_DIR, + progress, + )?; + stats.objects_removed += objects_removed; + stats.bytes_removed += object_bytes; + let (meta_removed, _) = sweep_layer( + &root.join(META_DIR), + &live, + cutoff, + dry_run, + META_DIR, + progress, + )?; + stats.meta_removed += meta_removed; + + Ok(stats) +} + +// A layer's retirement threshold: every shard at its cap, times the runaway +// factor. Below it the in-place sweep is affordable; above it the tree +// predates bounding or its eviction stopped firing. +const fn layer_ceiling(shard_cap: usize) -> u64 { + RUNAWAY_FACTOR * shard_cap as u64 * SHARD_COUNT +} + +// Rename every runaway layer and query kind aside for offline draining. The +// census makes the decision cheap even when a tree holds millions of files. +fn retire_runaway(root: &Path) -> io::Result<()> { + let census = census::take(root)?; + for layer in [OBJECTS_DIR, META_DIR] { + if census.files(layer) > layer_ceiling(OBJECT_SHARD_BUDGET.cap) { + retire_tree(root, &root.join(layer), layer)?; + } + } + for row in &census.layers { + let Some(kind) = query_kind(&row.name) else { + continue; + }; + if row.files > layer_ceiling(QUERY_SHARD_BUDGET.cap) { + retire_tree(root, &root.join(QUERIES_DIR).join(kind), &row.name)?; + } + } + Ok(()) +} + +// The kind a census row (or a retired-tree origin) names, when it names one. +fn query_kind(origin: &str) -> Option<&str> { + origin + .strip_prefix(QUERIES_DIR) + .and_then(|rest| rest.strip_prefix('/')) + .filter(|kind| !kind.is_empty()) +} + +// Retire one tree: record its origin inside it (as data; nothing ever parses +// the directory name back), then rename it to a unique dot-prefixed sibling +// at the store root. The rename is the entire visible cost; the tree drains +// afterwards, or on a later run if this one dies first. +fn retire_tree(root: &Path, src: &Path, origin: &str) -> io::Result<()> { + atomic_write( + &src.join(RETIRED_MANIFEST), + format!("{RETIRED_FORMAT}\n{origin}\n").as_bytes(), + )?; + fs::rename(src, unique_name(root, RETIRED_PREFIX)) +} + +// The origin recorded inside a retired tree, when present and well-formed. +fn read_manifest(tree: &Path) -> Option { + let text = fs::read_to_string(tree.join(RETIRED_MANIFEST)).ok()?; + let mut lines = text.lines(); + if lines.next() != Some(RETIRED_FORMAT) { + return None; + } + let origin = lines.next()?.trim(); + (!origin.is_empty()).then(|| origin.to_string()) +} + +// What draining one retired tree (or shard of one) did. +#[derive(Debug, Clone, Copy, Default)] +struct Drained { + removed: u64, + bytes: u64, + salvaged: u64, +} + +// Drain every retired tree at the store root: this run's renames plus any +// leftover from a crashed one. Hash-keyed trees (objects, meta) salvage +// entries the live set references or that were written within the fresh +// margin; a retired query kind salvages nothing (a binding is recomputed, +// never worth carrying across a layer reset). A tree without a readable +// manifest also salvages nothing: it is unreferenced by construction, so +// removal only costs cache warmth. +fn drain_retired( + root: &Path, + live: &BTreeSet, + fresh_cutoff: SystemTime, + dry_run: bool, + progress: GcProgressFn<'_>, + stats: &mut GcStats, +) -> io::Result<()> { + for entry in fs::read_dir(root)? { + let entry = entry?; + let name = entry.file_name(); + let Some(name) = name.to_str() else { + continue; + }; + if !name.starts_with(RETIRED_PREFIX) || !entry.file_type()?.is_dir() { + continue; + } + let tree = entry.path(); + let origin = read_manifest(&tree); + let origin = origin.as_deref(); + let salvage_into = match origin { + Some(layer @ (OBJECTS_DIR | META_DIR)) => Some(root.join(layer)), + _ => None, + }; + let drained = drain_tree( + &tree, + salvage_into.as_deref(), + live, + fresh_cutoff, + dry_run, + progress, + origin.unwrap_or("retired"), + )?; + match origin { + Some(META_DIR) => stats.meta_removed += drained.removed, + Some(origin) if query_kind(origin).is_some() => { + stats.queries_removed += drained.removed; + } + _ => stats.objects_removed += drained.removed, + } + stats.bytes_removed += drained.bytes; + stats.salvaged += drained.salvaged; + if !dry_run { + fs::remove_dir_all(&tree)?; + } + } + Ok(()) +} + +// Drain one retired tree. Shard-directory drains fan out like the layer +// sweep; files at the tree root (the manifest, layout stamps, temp relics) +// go with the tree's final removal and are not separately counted. +fn drain_tree( + tree: &Path, + salvage_into: Option<&Path>, + live: &BTreeSet, + fresh_cutoff: SystemTime, + dry_run: bool, + progress: GcProgressFn<'_>, + origin: &str, +) -> io::Result { + let mut shard_dirs: Vec<(String, PathBuf)> = Vec::new(); + for entry in fs::read_dir(tree)? { + let entry = entry?; + if !entry.file_type()?.is_dir() { + continue; + } + if let Some(shard) = entry.file_name().to_str() { + shard_dirs.push((shard.to_string(), entry.path())); + } + } + let total = shard_dirs.len() as u64; + let done = AtomicU64::new(0); + let removed = AtomicU64::new(0); + let bytes = AtomicU64::new(0); + let salvaged = AtomicU64::new(0); + let phase = format!("drain {origin}"); + fan_out(&shard_dirs, |(shard, dir)| { + let d = drain_shard(shard, dir, salvage_into, live, fresh_cutoff, dry_run)?; + removed.fetch_add(d.removed, Ordering::Relaxed); + bytes.fetch_add(d.bytes, Ordering::Relaxed); + salvaged.fetch_add(d.salvaged, Ordering::Relaxed); + progress(&GcProgress { + phase: phase.clone(), + done: done.fetch_add(1, Ordering::Relaxed) + 1, + total, + removed: removed.load(Ordering::Relaxed), + bytes: bytes.load(Ordering::Relaxed), + salvaged: salvaged.load(Ordering::Relaxed), + }); + Ok(()) + })?; + Ok(Drained { + removed: removed.into_inner(), + bytes: bytes.into_inner(), + salvaged: salvaged.into_inner(), + }) +} + +// Drain one shard directory of a retired tree. An entry that vanishes +// mid-drain (an external cleanup racing this walk) reads as already removed. +fn drain_shard( + shard: &str, + dir: &Path, + salvage_into: Option<&Path>, + live: &BTreeSet, + fresh_cutoff: SystemTime, + dry_run: bool, +) -> io::Result { + let entries = match fs::read_dir(dir) { + Ok(rd) => rd, + Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Drained::default()), + Err(e) => return Err(e), + }; + let mut out = Drained::default(); + for entry in entries { + let entry = entry?; + if !entry.file_type()?.is_file() { + continue; + } + let name = entry.file_name(); + let Some(rest) = name.to_str() else { + continue; + }; + if rest.starts_with(TEMP_PREFIX) { + continue; + } + let meta = match entry.metadata() { + Ok(meta) => meta, + Err(e) if e.kind() == io::ErrorKind::NotFound => continue, + Err(e) => return Err(e), + }; + if let Some(layer) = salvage_into { + let referenced = live.contains(&format!("{shard}{rest}")); + let fresh = meta.modified().is_ok_and(|m| m >= fresh_cutoff); + if referenced || fresh { + if !dry_run { + salvage(&entry.path(), &layer.join(shard).join(rest))?; + } + out.salvaged += 1; + continue; + } + } + if !dry_run { + match fs::remove_file(entry.path()) { + Ok(()) => {} + Err(e) if e.kind() == io::ErrorKind::NotFound => continue, + Err(e) => return Err(e), + } + } + out.removed += 1; + out.bytes += meta.len(); + } + Ok(out) +} + +// Move one entry back into its live layer. A hard link carries the blob over +// without a copy; an entry already republished live wins and the retired copy +// is simply dropped. The salvaged file's age is refreshed so the next +// eviction sees it as current rather than inheriting its retired-era mtime. +fn salvage(from: &Path, to: &Path) -> io::Result<()> { + if let Some(dir) = to.parent() { + fs::create_dir_all(dir)?; + } + match fs::hard_link(from, to) { + Ok(()) => refresh_entry_age(to), + Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {} + Err(e) => return Err(e), + } + let _ = fs::remove_file(from); + Ok(()) +} + +// Run `work` over every item on a bounded worker pool, one disjoint chunk of +// items per worker, surfacing the first error (or a worker panic) once the +// workers finish. +fn fan_out(items: &[T], work: F) -> io::Result<()> +where + T: Sync, + F: Fn(&T) -> io::Result<()> + Sync, +{ + if items.is_empty() { + return Ok(()); + } + let workers = thread::available_parallelism() + .map_or(1, NonZeroUsize::get) + .min(GC_MAX_WORKERS); + let chunk_len = items.len().div_ceil(workers).max(1); + let work = &work; + thread::scope(|scope| { + // Collected on purpose: materializing the handles spawns every worker + // before the first join, so the chunks run concurrently instead of + // spawn-join serially. + #[allow(clippy::needless_collect)] + let handles: Vec<_> = items + .chunks(chunk_len) + .map(|chunk| scope.spawn(move || chunk.iter().try_for_each(work))) + .collect(); + handles + .into_iter() + .map(|handle| { + handle + .join() + .unwrap_or_else(|_| Err(io::Error::other("store gc worker panicked"))) + }) + .collect::>>() + })?; + Ok(()) +} + +// Walk one sharded layer (`objects/<2hex>/` or `meta/<2hex>/`), +// removing every file whose reconstructed hash is absent from `live` and +// whose mtime is older than `cutoff`, fanning the per-shard scans out across +// the worker pool with a beat per finished shard. Returns (files removed, +// bytes removed). +fn sweep_layer( + dir: &Path, + live: &BTreeSet, + cutoff: SystemTime, + dry_run: bool, + layer: &str, + progress: GcProgressFn<'_>, +) -> io::Result<(u64, u64)> { + let shards = match fs::read_dir(dir) { + Ok(rd) => rd, + Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok((0, 0)), + Err(e) => return Err(e), + }; + let mut shard_dirs: Vec<(String, PathBuf)> = Vec::new(); + for shard_entry in shards { + let shard_entry = shard_entry?; + if !shard_entry.file_type()?.is_dir() { + continue; + } + let shard = shard_entry.file_name(); + let Some(shard) = shard.to_str() else { + continue; + }; + if shard.len() != SHARD_HEX { + continue; + } + shard_dirs.push((shard.to_string(), shard_entry.path())); + } + let total = shard_dirs.len() as u64; + let done = AtomicU64::new(0); + let removed = AtomicU64::new(0); + let bytes = AtomicU64::new(0); + let phase = format!("sweep {layer}"); + fan_out(&shard_dirs, |(shard, path)| { + let (r, b) = sweep_shard(shard, path, live, cutoff, dry_run)?; + removed.fetch_add(r, Ordering::Relaxed); + bytes.fetch_add(b, Ordering::Relaxed); + progress(&GcProgress { + phase: phase.clone(), + done: done.fetch_add(1, Ordering::Relaxed) + 1, + total, + removed: removed.load(Ordering::Relaxed), + bytes: bytes.load(Ordering::Relaxed), + salvaged: 0, + }); + Ok(()) + })?; + Ok((removed.into_inner(), bytes.into_inner())) +} + +// Sweep one shard directory. An entry that vanishes mid-sweep (an evicting +// publish or an external cleanup racing this walk) reads as already +// collected, never as an error. +fn sweep_shard( + shard: &str, + dir: &Path, + live: &BTreeSet, + cutoff: SystemTime, + dry_run: bool, +) -> io::Result<(u64, u64)> { + let entries = match fs::read_dir(dir) { + Ok(rd) => rd, + Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok((0, 0)), + Err(e) => return Err(e), + }; + let mut removed = 0u64; + let mut bytes = 0u64; + for file_entry in entries { + let file_entry = file_entry?; + if !file_entry.file_type()?.is_file() { + continue; + } + let name = file_entry.file_name(); + let Some(rest) = name.to_str() else { + continue; + }; + if rest.starts_with(TEMP_PREFIX) { + continue; + } + let hash = format!("{shard}{rest}"); + if live.contains(&hash) { + continue; + } + let meta = match file_entry.metadata() { + Ok(meta) => meta, + Err(e) if e.kind() == io::ErrorKind::NotFound => continue, + Err(e) => return Err(e), + }; + if meta.modified()? >= cutoff { + continue; + } + if !dry_run { + match fs::remove_file(file_entry.path()) { + Ok(()) => {} + Err(e) if e.kind() == io::ErrorKind::NotFound => continue, + Err(e) => return Err(e), + } + } + removed += 1; + bytes += meta.len(); + } + Ok((removed, bytes)) +} diff --git a/crates/prism-store/src/disk/index.rs b/crates/prism-store/src/disk/index.rs index e4190ccd..e888c43d 100644 --- a/crates/prism-store/src/disk/index.rs +++ b/crates/prism-store/src/disk/index.rs @@ -113,7 +113,10 @@ fn lock_exclusive(file: &fs::File) -> io::Result<()> { } #[cfg(not(unix))] -fn lock_exclusive(_file: &fs::File) -> io::Result<()> { +// Keep the fallible signature shared with the Unix implementation so callers +// cannot become target-dependent merely because wasm's lock is a no-op. +#[allow(clippy::unnecessary_wraps)] +const fn lock_exclusive(_file: &fs::File) -> io::Result<()> { Ok(()) } @@ -311,6 +314,25 @@ pub(super) fn remove_ref(root: &Path, name: &str) -> io::Result<()> { Ok(()) } +/// Every hash any index entry points at: `names` and `canonical` and `refs` +/// values, plus every `deps` key and dependent. Gc's mark phase: an object +/// reachable from any index survives the object-layer sweep alongside the +/// query-bound outputs from [`super::queries::live_outputs`]. +/// +/// # Errors +/// Fails on a filesystem error or a malformed index file. +pub(super) fn all_referenced_hashes(root: &Path) -> io::Result> { + let mut out = BTreeSet::new(); + out.extend(load_names(root)?.into_values()); + for (hash, deps) in load_deps(root)? { + out.insert(hash); + out.extend(deps); + } + out.extend(load_canonical(root)?.into_values()); + out.extend(load_refs(root)?.into_values()); + Ok(out) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/prism-store/src/disk/meta.rs b/crates/prism-store/src/disk/meta.rs index 1dcfbae6..47aae573 100644 --- a/crates/prism-store/src/disk/meta.rs +++ b/crates/prism-store/src/disk/meta.rs @@ -22,7 +22,10 @@ use std::fs; use std::io; use std::path::Path; -use super::{atomic_write, shard_path, HashHex, FIELD_SEP, META_DIR}; +use super::{ + atomic_write, evict_shard_overflow, shard_path, HashHex, FIELD_SEP, META_DIR, + OBJECT_SHARD_BUDGET, +}; const META_HEADER: &str = "prism-store-meta\tv1"; const KEY_NAME: &str = "name"; @@ -46,7 +49,15 @@ pub(super) fn put(root: &Path, hash: &HashHex<'_>, m: &DefMeta) -> io::Result<() "{META_HEADER}\n{KEY_NAME}{FIELD_SEP}{}\n{KEY_TYPE}{FIELD_SEP}{}\n{KEY_DOC}{FIELD_SEP}{}\n", m.name, m.ty, m.doc ); - atomic_write(&shard_path(&root.join(META_DIR), hash), body.as_bytes()) + let path = shard_path(&root.join(META_DIR), hash); + atomic_write(&path, body.as_bytes())?; + // Metadata rides the object layer's budget: one blob per object hash, so + // the layers grow in lockstep and share one bound. An evicted blob's + // object survives; only its human-facing facts are re-derived. + if let Some(shard_dir) = path.parent() { + evict_shard_overflow(shard_dir, &path, OBJECT_SHARD_BUDGET); + } + Ok(()) } pub(super) fn get(root: &Path, hash: &HashHex<'_>) -> io::Result> { diff --git a/crates/prism-store/src/disk/mod.rs b/crates/prism-store/src/disk/mod.rs index c0075fe3..851029b5 100644 --- a/crates/prism-store/src/disk/mod.rs +++ b/crates/prism-store/src/disk/mod.rs @@ -14,6 +14,10 @@ //! //! Everything is a cache. The store is derived from the source, never //! required for correctness: deleting it forces recomputation, nothing more. +//! That contract lets the high-churn layers bound themselves: a publish that +//! lands in a full shard retires that shard's oldest entries (see +//! `evict_shard_overflow`), so no layer grows without limit between explicit +//! `gc` runs and the sweep is a deep clean, never the only line of defense. //! //! Durability and concurrency rest on two disciplines. Every write goes to a //! uniquely named temp file in the destination directory and is renamed into @@ -32,15 +36,17 @@ use std::fs; use std::io::{self, Write}; use std::ops::Deref; use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; use prism_common::digest::SCHEME as HASH_SCHEME; +mod census; mod certs; mod decisions; #[cfg(test)] mod faults; +mod gc; mod index; mod meta; mod objects; @@ -49,8 +55,10 @@ mod queries; mod testutil; mod verified; +pub use census::{LayerCensus, StoreCensus}; #[cfg(test)] use faults::FaultPoint; +pub use gc::{GcProgress, GcProgressFn, GcStats}; pub use index::{CanonicalConflict, CanonicalKey}; pub use meta::DefMeta; pub use verified::VerifiedRecord; @@ -67,19 +75,89 @@ const META_DIR: &str = "meta"; const INDEX_DIR: &str = "index"; const VERIFIED_DIR: &str = "verified"; const CERTS_DIR: &str = "certs"; +const QUERIES_DIR: &str = "queries"; +const DECISIONS_DIR: &str = "decisions"; const LOCK_FILE: &str = "lock"; // Objects and metadata blobs are sharded git-style by the first byte of the hex // hash (two hex characters) so no single directory holds the whole store. const SHARD_HEX: usize = 2; +// How many shard directories one sharded layer fans out to. +const SHARD_COUNT: u64 = 1 << (4 * SHARD_HEX); + +// Publish-time bounds for the high-churn sharded layers. Keys are hashes, so +// shards fill uniformly and bounding every shard bounds the layer: a publish +// landing in a shard past its budget retires that one directory's oldest +// entries, paying retirement continuously and locally instead of deferring it +// to a full-store crawl that grows more expensive the longer it is postponed. +// Budgets are per shard; a layer's ceiling is the budget times the shard +// count. Eviction never consults liveness: bindings and objects are cache +// entries whose loss is a correct future miss (see the query and object read +// paths), so age is the only signal needed. + +/// A sharded layer's per-shard retention budget: watermark pairs on both the +/// entry count and the byte total. +/// +/// A publish that leaves its shard over either cap retires oldest entries +/// until the overflowing dimension sits at its low mark, so a layer of tiny +/// entries is bounded by count and a layer of large blobs by bytes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ShardBudget { + /// Most entries one shard retains before a publish trims it. + pub cap: usize, + /// The entry count an overfull shard is trimmed back to. + pub low: usize, + /// Most bytes one shard retains before a publish trims it. + pub byte_cap: u64, + /// The byte total an oversized shard is trimmed back to. + pub byte_low: u64, +} + +const KIB: u64 = 1 << 10; +const MIB: u64 = 1 << 20; + +/// Query bindings are small fixed-size pointers, so the entry cap is the +/// binding bound: one kind holds at most about 64K bindings. +pub const QUERY_SHARD_BUDGET: ShardBudget = ShardBudget { + cap: 256, + low: 192, + byte_cap: MIB, + byte_low: 768 * KIB, +}; + +/// Objects (and the metadata blobs keyed beside them) vary from bytes to +/// megabytes, so both dimensions bind: at most about 256K entries and 4 GiB +/// per layer. +pub const OBJECT_SHARD_BUDGET: ShardBudget = ShardBudget { + cap: 1024, + low: 768, + byte_cap: 16 * MIB, + byte_low: 12 * MIB, +}; + +// One publish never retires more than this many files: a shard inherited far +// above its budget is ground down across many publishes instead of stalling +// one. +const EVICT_BATCH: usize = 512; // Every in-flight write carries this prefix. Readers only ever open exact // object/index paths, so a file with this prefix is never content: a temp left // by a killed writer is inert until some later write in the same directory. pub const TEMP_PREFIX: &str = ".tmp."; +// A layer or query kind so far past its budget that an in-place sweep would +// grind for hours is retired wholesale: renamed to a dot-prefixed sibling at +// the store root, drained offline, then deleted. Readers only ever open exact +// live paths, so a retired tree is invisible the moment the rename lands, and +// a crash mid-drain leaves a tree the next sweep finds by prefix and resumes. +// The manifest written inside the tree names its origin layer as data, so +// resumption never parses facts back out of a directory name. +const RETIRED_PREFIX: &str = ".retired."; +const RETIRED_MANIFEST: &str = ".retired-manifest"; +const RETIRED_FORMAT: &str = "prism-store-retired-v1"; + // Line-oriented flat-file conventions shared by every index. A record is one -// line; fields within a record are tab-separated; a list within a field is +// line. Fields within a record are tab-separated. A list within a field is // space-separated. Canonical symbols and hex hashes contain neither, so the // separators are unambiguous. const FIELD_SEP: char = '\t'; @@ -445,6 +523,46 @@ impl Store { pub fn has_cert(&self, subject: &str) -> bool { StoreHash::new(subject).is_ok_and(|subject| certs::has(&self.root, &subject)) } + + /// Garbage-collect entries older than `min_age`: prune stale query + /// bindings, then remove any object or metadata blob that no surviving + /// query output or index entry (`names`/`deps`/`canonical`/`refs`) + /// references. `dry_run` reports what would be removed without touching + /// the filesystem. See the `gc` submodule for the reachability rules and + /// why the age cutoff exists. + /// + /// # Errors + /// Fails on a filesystem error or a malformed query/index entry. + pub fn gc(&self, min_age: std::time::Duration, dry_run: bool) -> io::Result { + self.gc_with_progress(min_age, dry_run, &|_| {}) + } + + /// As [`Store::gc`], reporting progress beats to `progress` as the sweep + /// walks shard directories, so an interactive caller can render a live + /// indicator. Beats arrive from the sweep's worker threads, hence the + /// `Sync` bound on the callback. + /// + /// # Errors + /// Fails on a filesystem error or a malformed query/index entry. + pub fn gc_with_progress( + &self, + min_age: std::time::Duration, + dry_run: bool, + progress: GcProgressFn<'_>, + ) -> io::Result { + let cutoff = SystemTime::now() - min_age; + gc::sweep(&self.root, cutoff, dry_run, progress) + } + + /// Count the files each store layer holds right now, without reading or + /// stating any of them (see the `census` submodule for how the count stays + /// cheap on stores holding millions of files). + /// + /// # Errors + /// Fails on a filesystem error. + pub fn census(&self) -> io::Result { + census::take(&self.root) + } } /// Resolve the store root: the explicit `override_` (the `PRISM_STORE_PATH` @@ -517,20 +635,102 @@ fn shard_path(layer: &Path, hash: &HashHex<'_>) -> PathBuf { layer.join(shard).join(rest) } +// Retire the oldest entries of a shard directory that has grown past its +// budget on either dimension, down toward the low watermarks, never touching +// `keep` (the entry just published), temp files, or subdirectories. +// Best-effort by contract: eviction is hygiene for a cache layer, so any +// error, including a race with a concurrent evictor or an external cleanup +// unlinking the same files, leaves the publish untouched. +fn evict_shard_overflow(shard_dir: &Path, keep: &Path, budget: ShardBudget) { + let Ok(entries) = fs::read_dir(shard_dir) else { + return; + }; + let mut aged: Vec<(SystemTime, PathBuf, u64)> = Vec::new(); + let mut total_bytes = 0u64; + for entry in entries.flatten() { + let path = entry.path(); + if path == keep + || entry.file_name().to_string_lossy().starts_with(TEMP_PREFIX) + || !entry.file_type().is_ok_and(|t| t.is_file()) + { + continue; + } + let Ok(meta) = entry.metadata() else { + continue; + }; + let Ok(modified) = meta.modified() else { + continue; + }; + total_bytes += meta.len(); + aged.push((modified, path, meta.len())); + } + // A dimension participates only if it was over its cap when the publish + // landed; the other stops trimming the moment its own low mark holds. + let over_entries = aged.len() > budget.cap; + let over_bytes = total_bytes > budget.byte_cap; + if !over_entries && !over_bytes { + return; + } + aged.sort(); + let mut count = aged.len(); + let mut bytes = total_bytes; + for (evicted, (_, path, len)) in aged.into_iter().enumerate() { + let past_count = over_entries && count > budget.low; + let past_bytes = over_bytes && bytes > budget.byte_low; + if evicted >= EVICT_BATCH || (!past_count && !past_bytes) { + break; + } + let _ = fs::remove_file(path); + count -= 1; + bytes = bytes.saturating_sub(len); + } +} + +// Refresh a published entry's age so shard eviction, which retires by mtime, +// sees a republished entry as live. Best-effort for the same reason eviction +// is: an entry whose refresh loses a race is merely evicted a little sooner. +fn refresh_entry_age(path: &Path) { + let _ = fs::File::open(path).and_then(|f| f.set_modified(SystemTime::now())); +} + +// Tripwire for a broken retirement path, shared by the sharded layers. Every +// publish bounds its own shard, so a shard can only reach a count far past its +// budget if eviction has stopped firing or the tree predates bounding. A +// publish landing in the sample shard counts that one directory (keys are +// hashes, so one publish in 256 pays one small directory read) and, past the +// threshold, returns the layer-wide estimate to warn with, at most once per +// process per `warned` flag. Counting failures return nothing because a +// metric must never fail a publish that already succeeded. +const SAMPLE_SHARD: &str = "00"; +fn runaway_estimate(shard_dir: &Path, shard_warn_entries: u64, warned: &AtomicBool) -> Option { + let rd = fs::read_dir(shard_dir).ok()?; + let count = rd.count() as u64; + if count > shard_warn_entries && !warned.swap(true, Ordering::Relaxed) { + return Some(count.saturating_mul(SHARD_COUNT)); + } + None +} + // A hash usable as a filesystem key: nonempty hex, long enough to shard. This // guards the path construction, not the hash's cryptographic strength. -// Unique temp path in `dir`. The temp prefix marks it as never an object or -// index file, so a reader (which only opens exact known paths) ignores a temp -// left by a killed writer. -fn unique_temp(dir: &Path) -> PathBuf { +// Unique dot-prefixed path in `dir`: the pid, a timestamp, and a process-wide +// counter make collisions impossible in practice across concurrent writers. +fn unique_name(dir: &Path, prefix: &str) -> PathBuf { static COUNTER: AtomicU64 = AtomicU64::new(0); let n = COUNTER.fetch_add(1, Ordering::Relaxed); let pid = std::process::id(); let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) .map_or(0, |d| d.as_nanos()); - dir.join(format!("{TEMP_PREFIX}{pid}.{nanos}.{n}")) + dir.join(format!("{prefix}{pid}.{nanos}.{n}")) +} + +// Unique temp path in `dir`. The temp prefix marks it as never an object or +// index file, so a reader (which only opens exact known paths) ignores a temp +// left by a killed writer. +fn unique_temp(dir: &Path) -> PathBuf { + unique_name(dir, TEMP_PREFIX) } // The directory a store file publishes into; every store path has one. diff --git a/crates/prism-store/src/disk/objects.rs b/crates/prism-store/src/disk/objects.rs index 7f8466a6..7c351156 100644 --- a/crates/prism-store/src/disk/objects.rs +++ b/crates/prism-store/src/disk/objects.rs @@ -1,24 +1,64 @@ -//! The anonymous object layer: immutable, append-only, content-addressed blobs. +//! The anonymous object layer: immutable, content-addressed blobs. //! //! One file per content hash at `objects//`. Writing a hash //! that already exists verifies the new bytes match the stored bytes and writes //! nothing; a mismatch means two different definitions collided on one hash (a //! codegen or hashing bug), which is corruption and a hard error, never a silent //! overwrite. +//! +//! Immutable does not mean unbounded: each shard is capped, and a publish that +//! lands in a full shard retires that shard's oldest entries. A retired object +//! whose binding survives reads as an ordinary cache miss and is re-derived +//! (the query read path treats an absent object as a miss, never corruption), +//! so eviction needs no liveness analysis. A hit refreshes the file's age, +//! keeping hot objects ahead of cold generations. use std::fs; use std::io; use std::path::Path; +use std::sync::atomic::AtomicBool; + +use super::{ + atomic_write_if_absent, evict_shard_overflow, refresh_entry_age, runaway_estimate, shard_path, + HashHex, Written, OBJECTS_DIR, OBJECT_SHARD_BUDGET, SAMPLE_SHARD, SHARD_COUNT, +}; + +// Tripwire threshold for a broken eviction path: a layer holding this many +// objects is worth naming (once per process, see `runaway_estimate`) long +// before it grows into a disk-eating catalog. +const OBJECT_LAYER_WARN_ENTRIES: u64 = 1 << 20; + +static LAYER_SIZE_WARNED: AtomicBool = AtomicBool::new(false); -use super::{atomic_write_if_absent, shard_path, HashHex, Written, OBJECTS_DIR}; +// The runaway-layer tripwire (see `runaway_estimate` for the sampling scheme). +fn warn_if_runaway_layer(hash: &HashHex<'_>, entry: &Path) { + if !hash.as_str().starts_with(SAMPLE_SHARD) { + return; + } + let Some(shard_dir) = entry.parent() else { + return; + }; + let threshold = OBJECT_LAYER_WARN_ENTRIES / SHARD_COUNT; + if let Some(estimate) = runaway_estimate(shard_dir, threshold, &LAYER_SIZE_WARNED) { + eprintln!( + "warning: store object layer holds roughly {estimate} objects; \ + `prism store gc` sweeps unreferenced ones" + ); + } +} pub(super) fn put(root: &Path, hash: &HashHex<'_>, bytes: &[u8]) -> io::Result { let path = shard_path(&root.join(OBJECTS_DIR), hash); if !path.exists() && atomic_write_if_absent(&path, bytes)? { + if let Some(shard_dir) = path.parent() { + evict_shard_overflow(shard_dir, &path, OBJECT_SHARD_BUDGET); + } + warn_if_runaway_layer(hash, &path); return Ok(Written::New); } let existing = fs::read(&path)?; if existing == bytes { + refresh_entry_age(&path); return Ok(Written::Hit); } Err(io::Error::new( diff --git a/crates/prism-store/src/disk/queries.rs b/crates/prism-store/src/disk/queries.rs index 6a264968..fe92a94f 100644 --- a/crates/prism-store/src/disk/queries.rs +++ b/crates/prism-store/src/disk/queries.rs @@ -1,15 +1,43 @@ +use std::collections::BTreeSet; use std::fs; use std::io; use std::path::{Path, PathBuf}; +use std::sync::atomic::AtomicBool; +use std::time::SystemTime; -use super::{atomic_write_if_absent, StoreHash}; +use super::{ + atomic_write_if_absent, evict_shard_overflow, refresh_entry_age, runaway_estimate, shard_path, + GcProgress, GcProgressFn, StoreHash, QUERIES_DIR, QUERY_SHARD_BUDGET, SAMPLE_SHARD, + SHARD_COUNT, +}; #[cfg(test)] use super::faults::{self, FaultPoint}; -const QUERIES_DIR: &str = "queries"; const QUERY_FORMAT: &str = "prism-query-index-v1"; +// The query layer's own layout version, stamped at `queries/LAYOUT` when the +// first binding is published. It moves independently of the store-wide +// `STORE_FORMAT` stamp because query bindings are disposable cache metadata: +// a layout change here must never refuse the objects, certificates, and +// decision records beside them. A tree that predates this stamp (the flat +// pre-sharding layout) is never opened through the sharded paths, so its +// bindings read as ordinary misses, and `sweep_stale` removes its relic files +// unconditionally rather than migrating them. +const QUERY_LAYOUT_FILE: &str = "LAYOUT"; +const QUERY_LAYOUT: &str = "prism-query-layout-v2"; + +// Tripwire threshold for a broken eviction path: a kind holding this many +// bindings is worth naming (once per process, see `runaway_estimate`) long +// before it grows into a disk-eating catalog. +const QUERY_KIND_WARN_ENTRIES: u64 = 1 << 20; + +static KIND_SIZE_WARNED: AtomicBool = AtomicBool::new(false); + +// The stale-binding sweep's walk has no total known up front, so it beats a +// running removal count every this many removals. +const QUERY_SWEEP_BEAT: u64 = 1024; + fn kind_ok(kind: &str) -> bool { !kind.is_empty() && kind @@ -17,6 +45,9 @@ fn kind_ok(kind: &str) -> bool { .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'-' | b'.')) } +// A binding's path is sharded on the key like every other layer, so a hot +// kind never accumulates one flat directory holding every entry it ever +// bound: `queries///`. fn path(root: &Path, kind: &str, key: &StoreHash<'_>) -> io::Result { if !kind_ok(kind) { return Err(io::Error::new( @@ -24,16 +55,12 @@ fn path(root: &Path, kind: &str, key: &StoreHash<'_>) -> io::Result { "query kind must contain only lowercase ASCII, digits, '-' or '.'", )); } - Ok(root.join(QUERIES_DIR).join(kind).join(key.as_str())) + Ok(shard_path(&root.join(QUERIES_DIR).join(kind), key)) } -pub(super) fn get(root: &Path, kind: &str, key: &StoreHash<'_>) -> io::Result> { - let path = path(root, kind, key)?; - let text = match fs::read_to_string(path) { - Ok(text) => text, - Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None), - Err(e) => return Err(e), - }; +// Parse a query entry body (shared by a single lookup and a full-store walk): +// the format tag, then the bound output hash, and nothing else. +fn decode_output(text: &str) -> io::Result { let mut lines = text.lines(); if lines.next() != Some(QUERY_FORMAT) { return Err(io::Error::new( @@ -49,7 +76,196 @@ pub(super) fn get(root: &Path, kind: &str, key: &StoreHash<'_>) -> io::Result) -> io::Result> { + let path = path(root, kind, key)?; + let text = match fs::read_to_string(path) { + Ok(text) => text, + Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None), + Err(e) => return Err(e), + }; + Ok(Some(decode_output(&text)?)) +} + +// Walk every kind directory under `queries/`, skipping in-flight temp files +// (see `TEMP_PREFIX`) and the layer's `LAYOUT` stamp. A live binding sits in a +// shard directory and reaches `f` with its decoded output hash; a plain file +// directly under a kind is a relic of the pre-sharding flat layout and reaches +// `f` with no output, never read, so a sweep can retire a huge stale catalog +// on unlinks alone. +fn walk( + root: &Path, + mut f: impl FnMut(&fs::DirEntry, Option<&str>) -> io::Result<()>, +) -> io::Result<()> { + let dir = root.join(QUERIES_DIR); + let kinds = match fs::read_dir(&dir) { + Ok(rd) => rd, + Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()), + Err(e) => return Err(e), + }; + for kind_entry in kinds { + let kind_entry = kind_entry?; + if !kind_entry.file_type()?.is_dir() { + continue; + } + // The layer self-evicts on publish and tolerates external cleanups, + // so a directory or entry that vanishes mid-walk is a completed + // removal, never an error. + let shards = match fs::read_dir(kind_entry.path()) { + Ok(rd) => rd, + Err(e) if e.kind() == io::ErrorKind::NotFound => continue, + Err(e) => return Err(e), + }; + for shard_entry in shards { + let shard_entry = shard_entry?; + let name = shard_entry.file_name(); + if name.to_string_lossy().starts_with(super::TEMP_PREFIX) { + continue; + } + if shard_entry.file_type()?.is_file() { + f(&shard_entry, None)?; + continue; + } + let keys = match fs::read_dir(shard_entry.path()) { + Ok(rd) => rd, + Err(e) if e.kind() == io::ErrorKind::NotFound => continue, + Err(e) => return Err(e), + }; + for key_entry in keys { + let key_entry = key_entry?; + let name = key_entry.file_name(); + if !key_entry.file_type()?.is_file() + || name.to_string_lossy().starts_with(super::TEMP_PREFIX) + { + continue; + } + let text = match fs::read_to_string(key_entry.path()) { + Ok(text) => text, + Err(e) if e.kind() == io::ErrorKind::NotFound => continue, + Err(e) => return Err(e), + }; + let output = decode_output(&text)?; + f(&key_entry, Some(&output))?; + } + } + } + Ok(()) +} + +/// Every hash bound as the output of a query entry not older than `cutoff`. +/// Gc's mark phase: an object still pointed at by a surviving query binding +/// must survive the object-layer sweep. `cutoff` matches [`sweep_stale`] so an +/// entry this call would itself prune never marks its output live, regardless +/// of whether the caller is dry-running or actually sweeping. +/// +/// # Errors +/// Fails on a filesystem error or a malformed query entry. +pub(super) fn live_outputs(root: &Path, cutoff: SystemTime) -> io::Result> { + let mut out = BTreeSet::new(); + walk(root, |entry, output| { + // A pre-shard relic carries no output: its binding is already + // invalidated by the layout bump, so it never marks an object live. + if let Some(output) = output { + match entry.metadata().and_then(|m| m.modified()) { + Ok(modified) if modified >= cutoff => { + out.insert(output.to_string()); + } + Ok(_) => {} + // Concurrently retired; a gone binding marks nothing live. + Err(e) if e.kind() == io::ErrorKind::NotFound => {} + Err(e) => return Err(e), + } + } + Ok(()) + })?; + Ok(out) +} + +/// What one stale-binding sweep did. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub(super) struct QuerySweepStats { + pub removed: u64, +} + +/// Remove query bindings whose file was last written before `cutoff`, plus +/// every relic of the pre-sharding flat layout regardless of age (the layout +/// bump already invalidated those bindings, so removal is the deferred bulk +/// invalidation, never a migration). A pruned binding is an ordinary future +/// cache miss (see [`get`]); nothing else in the store depends on a query +/// entry's continued existence, so this is always safe regardless of what it +/// pointed at. +/// +/// # Errors +/// Fails on a filesystem error or a malformed query entry. +pub(super) fn sweep_stale( + root: &Path, + cutoff: SystemTime, + dry_run: bool, + progress: GcProgressFn<'_>, +) -> io::Result { + let mut stats = QuerySweepStats::default(); + walk(root, |entry, output| { + if output.is_some() { + match entry.metadata().and_then(|m| m.modified()) { + Ok(modified) if modified >= cutoff => return Ok(()), + Ok(_) => {} + // Concurrently retired; nothing left to remove or count. + Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()), + Err(e) => return Err(e), + } + } + if !dry_run { + match fs::remove_file(entry.path()) { + Ok(()) => {} + Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()), + Err(e) => return Err(e), + } + } + stats.removed += 1; + if stats.removed % QUERY_SWEEP_BEAT == 0 { + progress(&GcProgress { + phase: format!("sweep {QUERIES_DIR}"), + done: 0, + total: 0, + removed: stats.removed, + bytes: 0, + salvaged: 0, + }); + } + Ok(()) + })?; + Ok(stats) +} + +// Stamp the layer's layout version beside the kind directories. Only the +// write path pays the existence probe; readers rely on the layout structurally +// (a pre-shard tree simply never matches a sharded path). +fn stamp_layout(root: &Path) -> io::Result<()> { + let stamp = root.join(QUERIES_DIR).join(QUERY_LAYOUT_FILE); + if !stamp.exists() { + atomic_write_if_absent(&stamp, format!("{QUERY_LAYOUT}\n").as_bytes())?; + } + Ok(()) +} + +// The runaway-kind tripwire (see `runaway_estimate` for the sampling scheme): +// names the kind so the user knows which query family outgrew its bounds. +fn warn_if_runaway_kind(kind: &str, key: &StoreHash<'_>, entry: &Path) { + if !key.as_str().starts_with(SAMPLE_SHARD) { + return; + } + let Some(shard_dir) = entry.parent() else { + return; + }; + let threshold = QUERY_KIND_WARN_ENTRIES / SHARD_COUNT; + if let Some(estimate) = runaway_estimate(shard_dir, threshold, &KIND_SIZE_WARNED) { + eprintln!( + "warning: store query kind {kind:?} holds roughly {estimate} bindings; \ + `prism store gc` prunes stale ones" + ); + } } pub(super) fn put( @@ -61,6 +277,9 @@ pub(super) fn put( let path = path(root, kind, key)?; if let Some(existing) = get(root, kind, key)? { if existing == output.as_str() { + // A re-publish confirms the binding is hot; refreshing its age + // keeps it ahead of colder entries when its shard evicts. + refresh_entry_age(&path); return Ok(()); } return Err(io::Error::new( @@ -73,8 +292,13 @@ pub(super) fn put( } #[cfg(test)] faults::hit(FaultPoint::BeforeQueryPublish)?; + stamp_layout(root)?; let bytes = format!("{QUERY_FORMAT}\n{}\n", output.as_str()); if atomic_write_if_absent(&path, bytes.as_bytes())? { + if let Some(shard_dir) = path.parent() { + evict_shard_overflow(shard_dir, &path, QUERY_SHARD_BUDGET); + } + warn_if_runaway_kind(kind, key, &path); return Ok(()); } match get(root, kind, key)? { diff --git a/crates/prism-syntax/src/ast.rs b/crates/prism-syntax/src/ast.rs index 4e642c9e..bc953e31 100644 --- a/crates/prism-syntax/src/ast.rs +++ b/crates/prism-syntax/src/ast.rs @@ -1551,10 +1551,10 @@ impl ReflectKind { } // One step of an update path, read left to right. `Field(f)` descends into a -// record field; `Each` fans out over every element of a functor; `Case(C)` -// focuses through a sum constructor (a prism), leaving other constructors -// untouched; `Index(i)` focuses one element of an array/list; `Where(p)` keeps -// only foci satisfying `p`. All but `Field` are removed by desugar (lowered to +// record field. `Each` fans out over every element of a functor. `Case(C)` +// focuses through a sum constructor and leaves other constructors untouched. +// `Index(i)` focuses one array or list element. `Where(p)` keeps only foci +// satisfying `p`. All but `Field` are removed by desugar (lowered to // `fmap`, a `match`, `index_set`, and a guard), so a path that reaches // tc/elaborate is `Field`-only. #[derive(Clone, Debug)] diff --git a/crates/prism-syntax/src/error/code.rs b/crates/prism-syntax/src/error/code.rs index 4ea1fb7b..4f7fa2ae 100644 --- a/crates/prism-syntax/src/error/code.rs +++ b/crates/prism-syntax/src/error/code.rs @@ -19,6 +19,28 @@ pub enum ErrorPhase { Internal, } +impl ErrorPhase { + /// The subsystem's name, as a machine-readable artifact spells it. + /// + /// One home for the spelling: the diagnostic seams a Prism-written front + /// end is diffed against name the phase in their rows, and a second table + /// of the same names elsewhere would drift from this one. + #[must_use] + pub const fn as_str(self) -> &'static str { + match self { + Self::Type => "type", + Self::Lex => "lex", + Self::Parse => "parse", + Self::Resolve => "resolve", + Self::Lower => "lower", + Self::Codegen => "codegen", + Self::Runtime => "runtime", + Self::Io => "io", + Self::Internal => "internal", + } + } +} + /// Stable external identity of a compiler diagnostic. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ErrorCode { diff --git a/crates/prism-syntax/src/error/diag.rs b/crates/prism-syntax/src/error/diag.rs index 0af643b6..fe28c94f 100644 --- a/crates/prism-syntax/src/error/diag.rs +++ b/crates/prism-syntax/src/error/diag.rs @@ -310,6 +310,24 @@ pub enum ErrKind { MissingFields { fields: String, ctor: String }, #[error("field access on non-record type {ty}")] FieldAccessNonRecord { ty: String }, + #[error( + "field `{field}` cannot be projected from unrefined sum type `{ty}` with \ + {constructors} constructors; match a constructor first" + )] + PartialFieldProjection { + field: String, + ty: String, + constructors: usize, + }, + #[error( + "cannot spread an unrefined sum type `{ty}` into record constructor `{ctor}`; \ + the type has {constructors} constructors" + )] + RecordSpreadMultiCtor { + ctor: String, + ty: String, + constructors: usize, + }, /// The unboxed-values surface (`#(...)`, `#{...}`, `.#field`) parses, but /// representation-aware checking and lowering are unsupported. The `what` /// names the form ("tuples", "records", "projection"). @@ -464,6 +482,8 @@ pub enum ErrKind { }, #[error("no field `{field}` on type `{ctor_name}`")] NoFieldOnType { field: String, ctor_name: String }, + /// Retired in v0.20 when unrefined sum projection moved to E1023. Kept in + /// the catalogue so the published E4008 identity is never reassigned. #[error( "field `{field}` of type `{type_name}` has conflicting types across its constructors: \ `{first}` in one and `{second}` in another; a field read `x.{field}` cannot be given \ @@ -829,6 +849,8 @@ impl ErrKind { Self::NotRecordCtor { .. } => "E1007", Self::MissingFields { .. } => "E1008", Self::FieldAccessNonRecord { .. } => "E1009", + Self::PartialFieldProjection { .. } => "E1023", + Self::RecordSpreadMultiCtor { .. } => "E1024", Self::UnboxedUnsupported { .. } => "E1018", Self::ConflictingUpdatePaths { .. } => "E1010", Self::OpticPathSurvived { .. } => "E1011", @@ -1167,6 +1189,16 @@ mod tests { NotRecordCtor { ctor }, MissingFields { fields, ctor }, FieldAccessNonRecord { ty }, + PartialFieldProjection { + field, + ty, + constructors + }, + RecordSpreadMultiCtor { + ctor, + ty, + constructors + }, UnboxedUnsupported { what }, ConflictingUpdatePaths { a, b }, OpticPathSurvived, diff --git a/crates/prism-syntax/src/fmt/decl.rs b/crates/prism-syntax/src/fmt/decl.rs index e04249c0..d41e88e0 100644 --- a/crates/prism-syntax/src/fmt/decl.rs +++ b/crates/prism-syntax/src/fmt/decl.rs @@ -1,7 +1,8 @@ //! Top-level declaration formatting. //! //! Imports, effects, classes, instances, data and pattern declarations, -//! type/row/param rendering, and the function definition printer. The expression and statement printers live in the +//! type/row/param rendering, and the function definition printer. Expression and +//! statement printers live in the //! parent module; the body-bearing printers here are methods on `Fmt` so they //! can call back into them, while the purely structural printers stay free. diff --git a/crates/prism-syntax/src/lex/highlight.rs b/crates/prism-syntax/src/lex/highlight.rs index c30b99b1..4572ffd4 100644 --- a/crates/prism-syntax/src/lex/highlight.rs +++ b/crates/prism-syntax/src/lex/highlight.rs @@ -69,10 +69,8 @@ pub const PLAIN_CLASS: &str = "id"; /// Every token in `src` as `(start, end, class)`, in source order. /// /// The one producer of highlight spans. The browser highlighter, the docs -/// tooltips, and the code index all paint from this, so what they colour cannot -/// disagree about what a token is — the risk a second, hand-written tokenizer -/// would introduce, where highlighting that contradicts the compiler is worse -/// than none. +/// tooltips, and the code index all paint from this, keeping their token +/// classifications consistent with the compiler. /// /// Comments are included (they are among the most useful things to colour), so /// this reads the raw token stream rather than the layout-processed one that diff --git a/crates/prism-syntax/src/names.rs b/crates/prism-syntax/src/names.rs index adeba519..6a7b8ed2 100644 --- a/crates/prism-syntax/src/names.rs +++ b/crates/prism-syntax/src/names.rs @@ -40,8 +40,8 @@ pub const FAIL_OP: &str = "fail"; // Both names are spelled canonically, module-qualified: an operation is a member // of its declaring module's namespace exactly as a constructor is, so a bare // `alloc` would match any module that happened to declare one. `ARENA_MODULE` -// plus the tests below pin the two apart, so a move of the declaration cannot -// leave the compiler's hooks quietly matching nothing. +// plus the tests below pin the two apart, so moving the declaration breaks a test +// instead of disabling the compiler hooks. pub const ARENA_MODULE: &str = "Arena"; pub const ALLOC_EFFECT: &str = "Arena.Alloc"; pub const ALLOC_OP: &str = "Arena.alloc"; @@ -59,9 +59,8 @@ pub const CONTINUE_OP: &str = "loop@continue"; // The reserved capability effects (the IO-as-effects surface). `Console`/ // `FileSystem`/`Random`/`Env` route the nondeterministic input primitives; the // separate `Output` effect routes `print`/`println`. The effect declarations -// themselves live in the prelude; this is the single source of truth for the set -// of names, so the `replayable` row check reads it rather than re-spelling the -// literal list. +// themselves live in the prelude. The `replayable` row check reads this canonical +// list rather than re-spelling the names. pub const OUTPUT_EFFECT: &str = "Output"; // `Clock` (declared in `Concurrent`, not the prelude) joins the replayable set: // its real reads (`wall_now`/`mono_now`) are recorded observations like the other @@ -157,7 +156,7 @@ pub const FOREVER: &str = "forever"; // The prelude helper functions and stream op the desugarer and elaborator emit // calls to by name while lowering surface sugar: `run_io` is the default IO world // handler that `wrap_main_world` wraps `main` in; `force` is the `?.`/`??` Option -// forcer; `guard`/`succeeds` are list-comprehension qualifier tests; `scollect` +// forcer. `guard`/`succeeds` are list-comprehension qualifier tests. `scollect` // collects a comprehension's `emit`s into a list; `smap` maps a function over a // stream, the fusing collector a guard-free comprehension lowers through; // `concat_map` flattens a mapped stream; `emit` is the `Stream` effect op a @@ -1050,6 +1049,15 @@ pub fn specialized_clone(function: &str, n: usize) -> String { format!("{function}$sp{n}") } +/// Phase-private clone at one thunk-demand convention. +/// +/// Distinct from dictionary `$sp` clones so the independently ordered passes +/// cannot collide even when their counters agree. +#[must_use] +pub fn convention_clone(function: &str, n: usize) -> String { + format!("{function}$ec{n}") +} + // The top-level join function stream fusion emits when it ties a driven pipeline's // knot. `n` is a compilation-deterministic counter so two fused pipelines in one // program get distinct names; the `%` lead is unforgeable, so the join collides diff --git a/docs/examples/deriving.core.txt b/docs/examples/deriving.core.txt index 2f89dbb7..3144bee2 100644 --- a/docs/examples/deriving.core.txt +++ b/docs/examples/deriving.core.txt @@ -1,24 +1,24 @@ fn i@eqColor@eq(_x, _y) = - return _x to t@2076 - return _y to t@2077 - return (t@2076, t@2077) to t@2078 - case t@2078 of - (t@2079, t@2080) => - case t@2079 of + return _x to t@2074 + return _y to t@2075 + return (t@2074, t@2075) to t@2076 + case t@2076 of + (t@2077, t@2078) => + case t@2077 of Blue => - case t@2080 of + case t@2078 of Blue => return true _ => return false Green => - case t@2080 of + case t@2078 of Green => return true _ => return false Red => - case t@2080 of + case t@2078 of Red => return true _ => @@ -26,8 +26,8 @@ fn i@eqColor@eq(_x, _y) = _ => return false fn i@showColor@show(_x) = - return _x to t@2081 - case t@2081 of + return _x to t@2079 + case t@2079 of Red => return "Red" Green => diff --git a/docs/examples/lens.core.txt b/docs/examples/lens.core.txt index 13d3dad7..53f0d1df 100644 --- a/docs/examples/lens.core.txt +++ b/docs/examples/lens.core.txt @@ -1,22 +1,22 @@ fn x_of(_r) = + return _r to t@1796 + case t@1796 of + Vec2(t@1797, _) => + return t@1797 +fn with_x(_r, _v) = return _r to t@1798 + return _v to t@1799 case t@1798 of - Vec2(t@1799, _) => - return t@1799 -fn with_x(_r, _v) = - return _r to t@1800 - return _v to t@1801 - case t@1800 of - Vec2(t@1802, t@1803) => - return Vec2(t@1801, t@1803) + Vec2(t@1800, t@1801) => + return Vec2(t@1799, t@1801) fn y_of(_r) = + return _r to t@1802 + case t@1802 of + Vec2(_, t@1803) => + return t@1803 +fn with_y(_r, _v) = return _r to t@1804 + return _v to t@1805 case t@1804 of - Vec2(_, t@1805) => - return t@1805 -fn with_y(_r, _v) = - return _r to t@1806 - return _v to t@1807 - case t@1806 of - Vec2(t@1808, t@1809) => - return Vec2(t@1808, t@1807) + Vec2(t@1806, t@1807) => + return Vec2(t@1806, t@1805) diff --git a/docs/examples/lower_llvm.txt b/docs/examples/lower_llvm.txt index f397a2bc..ccf6729b 100644 --- a/docs/examples/lower_llvm.txt +++ b/docs/examples/lower_llvm.txt @@ -10,15 +10,13 @@ entry: ] b2: ; preds = %entry - call void @prism_rc_dec(i64 %a0) ret i64 1 b3: ; preds = %entry - %t5 = getelementptr inbounds i8, ptr %t0, i64 24 - %t6 = load i64, ptr %t5, align 8 - call void @prism_rc_inc(i64 %t6) - call void @prism_rc_dec(i64 %a0) - ret i64 %t6 + %t4 = getelementptr inbounds i8, ptr %t0, i64 24 + %t5 = load i64, ptr %t4, align 8 + call void @prism_rc_inc(i64 %t5) + ret i64 %t5 b1: ; preds = %entry call void @prism_match_error() diff --git a/docs/internal/SCOREBOARD.md b/docs/internal/SCOREBOARD.md index ec55f62c..db5c598e 100644 --- a/docs/internal/SCOREBOARD.md +++ b/docs/internal/SCOREBOARD.md @@ -24,11 +24,11 @@ compaction, above it is not. | component | Rust raw | Rust code | Prism raw | Prism code | ratio | | ---------------- | -------: | --------: | --------: | ---------: | ----: | -| lexer and layout | 1,548 | 1,230 | 1,693 | 1,241 | 1.01 | -| parser | 2,555 | 1,931 | 7,114 | 6,196 | 3.21 | +| lexer and layout | 1,546 | 1,230 | 1,684 | 1,234 | 1.00 | +| parser | 2,555 | 1,931 | 7,538 | 6,484 | 3.36 | | surface AST | 1,994 | 1,417 | 354 | 199 | 0.14 | -| syntax codecs | none | none | 2,898 | 2,386 | n/a | -| checker | 9,814 | 7,692 | 2,347 | 1,832 | 0.24 | +| syntax codecs | none | none | 2,902 | 2,382 | n/a | +| checker | 10,589 | 8,410 | 4,233 | 3,386 | 0.40 | What each row counts: @@ -57,20 +57,26 @@ What each row counts: ## Verdicts - **lexer and layout**, threshold control, recorded rather than judged: recorded - at 1.01. The Prism side was deliberately written to track the Rust side token + at 1.00. The Prism side was deliberately written to track the Rust side token for token so the two can be diffed, so what this row measures is that decision and not the language. -- **parser**, threshold ratio 0.50 or lower: FAILED at 3.21. The pre-registered +- **parser**, threshold ratio 0.50 or lower: FAILED at 3.36. The pre-registered bet was that the library floor had absorbed the plumbing. The first judgment decomposed the gap into three named causes, and the two that were compiler work have landed: the sequencing rewrite and the `let ... else` early-return binding form, which the parser now uses throughout. Rewriting the parser onto them removed 1,002 code lines, fourteen percent, while the same release grew the Rust side by 279 lines of new surface syntax, so the ratio fell from 4.31 - at v0.17.0. The residual gap stands on the remaining cause, that the generated - side never pays line by line for the productions its tables derive. No further - compiler work is pre-registered against this row; it stays failed rather than - re-excused. + at v0.17.0. It has since risen from 3.21, and the cause is worth naming + because it is not a regression in the language: closing the last four gaps + between the two parsers put 288 code lines onto the shadow and none onto the + oracle, which already derived those productions. Coverage catching up moves + this row the unfavorable way by construction, which is what a board measuring + lines rather than capability will do. The residual gap stands on the remaining + cause, that the generated side never pays line by line for the productions its + tables derive. No further compiler work is pre-registered against this row; it + stays failed rather than re-excused, and the rise is recorded rather than + netted against the earlier fall. - **surface AST**, threshold not evidence for the claim: not evidence at 0.14. The Rust file carries derives and hand-written trait impls alongside the declarations and the Prism file carries declarations only, so most of the gap @@ -83,13 +89,19 @@ What each row counts: record. There is no Rust counterpart to divide by because that side is derived and occupies no lines, and that asymmetry is exactly the gap. - **checker**, threshold ratio 0.50 or lower at full coverage: recorded, not - judged, at 0.24. The Prism side checks the pure first-order subset the - bootstrap workbench supports, so the number says what a subset costs, not what - the full checker will. Both counting asymmetries push the ratio up rather than - down: the Prism files carry their own type definitions and the artifact - decoding, while the Rust side counts the inference engine alone. The threshold - binds when the shadow's coverage reaches the whole language, and the subset - number stays recorded so the curve from subset to full checker is public. + judged, at 0.40. The Prism side checks the subset the bootstrap workbench + supports, so the number says what a subset costs, not what the full checker + will. That subset is no longer the pure first-order one it was: this release + added written effect rows, parameterized effect labels, shared handler effect + evidence, and generalization of local types, and the Prism side nearly doubled + paying for them while the ratio rose from 0.24. That is the curve this row + exists to publish and it is moving against the claim, which is the expected + shape, since the cheapest part of a checker is the part written first. Both + counting asymmetries push the ratio up rather than down: the Prism files carry + their own type definitions and the artifact decoding, while the Rust side + counts the inference engine alone. The threshold binds when the shadow's + coverage reaches the whole language, and every subset number stays recorded so + the curve from subset to full checker is public. ## Cost @@ -99,39 +111,47 @@ it, and a scoreboard printing only the flattering half of that pair is advertising. The size half above is computed from the tree and checked by CI. This half is not: it is measured by hand and carries its provenance instead. -Lex layers measured 2026-07-28 and the parse layer 2026-08-01 with `just -lexperf` on an Apple M5 running macOS 26.3, release profile, the Prism side -compiled to a native binary. The absolute rates belong to that machine; the -ratio is the figure that carries across hosts, and `just lexperf` reprints all -of it. - -| workload | layer | Rust MB/s | Prism MB/s | ratio | Rust peak | Prism peak | -| -------- | ------ | --------: | ---------: | ----: | --------: | ---------: | -| stdlib | raw | 217.6 | 8.223 | 26x | 126.2M | 154.7M | -| example | raw | 174.0 | 5.280 | 33x | 134.7M | 233.4M | -| flat | raw | 143.7 | 4.063 | 35x | 184.6M | 327.9M | -| comments | raw | 1127.0 | 32.292 | 35x | 29.9M | 15.4M | -| nesting | raw | 67.4 | 2.873 | 23x | 476.6M | 871.7M | -| interp | raw | 58.7 | 1.325 | 44x | 173.1M | 313.6M | -| stdlib | layout | 63.8 | 3.083 | 21x | 202.0M | 183.0M | -| example | layout | 45.6 | 2.065 | 22x | 200.3M | 287.4M | -| flat | layout | 43.6 | 1.499 | 29x | 274.5M | 406.3M | -| comments | layout | 853.1 | 31.914 | 27x | 25.1M | 15.4M | -| nesting | layout | 10.5 | 0.084 | 126x | 48.3M | 68.9M | -| interp | layout | 31.0 | 0.857 | 36x | 129.7M | 193.4M | -| stdlib | parse | 22.4 | 1.667 | 13x | 299.0M | 231.4M | -| example | parse | 16.5 | 1.207 | 14x | 322.9M | 346.9M | -| flat | parse | 13.8 | 0.890 | 16x | 294.6M | 269.4M | -| comments | parse | 631.4 | 15.867 | 40x | 30.7M | 15.6M | -| nesting | parse | 3.8 | 0.065 | 59x | 27.0M | 37.9M | -| interp | parse | 11.8 | 0.569 | 21x | 254.7M | 278.6M | +Measured 2026-08-23, all three layers in one `just lexperf` run, on an Apple M5 +running macOS 26.3, release profile, the Prism side compiled to a native binary. +The absolute rates belong to that machine; the ratio is the figure that carries +across hosts, and `just lexperf` reprints all of it. Each class climbs a +doubling ladder until the Prism side crosses two seconds, so a row is measured +at whatever size that ladder reached and the `at KiB` column carries it: the two +peak columns compare to each other within a row and to nothing across rows. + +| workload | layer | at KiB | Rust MB/s | Prism MB/s | ratio | Rust peak | Prism peak | +| -------- | ------ | -----: | --------: | ---------: | ----: | --------: | ---------: | +| stdlib | raw | 4096 | 210.6 | 7.396 | 28x | 130.8M | 164.3M | +| example | raw | 4095 | 175.6 | 5.210 | 34x | 135.6M | 233.1M | +| flat | raw | 4096 | 147.1 | 4.102 | 36x | 184.7M | 328.0M | +| comments | raw | 4096 | 1024.1 | 29.956 | 34x | 30.0M | 15.5M | +| nesting | raw | 4096 | 75.7 | 3.019 | 25x | 476.7M | 871.8M | +| interp | raw | 4096 | 59.0 | 1.534 | 38x | 173.3M | 307.9M | +| corpus | raw | 1282 | 206.0 | 7.648 | 27x | 35.9M | 52.5M | +| stdlib | layout | 4096 | 62.1 | 4.264 | 15x | 214.3M | 242.7M | +| example | layout | 4095 | 47.0 | 2.993 | 16x | 201.3M | 355.7M | +| flat | layout | 4096 | 43.6 | 2.304 | 19x | 274.7M | 493.8M | +| comments | layout | 4096 | 824.0 | 29.753 | 28x | 30.1M | 15.5M | +| nesting | layout | 4096 | 9.9 | 1.265 | 8x | 709.1M | 1272.4M | +| interp | layout | 4096 | 31.7 | 1.187 | 27x | 254.6M | 456.1M | +| corpus | layout | 1282 | 59.9 | 4.036 | 15x | 61.6M | 76.9M | +| stdlib | parse | 4109 | 26.7 | 2.685 | 10x | 298.9M | 276.8M | +| example | parse | 4102 | 19.0 | 1.848 | 10x | 323.5M | 414.3M | +| flat | parse | 4096 | 15.6 | 1.359 | 11x | 577.9M | 616.8M | +| comments | parse | 4096 | 673.3 | 14.743 | 46x | 25.9M | 15.7M | +| nesting | parse | 2048 | 3.9 | 0.535 | 7x | 359.7M | 669.1M | +| interp | parse | 2048 | 12.5 | 0.798 | 16x | 254.7M | 286.5M | +| corpus | parse | 1282 | 25.2 | 2.554 | 10x | 88.3M | 85.9M | Every workload class the harness offers is above except `modules`, which it -flagged on both layers as launch dominated: most of the Prism wall clock was -process startup, subtracted rather than measured. Its ratio would be an artifact -of that subtraction, so it is named here instead of quoted. - -Log-log slopes of time against input size came back between 0.94 and 1.07 on +flagged on all three layers as launch dominated: most of the Prism wall clock +was process startup, subtracted rather than measured. Its ratio would be an +artifact of that subtraction, so it is named here instead of quoted. The +`corpus` class is the one input that is not synthetic, being the 110 committed +modules concatenated whole, which is why its ladder stops at the size the tree +actually is rather than at a doubling. + +Log-log slopes of time against input size came back between 0.91 and 1.07 on both sides, which is to say linear on both. The Prism lexer is a constant factor behind rather than asymptotically worse, and the size of that constant is the thing to keep reporting. @@ -139,24 +159,43 @@ thing to keep reporting. Peak resident set is the other half of the cost, and it does not track throughput: the Prism side peaks higher on most classes and lower on the comment-heavy one, where it allocates less per byte of input than the token -stream the Rust side materializes. +stream the Rust side materializes. Read it against `at KiB` and never against +the previous reading of this table, which carried no such column: a class that +got faster climbs further up the ladder before the two-second cut and reports a +larger peak for that reason alone. + +Against the readings this table replaces, the two structured layers closed most +of the way and the raw layer barely moved. Layout went from 21x to 15x on the +standard library and from 126x to 8x on the deeply nested class; parse went from +13x to 10x and from 59x to 7x on the same two. The comment-heavy class is the +one that went the other way, 40x to 46x at parse. This run measured the tree, +not the cause: nothing here attributes the move to a particular change, and the +earlier figures were taken on two separate days against a table that did not +record the size each row reached. Where the other pairs stand on cost: - **lexer and layout**: measured, in the table above. -- **parser**: measured, in the table above; a second receipt reproduced every - ratio within noise. +- **parser**: measured, in the table above, from the one run that table + transcribes; the duplicate receipt that backed the earlier reading of this + layer was not repeated, so these rates carry a single receipt. - **surface AST**: declarations, so nothing executes and there is no cost to report. - **syntax codecs**: executes, but no paired driver runs the same bytes through both sides, so the ratio is unmeasured rather than favorable. -- **checker**: measured 2026-08-14 on an Apple M5, release profile: the checker - compiled to a native binary and run to full parity on the committed bootstrap - fixture's exported artifacts takes 625 ms median of 30 against 11.6 ms median - of 20 for the Rust typecheck phase on the same 270-definition universe, about - 54x, with artifact decode inside the Prism figure and outside the Rust one; - the shipped interpreted workbench measures 1.9 s end to end on the same - fixture. +- **checker**: the reproducible figure is end to end: the shipped workbench, + which is `just tc` on the committed bootstrap fixture with the release binary + hosting the interpreter, takes 2.29 s median of 20 on an Apple M5 measured + 2026-08-23, against 1.9 s for the same fixture on 2026-08-14, the workbench + having grown the effect-row and local-generalization coverage named above in + between. The pair that produced the 54x, 625 ms for the checker compiled to a + native binary and run to full parity on the fixture's exported artifacts + against 11.6 ms for the Rust typecheck phase on the same 270-definition + universe, was measured 2026-08-14 on an apparatus that was never committed, + with artifact decode inside the Prism figure and outside the Rust one. It is + carried here as vintage rather than as a number this tree can re-derive, and a + committed driver that isolates the Rust phase and the compiled checker over + one universe is what would make that ratio quotable again. ## Retirements diff --git a/docs/src/compiler.md b/docs/src/compiler.md index 55a2503b..ad7ce78b 100644 --- a/docs/src/compiler.md +++ b/docs/src/compiler.md @@ -19,7 +19,7 @@ Fifteen ideas structure the whole compiler. Everything later in this chapter is 10. **Backends render; the emitter decides.** One shared emitter owns every semantic decision, and a backend is only instruction spelling behind a small trait, so targets cannot drift apart semantically and their agreement is a continuous cross-check. The interpreter, consuming pre-lowering Core, is the reference oracle every native backend must match byte for byte.[^p-backends] 11. **The store remembers; it does not decide.** A cached artifact may accelerate the canonical pipeline only when its identity and phase invariants validate. A cache hit can reuse an answer; it can never become a second semantic authority or invent an answer the uncached compiler would not produce. 12. **Every guarantee names its witness.** A phase type, an independent verifier, an interpreter comparison, a Lean theorem, a solver receipt, a digest, and a replay certificate establish different things. Every claim says which witness supports it, what that witness checked, and what remains trusted; no claim is allowed to grow stronger in the retelling. -13. **Nothing is ambient; every dependency is data.** The world a program touches is spelled in its row; the moments it observes are frames in its trace; the identity of a definition is the hash of its behavior; the origin of every artifact is an edge in its lineage. What is not declared cannot happen within the guarantee; what happened can be replayed; what exists can be named; what was produced can be traced.[^p-no-ambient] +13. **Nothing is ambient; every dependency is data.** The world a program touches is spelled in its row; the moments it observes are frames in its trace; the identity of a definition is the hash of its semantic content; the origin of every artifact is an edge in its lineage. What is not declared cannot happen within the guarantee; what happened can be replayed; what exists can be named; what was produced can be traced.[^p-no-ambient] 14. **The compiler should be fun to hack on.** If it is not fun, what is the point? Prism does not need to be useful or do anything in particular; it just needs to be fun. [^p-accountability]: "Effect" in this principle is the broad semantic sense, not a claim that determinism and termination are surface row labels. Prism realizes the account through separate mechanisms: the effect row bounds permitted authority, totality analysis records what is known about return, the trace fixes actual external observations, deterministic semantics relates those inputs to one result, hashes name exact content, and lineage records the claimed relation and whatever validation or replay evidence was actually obtained. A digest alone does not prove behavior; each part is weaker alone, and lineage must say honestly when an edge was recorded, rehashed, structurally checked, or replay-verified. @@ -544,7 +544,7 @@ The [`stable` block](./spec.md#stable-blocks) is also pure desugar: each rung be ## 6. Type and Effect Inference {#type-and-effect-inference} -Type inference is the bidirectional, higher-rank algorithm of [Dunfield & Krishnaswami (2013)](bibliography.md#dunfield-krishnaswami-2013); the surface rules are in [types and kinds](./spec.md#types-and-kinds). Type classes elaborate to dictionary-passing: a constraint becomes a hidden parameter, resolved to a global instance, a passed dictionary, or a projection of a superclass dictionary. +Type inference is the bidirectional, higher-rank algorithm of [Dunfield & Krishnaswami (2013)](bibliography.md#dunfield-krishnaswami-2013); the surface rules are in [types and kinds](./spec.md#types-and-kinds). Type classes elaborate to dictionary-passing: a constraint becomes a hidden parameter, resolved to a global instance, a passed dictionary, or a projection of a superclass dictionary. Existential solving fails closed: a missing, forward, or already-solved type variable is an internal error rather than a release-mode no-op, row-scope escapes are diagnosed, and standalone expressions finish the same deferred checks as whole programs before their facts are frozen. Instances are global, but each records its defining module, so coherence is checked by provenance. Resolution is coherent: for each `(class, type-head)` there is exactly one canonical instance, and implicit resolution always selects it. A single instance for a head is canonical automatically. When two or more instances share a head, one must be designated with a top-level `canonical Class(Head) = name` declaration (see [coherence and resolution](./spec.md#coherence-and-resolution)). An undesignated overlap is a hard error reported at definition, naming the candidates and their modules, with a source caret when they point into the program being compiled. An orphan instance (defined apart from both its class and its head type) is reported as a warning. An explicit override is written at the use site as a trailing `using` argument, `f(args, using name)`, which changes nothing else's resolution. @@ -560,7 +560,7 @@ Indexing (`a[i]`, `a[i] := v`) is resolved the same way the `print`/interpolatio A bracket with two or more indices lowers to a list-keyed index for the tensor's strided lookup. A receiver whose type is still an unsolved existential when first synthesized (a `var` indexed before its initializer fixes its state type) defers to one pass at the end of the declaration, after the initializer has constrained it. Concrete indexing is a closed, wired dispatch rather than a class or type-system extension; the desugar targets are `index` and `index_set`. -Effect-row inference is **principal**: each declaration infers its most general row from its body alone. The row unifier discovers every label on its own (a **row** is a function's effect set; see [types and kinds](./spec.md#types-and-kinds)) from direct performs, applied effect-carrying callees, builtin rows, and `mask`. At a call it adds the callee's row to the caller's **ambient row** (the effect set accumulated for the body so far), and a handler removes the operations it discharges. The row is the single source of truth: there is no separate set-pass seed and no subset reconciliation against one. +Effect-row inference is **principal**: each declaration infers its most general row from its body alone. The row unifier discovers every label on its own (a **row** is a function's effect set; see [types and kinds](./spec.md#types-and-kinds)) from direct performs, applied effect-carrying callees, builtin rows, and `mask`. At a call it adds the callee's row to the caller's **ambient row** (the effect set accumulated for the body so far), and a handler removes the operations it discharges. The row alone determines the effect set: there is no separate set-pass seed and no subset reconciliation against one. A syntactic **set-pass** (a pass that computes a _set_ of operation labels by a call-graph fixpoint) still runs, but only to feed the syntactic purity checks: it confirms a `konst` declaration and a declared-pure instance method perform nothing. It no longer seeds the row. After lowering, `reconcile_effects` checks the operations the lowered code actually performs against the inferred row, and the interpreter parity oracle (see [verification](#verification)) is the final backstop. Effect lowering computes its own per-function **latent** operation set by an independent call-graph fixpoint (see [effect lowering](#effect-lowering)), so the two phases no longer share the set-pass result. @@ -871,7 +871,7 @@ Concretely, the HIR is not a new tree. It is the desugared surface tree `Expr

, newtype_ctors: &BTreeSet, env: &VerifyEnv, -) -> Result<(TypedCore

, u64), Error> { +) -> Result<(UncheckedTypedCore

, u64), Error> { Ok(match pass { CorePass::Fuse => { let (next, stats) = fuse_typed(core); (next, stats.ticks()) } - CorePass::EraseNewtypes => { - let (next, stats) = erase_newtypes_typed(core, newtype_ctors, env); - (next, stats.ticks()) - } CorePass::Specialize => { let (next, stats) = specialize_typed(core).map_err(Error::from)?; (next, stats.ticks()) } - CorePass::Simplify => { - let (next, stats) = simplify_typed(core).map_err(Error::from)?; - (next, stats.ticks()) - } CorePass::Inline => { let (next, stats) = inline_typed(core); (next, stats.ticks()) } - CorePass::Cse => { - let (next, stats) = cse_typed(core); - (next, stats.ticks()) + CorePass::EraseNewtypes | CorePass::Simplify | CorePass::Cse => { + return run_typed_local_transform(pass, core.into_unchecked(), newtype_ctors, env); } }) } @@ -254,7 +245,7 @@ fn optimizer_identity(members: &[Sym], stage: PassStage, pass: CorePass) -> Stri fn run_typed_local_pass( core: TypedCore

, query: &TypedSccQuery<'_>, -) -> Result, Error> { +) -> Result, Error> { let erased = core.clone().erase(); let groups = scc_groups(&erased); let digests = core @@ -290,7 +281,7 @@ fn run_typed_local_pass( let mut kept = BTreeMap::new(); let mut to_run = BTreeMap::new(); - for function in core.into_functions() { + for function in core.into_unchecked().into_functions() { if skip.contains(&function.name()) { kept.insert(function.name(), function); } else { @@ -310,9 +301,9 @@ fn run_typed_local_pass( let results = QueryScheduler::new(query.cfg.flags.query_threads).map_ordered(&inputs, |group| { stacker::maybe_grow(TYPED_PASS_STACK, TYPED_PASS_STACK, || { - run_typed_pass( + run_typed_local_transform( query.pass, - TypedCore::

::from_functions(group.clone()), + UncheckedTypedCore::

::new(group.clone()), query.newtype_ctors, query.env, ) @@ -323,6 +314,17 @@ fn run_typed_local_pass( let mut transformed = BTreeMap::::new(); for (((members, key), input), result) in pending.iter().zip(&inputs).zip(results) { let output = result?; + let expected_names = members.iter().copied().collect::>(); + let output_names = output + .iter() + .map(TypedCoreFn::name) + .collect::>(); + if output.len() != members.len() || output_names != expected_names { + return Err(Error::InternalInvariant(format!( + "SCC-local pass changed the global definitions in `{}`", + query.pass.name() + ))); + } let fixed_point = output == *input; if fixed_point { store_fixed_point(query.store, key, members, query.stage, query.pass)?; @@ -368,7 +370,35 @@ fn run_typed_local_pass( ) }) .collect::, Error>>()?; - Ok(TypedCore::from_functions(fns)) + Ok(UncheckedTypedCore::new(fns)) +} + +fn run_typed_local_transform

( + pass: CorePass, + core: UncheckedTypedCore

, + newtype_ctors: &BTreeSet, + env: &VerifyEnv, +) -> Result<(UncheckedTypedCore

, u64), Error> { + Ok(match pass { + CorePass::EraseNewtypes => { + let (next, stats) = erase_newtypes_typed(core, newtype_ctors, env); + (next, stats.ticks()) + } + CorePass::Simplify => { + let (next, stats) = simplify_typed(core).map_err(Error::from)?; + (next, stats.ticks()) + } + CorePass::Cse => { + let (next, stats) = cse_typed(core); + (next, stats.ticks()) + } + CorePass::Fuse | CorePass::Specialize | CorePass::Inline => { + return Err(Error::InternalInvariant(format!( + "whole-program pass routed through SCC-local runner: {}", + pass.name() + ))); + } + }) } fn typed_query_key( @@ -415,7 +445,15 @@ fn load_fixed_point( let Some(object_hash) = store.get_query(OPT_SCC_QUERY, key)? else { return Ok(false); }; - let bytes = store.get(&object_hash)?; + // A query binding surviving a swept object is a normal cache miss, not + // corruption: gc (crate::store::disk::gc) prunes objects/meta by age + // without touching queries/index, so a stale binding must fall back to + // recompute exactly like an absent binding does above. + let bytes = match store.get(&object_hash) { + Ok(bytes) => bytes, + Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(false), + Err(e) => return Err(Error::Io(e)), + }; if bytes.len() > MAX_OPT_SCC_ARTIFACT_BYTES { return Err(corrupt("optimized SCC artifact exceeds the size limit")); } diff --git a/src/driver/dump.rs b/src/driver/dump.rs index 9d506db5..b303215b 100644 --- a/src/driver/dump.rs +++ b/src/driver/dump.rs @@ -47,9 +47,9 @@ use super::identity::{native_kont_table_of, NativeKontIdentityRows}; use super::module_graph::module_graph; use super::report::types_section; use super::{ - check_on, elaborated, frontend, hash_meta, lowered_core, prelude_fn_names, stdlib_hash, - strip_prelude, tooltip_checked_on, typed_effect_facts, typed_effect_plan, typed_tier_explain, - Config, WireKind, NAMESPACE_FORMAT, + check_on, elaborated, front_verdict_on, frontend, hash_meta, lowered_core, prelude_fn_names, + rc_borrow_sigs, stdlib_hash, strip_prelude, tooltip_checked_on, typed_effect_facts, + typed_effect_plan, typed_tier_explain, Config, WireKind, NAMESPACE_FORMAT, }; /// Format tag shared by all three usage-summary projections (`usage-summary`, @@ -180,6 +180,34 @@ pub fn dump_on(phase: &str, src: &str, roots: &[Root], cfg: &Config) -> Result { + let (program, verdict) = front_verdict_on(src, roots, cfg)?; + let map = SourceMap::new(src); + let (user, base) = (map.user(), map.prelude_len()); + let doc = TcRejection { + schema: TC_REJECTION_SCHEMA, + compiler: COMPILER_VERSION, + status: if verdict.is_some() { + TC_STATUS_REJECTED + } else { + TC_STATUS_ACCEPTED + }, + error: verdict.as_ref().map(|error| rejection_row(error, base)), + source: ResolvedSource { + digest: hash_hex(user).to_string(), + text: user.to_string(), + }, + functions: resolved_syntax_body(&program, src), + }; + pretty_json(&doc) + } // The checker's output facts as the versioned front-end seam. // Per-declaration principal schemes and effect rows, and every per-node // fact checking recorded (resolution, dictionary evidence, numeric lane, @@ -443,8 +471,8 @@ pub fn dump_on(phase: &str, src: &str, roots: &[Root], cfg: &Config) -> Result { - let (program, _, core) = frontend(src, roots, cfg)?; - let sigs = borrow_sigs(&program); + let (program, checked, core) = frontend(src, roots, cfg)?; + let sigs = rc_borrow_sigs(&program, &checked, &core, cfg); Ok(pp_core_pretty(&reuse(&insert_rc(&core, &sigs)))) } "lowered" => { @@ -1445,6 +1473,55 @@ struct ResolvedFunction { body: ResolvedNode, } +const TC_REJECTION_SCHEMA: &str = "prism-tc-rejection-v1"; +// The two verdicts, spelled the way the artifact spells them. +const TC_STATUS_ACCEPTED: &str = "accepted"; +const TC_STATUS_REJECTED: &str = "rejected"; + +// The checker's verdict on a program, with the resolved tree it judged. The +// accepted case is `resolved-syntax` plus a status; the rejected case is the +// half no other seam exports, since every other one returns nothing when the +// checker refuses. A Prism-written checker is diffed on both: same tree in, +// same accept-or-refuse out, and on a refusal the same code and span. +#[derive(Serialize)] +struct TcRejection { + schema: &'static str, + compiler: &'static str, + status: &'static str, + #[serde(skip_serializing_if = "Option::is_none")] + error: Option, + source: ResolvedSource, + functions: Vec, +} + +// A refusal reduced to what a differential comparison can hold the two front +// ends to: the stable code, the phase that owns it, and the user-relative +// primary span. The rendered message is deliberately absent; wording is not a +// contract, and pinning it would make every diagnostic reword a fixture break. +#[derive(Serialize)] +struct RejectionRow { + code: &'static str, + phase: &'static str, + #[serde(skip_serializing_if = "Option::is_none")] + span: Option<[usize; 2]>, +} + +// Spans in a diagnostic are absolute in the prelude-prefixed source; the +// artifact's are user-relative, like every other exported span. A diagnostic +// pointing inside the prelude has no user-relative position, so it reports +// none rather than an index into text the artifact does not carry. +fn rejection_row(error: &Error, base: usize) -> RejectionRow { + let code = error.code(); + RejectionRow { + code: code.as_str(), + phase: code.phase().as_str(), + span: error + .primary_span() + .filter(|span| span.start >= base) + .map(|span| [span.start - base, span.end - base]), + } +} + // One node of a resolved expression tree: its NodeId (the join key into // `tc-facts`), its expression-form kind, its user-relative span, and its // immediate children in source order. The tree is the resolved Core-phase diff --git a/src/driver/dump_syntax.rs b/src/driver/dump_syntax.rs index 08329ac4..777231ea 100644 --- a/src/driver/dump_syntax.rs +++ b/src/driver/dump_syntax.rs @@ -23,7 +23,7 @@ use serde::Serialize; use serde_json::{json, Map, Value}; use crate::core::hash::hex; -use crate::error::{Error, LexError, ParseError, SourceMap, SyntaxFault}; +use crate::error::{Error, ErrorPhase, LexError, ParseError, SourceMap, SyntaxFault}; use crate::lex::{lex, lex_raw, LexSpanned}; use crate::parse::parse; use crate::syntax::ast::{ @@ -1184,9 +1184,6 @@ struct DiagnosticRow { related: Vec<[usize; 2]>, } -const DIAG_PHASE_LEX: &str = "lex"; -const DIAG_PHASE_PARSE: &str = "parse"; - /// Render the `syntax-diagnostics` seam for one source file: lex, then parse, /// and report every syntax-boundary refusal against the user-relative source. /// A file that lexes and parses cleanly exports the empty diagnostic list. @@ -1200,7 +1197,7 @@ pub(super) fn dump_syntax_diagnostics(full: &str) -> String { let at = e.offset(); diagnostics.push(DiagnosticRow { code: e.code(), - phase: DIAG_PHASE_LEX, + phase: ErrorPhase::Lex.as_str(), span: [at, at], message: e.to_string(), expected: Vec::new(), @@ -1212,7 +1209,7 @@ pub(super) fn dump_syntax_diagnostics(full: &str) -> String { let span = e.span(); diagnostics.push(DiagnosticRow { code: e.code(), - phase: DIAG_PHASE_PARSE, + phase: ErrorPhase::Parse.as_str(), span: [span.start, span.end], message: e.to_string(), expected: e.expected().to_vec(), diff --git a/src/driver/front.rs b/src/driver/front.rs index 96c23e97..5ef1f9ae 100644 --- a/src/driver/front.rs +++ b/src/driver/front.rs @@ -35,7 +35,8 @@ use super::input::{ use super::timing::{self, ArtifactKind, CountKey, Phase, RowExtras}; use super::verify::{fip_check, reconcile_effects, replayable_check}; use super::{ - core_root_digest, dupes, emit_warning, emit_warnings, lint_surface, stdlib_hash, Config, + core_root_digest, dupes, emit_warning, emit_warnings, lint_surface, stdlib_hash, + validated_elaborated_core, Config, }; const RAW_FRONT_QUERY_SCHEMA: &[u8] = b"prism-session-front-v1"; @@ -78,8 +79,8 @@ pub(super) enum FrontRequest { IdentityValidated, /// The identity surface plus the per-node type strings, for the code index: /// it needs the same pre-optimizer Core every address is taken over *and* the - /// types a reader hovers. Collection only fills side tables, so the Core — and - /// therefore every hash — is byte-identical to `Identity`. + /// types a reader hovers. Collection only fills side tables, leaving Core and + /// its hashes byte-identical to `Identity`. IdentityTooltips, /// Typecheck-only analysis with per-node type/effect strings, for /// `dump typespans` and static documentation tooltips. @@ -495,6 +496,34 @@ fn front_key_for(schema: &[u8], input: &str, cfg: &Config, opts: FrontOpts) -> S h.finalize().to_hex().to_string() } +/// The resolved program and the checker's verdict on it. +/// +/// Every other entry point returns nothing at all when the checker refuses, so +/// no front-end artifact exists for a rejected program and a negative oracle +/// has nothing to compare against. Desugar runs before typechecking, so the +/// resolved tree of a rejected program was already built; this hands it back +/// alongside the refusal instead of discarding both. The policy is the one the +/// accepting artifacts use, so an accepted program's tree is identical here. +/// +/// Not cached: the session cache stores fronts, and a refusal is not one. +/// +/// # Errors +/// Fails on lex, parse, module, or contract errors. Those refuse before a +/// resolved tree exists, so there is nothing to report a verdict against. +pub(super) fn run_front_verdict( + src: &str, + roots: &[Root], + cfg: &Config, +) -> Result<(Program, Option), Error> { + let opts = FrontRequest::TypedTooltips.policy(); + let prepared = prepare_front(src, roots, cfg, opts)?; + let program = prepared.program.clone(); + match finish_front(src, cfg, opts, prepared) { + Ok(_) => Ok((program, None)), + Err(error) => Ok((program, Some(error))), + } +} + fn run_front_uncached( src: &str, roots: &[Root], @@ -651,6 +680,19 @@ fn finish_front( identity_core: None, }); } + // The instrumented tooltip checker delimits a fresh effect row around every + // node, so its zonked schemes can be alpha-variants of the plain judgment's + // (same types, differently ordered quantifiers), and every content hash + // folds the rendered scheme. Nothing presentation-driven may reach + // elaboration or a hash, so past the checked stop the judgment is re-taken + // plainly and only the per-node tooltip strings are kept. + let mut checked = if opts.typed_tooltips { + let mut canonical = typecheck(&program)?; + canonical.facts.adopt_tooltips(checked.facts); + canonical + } else { + checked + }; let elaboration = timing::timed_res( timer, Phase::Elaborate, @@ -712,9 +754,9 @@ fn finish_front( core: typed, verify_env, }), - core: Some(ElaboratedCore::new(core)), + core: Some(validated_elaborated_core(core)?), #[cfg(feature = "native")] - identity_core: Some(ElaboratedCore::new(identity_core)), + identity_core: Some(validated_elaborated_core(identity_core)?), }) } @@ -817,9 +859,9 @@ mod typed_pass_route_tests { let (_, _, warm_compatibility, warm_typed, warm_env) = warm; assert_eq!(session.stats().hits, 1); - crate::core::verify_typed_core(&cold_typed, &cold_env) + crate::core::audit_typed_core(&cold_typed, &cold_env) .expect("cold retained pre result verifies"); - crate::core::verify_typed_core(&warm_typed, &warm_env) + crate::core::audit_typed_core(&warm_typed, &warm_env) .expect("cached retained pre result verifies"); assert_eq!(cold_typed, warm_typed, "the cache clones typed witnesses"); assert_eq!(&cold_typed.erase(), &*cold_compatibility); diff --git a/src/driver/identity.rs b/src/driver/identity.rs index dac7dba0..a3df1bd3 100644 --- a/src/driver/identity.rs +++ b/src/driver/identity.rs @@ -40,7 +40,10 @@ use crate::tc::parse_checked_signature; use crate::types::{Checked, Env, Type, TypecheckSeed}; use super::front::{run_front, Front, FrontRequest}; -use super::{elaborated, hash_meta, with_prelude, Config, WireKind, NAMESPACE_ARTIFACT_KIND}; +use super::{ + elaborated, hash_meta, stage_validation_error, with_prelude, Config, WireKind, + NAMESPACE_ARTIFACT_KIND, +}; #[cfg(feature = "native")] use super::{ArtifactField, ArtifactIdentity}; @@ -147,9 +150,9 @@ fn with_konsts( checked: &Checked, core: &ElaboratedCore, ) -> Result { - let mut core = core.clone(); - core.core_mut().fns.extend(konst_fns(program, checked)?); - Ok(core) + core.clone() + .with_functions(konst_fns(program, checked)?) + .map_err(|violations| stage_validation_error("elaborated", &violations)) } // The layers of an already-augmented program (see [`with_konsts`]). @@ -630,10 +633,10 @@ pub struct PublicDef { /// /// Unlike the driver's cache-bust query salts, this is a real content-identity /// version: it is hashed into `interface_digest`, so its value must not be -/// renumbered casually (a change reseats every interface digest). `v3` is simply -/// the current format; there is no legacy reader, a non-`v3` document is rejected +/// renumbered casually (a change reseats every interface digest). `v4` is simply +/// the current format; there is no legacy reader, a non-`v4` document is rejected /// outright in `validate`. -pub const MODULE_INTERFACE_FORMAT: &str = "prism-module-interface-v3"; +pub const MODULE_INTERFACE_FORMAT: &str = "prism-module-interface-v4"; /// One deterministic semantic row exported to an importing checker. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -774,10 +777,12 @@ pub fn public_surface( roots: &[Root], ) -> Result, Error> { let exports = parse(entry_src)?.program.exports; - let (program, checked, mut core) = elaborated(full_src, roots)?; + let (program, checked, core) = elaborated(full_src, roots)?; // Top-level constants inline at use sites, so lift them to zero-param CoreFns // for their own behavior hash, exactly as the stdlib fingerprint does. - core.core_mut().fns.extend(konst_fns(&program, &checked)?); + let core = core + .with_functions(konst_fns(&program, &checked)?) + .map_err(|violations| stage_validation_error("elaborated", &violations))?; let defs = hash_program( &core, &hash_meta(&checked, &borrow_sigs(&program), &fip_annots(&program)), diff --git a/src/driver/interface.rs b/src/driver/interface.rs index 18a5d6b7..411b237c 100644 --- a/src/driver/interface.rs +++ b/src/driver/interface.rs @@ -6,8 +6,8 @@ use crate::sym::Sym; use crate::syntax::ast::{Grade, Program}; use crate::types::ty::Kind; use crate::types::{ - Canon, Checked, ClassInfo, CtorInfo, DataInfo, EffOpInfo, Env, InstInfo, InstKeys, Type, - TypecheckSeed, + Canon, Checked, ClassInfo, CtorInfo, DataInfo, EffOpInfo, Env, InstInfo, InstKeys, NominalRepr, + Type, TypecheckSeed, }; use super::identity::{interface_entry, ModuleInterface, ModuleInterfaceEntry}; @@ -72,6 +72,35 @@ struct DataPayload { params: Vec, param_kinds: Vec, ctors: Vec, + repr: NominalReprWire, +} + +#[derive(Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +enum NominalReprWire { + BoxedCell, + Transparent, + Vec128, +} + +impl From for NominalReprWire { + fn from(repr: NominalRepr) -> Self { + match repr { + NominalRepr::BoxedCell => Self::BoxedCell, + NominalRepr::Transparent => Self::Transparent, + NominalRepr::Vec128 => Self::Vec128, + } + } +} + +impl From for NominalRepr { + fn from(repr: NominalReprWire) -> Self { + match repr { + NominalReprWire::BoxedCell => Self::BoxedCell, + NominalReprWire::Transparent => Self::Transparent, + NominalReprWire::Vec128 => Self::Vec128, + } + } } #[derive(Serialize, Deserialize)] @@ -203,6 +232,7 @@ pub(super) fn metadata_entries( } else { info.ctors.clone() }, + repr: info.repr.into(), }, )?); if !opaques.contains(name) { @@ -337,6 +367,7 @@ pub(super) fn rehydrate(interface: &ModuleInterface) -> Result Result<(Program, Option), Error> { + run_front_verdict(src, roots, cfg) +} + /// The public validity verdict behind `prism check`. /// /// Type-checks, elaborates, and runs every semantic validator (fip / replayable / @@ -610,6 +621,24 @@ fn prepared_core(src: &str, roots: &[Root], cfg: &Config) -> Result Result { + let (_program, _checked, core, _typed, _verify_env) = + run_front(src, roots, cfg, FrontRequest::Full)?.into_typed_pre(); + Ok(core) +} + // Interpreter-only typed-hole lane. Native/wasm/build never call this and keep // the ordinary `E1021` refusal before elaboration. fn prepared_core_deferred_holes( @@ -620,6 +649,32 @@ fn prepared_core_deferred_holes( prepared_core_with_opts(src, roots, cfg, FrontRequest::FullDeferredHoles) } +// The borrow masks the reference-count lanes consume. With borrow inference +// enabled, the declared masks are extended over the provably pure functions; +// every RC consumer in one compilation reads this single map, so caller and +// callee always agree on each call's convention. Definition identity +// (`hash_meta`) always reads the declared `borrow_sigs` instead: the inferred +// masks are a pure function of the checked source, a cost decision like a +// lowering tier, never part of what a definition is. +fn rc_borrow_sigs( + program: &Program, + checked: &Checked, + core: &Core, + cfg: &Config, +) -> Sigs { + let declared = borrow_sigs(program); + if !cfg.flags.borrow_infer { + return declared; + } + let pure_fns = checked + .decls + .iter() + .filter(|decl| decl.pure) + .map(|decl| Sym::new(&decl.name)) + .collect(); + infer_borrow_sigs(core, &pure_fns, &declared) +} + fn prepared_core_with_opts( src: &str, roots: &[Root], @@ -628,7 +683,7 @@ fn prepared_core_with_opts( ) -> Result { let (program, checked, core, typed, verify_env) = run_front(src, roots, cfg, request)?.into_typed_pre(); - let sigs = borrow_sigs(&program); + let sigs = rc_borrow_sigs(&program, &checked, &core, cfg); let lowered = lower_opt( typed, &verify_env, @@ -658,6 +713,22 @@ fn on_typed_lower_stack(f: impl FnOnce() -> T) -> T { stacker::maybe_grow(TYPED_LOWER_STACK, TYPED_LOWER_STACK, f) } +fn stage_validation_error(stage: &str, violations: &[String]) -> Error { + Error::InternalInvariant(format!( + "{stage} Core failed structural validation:\n{}", + violations.join("\n") + )) +} + +fn validated_elaborated_core(core: Core) -> Result { + ElaboratedCore::validate(core) + .map_err(|violations| stage_validation_error("elaborated", &violations)) +} + +fn validated_lowered_core(core: Core) -> Result { + LoweredCore::validate(core).map_err(|violations| stage_validation_error("lowered", &violations)) +} + fn lower_opt( typed: TypedCore, verify_env: &VerifyEnv, @@ -726,16 +797,16 @@ fn finish_lowered(lowered: LoweredSpine, sigs: &Sigs, cfg: &Config) -> Result Result { - let typed_owned = insert_typed_rc(lowered.core, sigs); - verify_typed_core(&typed_owned, &lowered.verify_env).map_err(typed_verification_error)?; - let typed_reused = reuse_typed(typed_owned); - verify_typed_core(&typed_reused, &lowered.verify_env).map_err(typed_verification_error)?; + let typed_owned = verify_typed_core(insert_typed_rc(lowered.core, sigs), &lowered.verify_env) + .map_err(typed_verification_error)?; + let typed_reused = verify_typed_core(reuse_typed(typed_owned), &lowered.verify_env) + .map_err(typed_verification_error)?; // The one semantic erasure on the native route: nothing downstream of this // call sees a typed node. let final_core = typed_reused.erase(); balanced(&final_core, sigs) .map_err(|error| Error::CodegenBackend(format!("ICE: rc imbalance: {error}")))?; - Ok(LoweredCore::new(final_core)) + validated_lowered_core(final_core) } fn lowered_core( @@ -744,7 +815,7 @@ fn lowered_core( cfg: &Config, ) -> Result<(Checked, LoweredCore, BTreeMap, Sigs), Error> { let (checked, sigs, lowered) = lowered_front(src, roots, cfg)?; - let core = on_typed_lower_stack(|| LoweredCore::new(lowered.core.clone().erase())); + let core = on_typed_lower_stack(|| validated_lowered_core(lowered.core.clone().erase()))?; Ok((checked, core, lowered.ctors, sigs)) } @@ -764,9 +835,9 @@ fn lowered_front( roots: &[Root], cfg: &Config, ) -> Result<(Checked, Sigs, LoweredSpine), Error> { - let (program, checked, _, typed, verify_env) = + let (program, checked, core, typed, verify_env) = run_front(src, roots, cfg, FrontRequest::Full)?.into_typed_pre(); - let sigs = borrow_sigs(&program); + let sigs = rc_borrow_sigs(&program, &checked, &core, cfg); let lowered = lower_opt( typed, &verify_env, @@ -809,9 +880,10 @@ fn lowered_spine_with_identity( roots: &[Root], cfg: &Config, ) -> Result<(Checked, LoweredSpine, Sigs, crate::core::Hashes), Error> { - let (program, checked, identity_core, _, typed, verify_env) = + let (program, checked, identity_core, core, typed, verify_env) = run_front(src, roots, cfg, FrontRequest::Full)?.into_compilation(); - let sigs = borrow_sigs(&program); + let declared = borrow_sigs(&program); + let sigs = rc_borrow_sigs(&program, &checked, &core, cfg); let hashes = if cfg.scheduler().retarget().is_some() { // Scheduler policy is execution configuration, never source identity. // The full path has already retargeted its surface program, so recover @@ -826,7 +898,7 @@ fn lowered_spine_with_identity( ), ) } else { - let metas = hash_meta(&checked, &sigs, &fip_annots(&program)); + let metas = hash_meta(&checked, &declared, &fip_annots(&program)); hash_program(&identity_core, &metas) }; let lowered = lower_opt( @@ -946,6 +1018,28 @@ mod typed_post_route_tests { finish_lowered(lowered, &sigs, &cfg).expect("typed final route"); } + #[test] + fn production_route_finishes_with_inferred_borrows() { + let mut cfg = Config::default(); + cfg.flags.borrow_infer = true; + cfg.flags.quiet = true; + // The row reaches `len` as the result of a call, which stays let-bound + // through optimization, so the borrowed position is covered by a named + // token at the call site (a literal constructor argument would force + // the parameter back to owned). + let src = "type Row = Tip | Node(Int, Row)\n\nfn build(n : Int) : Row =\n if n == 0 then Tip else Node(n, build(n - 1))\n\nfn len(r : Row) : Int =\n match r of\n Tip => 0\n Node(_, rest) => 1 + len(rest)\n\nfn main() : Int =\n len(build(2))\n"; + let (_, core, _, sigs) = reuse_lowered_core(src, &[], &cfg).expect("typed route"); + balanced(&core, &sigs).expect("balanced with inferred borrows"); + crate::core::residual_effects(&core).expect("no residual effect nodes"); + let mask = sigs + .get(&Sym::new("len")) + .expect("len earns an inferred loan"); + assert!( + mask.iter().any(|b| *b), + "len keeps at least one borrowed parameter" + ); + } + #[test] fn interpreter_preparation_returns_the_unlowered_core() { let src = crate::with_prelude( @@ -1145,8 +1239,9 @@ pub fn core_of(src: &str) -> Result { /// Fails on front-end errors. pub fn core_ir_full(full: &str, base: &Path) -> Result { let prelude = prelude_fn_names()?; - let (program, _, core) = frontend(full, &default_roots(base), &Config::from_env())?; - let sigs = borrow_sigs(&program); + let cfg = Config::from_env(); + let (program, checked, core) = frontend(full, &default_roots(base), &cfg)?; + let sigs = rc_borrow_sigs(&program, &checked, &core, &cfg); let optimized = reuse(&insert_rc(&core, &sigs)); Ok(pp_core_pretty(&strip_prelude(optimized, &prelude))) } diff --git a/src/driver/modules.rs b/src/driver/modules.rs index beabb0d0..b0e27ab9 100644 --- a/src/driver/modules.rs +++ b/src/driver/modules.rs @@ -1,4 +1,5 @@ use std::collections::{BTreeMap, BTreeSet}; +use std::io; use std::sync::OnceLock; use crate::error::Error; @@ -23,14 +24,15 @@ use super::input::field; use super::scheduler::QueryScheduler; use super::{Config, PRELUDE, ROOT_MODULE_NAME}; -const MODULE_CHECK_QUERY_SCHEMA: &[u8] = b"prism-module-check-query-v2"; -const CHECKED_INTERFACE_QUERY_SCHEMA: &[u8] = b"prism-checked-interface-query-v2"; +const MODULE_CHECK_QUERY_SCHEMA: &[u8] = b"prism-module-check-query-v3"; +const CHECKED_INTERFACE_QUERY_SCHEMA: &[u8] = b"prism-checked-interface-query-v3"; const CHECKED_INTERFACE_QUERY: &str = "checked-interface"; -// v5 keys the v4 body format by semantic tokens. It still never publishes a +// v6 keys the v4 body format by semantic tokens and explicit nominal +// representation evidence. It still never publishes a // body whose inferred declarations or node facts contain unification // metavariables: such a body cannot be parsed back as a checked signature, and // a cache must never turn a valid cold build into a warm-build failure. -const CHECKED_BODY_QUERY_SCHEMA: &[u8] = b"prism-checked-body-query-v5"; +const CHECKED_BODY_QUERY_SCHEMA: &[u8] = b"prism-checked-body-query-v6"; const CHECKED_BODY_QUERY: &str = "checked-body"; // v2 carries the checked handler-residual facts (known operations, opaque // effect labels, and an open-row marker). A v1 body cannot be promoted by @@ -75,7 +77,15 @@ pub struct ModuleCheckReport { pub root: Checked, /// True when the root checked body was rehydrated from the durable store. pub root_reused: bool, + /// Modules whose checked bodies exist this command: freshly checked, + /// session hits, and durable body hits. A module served from an + /// interface-only durable hit has no body to carry, so it appears in + /// `interfaces` but not here; consumers that need every module must read + /// `interfaces`. pub modules: Vec, + /// The public interface of every non-shipped module in the DAG, complete + /// regardless of which cache tier served each module. + pub interfaces: BTreeMap, /// Deterministically ordered reuse/recompilation explanations. pub decisions: Vec, } @@ -183,6 +193,7 @@ pub fn check_modules_on( root: super::check_on_in(src, roots, cfg)?, root_reused: false, modules: Vec::new(), + interfaces: BTreeMap::new(), decisions: Vec::new(), }); } @@ -405,6 +416,7 @@ pub fn check_modules_on( root, root_reused, modules: checked_modules.into_values().collect(), + interfaces, decisions, }) } @@ -783,6 +795,9 @@ impl CheckedBody { .into_iter() .map(crate::sym::Sym::from) .collect(), + // The serialized interface carries no body-effect witness, + // so a rehydrated declaration is never provably pure. + pure: false, }) }) .collect::, Error>>()?; @@ -846,7 +861,15 @@ impl DurableInterfaceCache { let Some(output) = self.store.get_query(CHECKED_INTERFACE_QUERY, key)? else { return Ok(None); }; - let bytes = self.store.get(&output)?; + // A query binding surviving a swept object is a normal cache miss, not + // corruption: gc (crate::store::disk::gc) prunes objects/meta by age + // without touching queries/index, so a stale binding must fall back to + // recompute exactly like an absent binding does above. + let bytes = match self.store.get(&output) { + Ok(bytes) => bytes, + Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None), + Err(e) => return Err(Error::Io(e)), + }; let actual = blake3::hash(&bytes).to_hex().to_string(); if actual != output { return Err(Error::ResolveModule(format!( @@ -870,7 +893,13 @@ impl DurableInterfaceCache { let Some(output) = self.store.get_query(CHECKED_BODY_QUERY, key)? else { return Ok(None); }; - let bytes = self.store.get(&output)?; + // See the matching comment in `load` above: a swept object behind a + // surviving query binding is a normal miss, not corruption. + let bytes = match self.store.get(&output) { + Ok(bytes) => bytes, + Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None), + Err(e) => return Err(Error::Io(e)), + }; let actual = blake3::hash(&bytes).to_hex().to_string(); if actual != output { return Err(Error::ResolveModule(format!( diff --git a/src/driver/native.rs b/src/driver/native.rs index 7f1ded69..5962895b 100644 --- a/src/driver/native.rs +++ b/src/driver/native.rs @@ -19,6 +19,16 @@ const NO_FP_CONTRACT_FLAG: &str = "-ffp-contract=off"; const NO_OVERRIDE_MODULE_WARNING_FLAG: &str = "-Wno-override-module"; const COMPILE_ONLY_FLAG: &str = "-c"; const OUTPUT_FLAG: &str = "-o"; +const LLD_FLAG: &str = "-fuse-ld=lld"; +const VERSION_FLAG: &str = "--version"; +/// Asks the driver where it would find a program, without building any job. +const PRINT_PROG_NAME_FLAG: &str = "-print-prog-name="; +const LLD_PROGRAM: &str = "ld.lld"; +const DEFAULT_LINKER_PROGRAM: &str = "ld"; +/// Stands in for a toolchain component whose own banner could not be read. +const UNKNOWN_COMPONENT: &str = "unavailable"; +/// Names the linker when the driver would not say which one it launches. +const PLATFORM_LINKER: &str = "platform"; /// Direct C-toolchain work performed by one native link. /// @@ -175,42 +185,167 @@ fn compile_object( Ok(ObjectCompileStats::Invoked(elapsed)) } -fn cc_version(cc: &str) -> (String, Option) { - static VERSIONS: OnceLock>> = OnceLock::new(); - let versions = VERSIONS.get_or_init(|| Mutex::new(BTreeMap::new())); - let mut versions = versions +/// The first line a toolchain probe prints, memoized for the process. +/// +/// Every probe is a subprocess launch on the critical path of a link and its +/// answer cannot change while the compiler runs, so each distinct command is +/// asked once. `None` reports that the command failed or said nothing; what +/// that absence means is the caller's to decide. +fn probe_line(cmd: &str, args: &[&str]) -> (Option, Option) { + static LINES: OnceLock>>> = OnceLock::new(); + let mut key = cmd.to_string(); + for arg in args { + key.push('\0'); + key.push_str(arg); + } + let lines = LINES.get_or_init(|| Mutex::new(BTreeMap::new())); + let mut lines = lines .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); - if let Some(version) = versions.get(cc) { - return (version.clone(), None); + if let Some(line) = lines.get(&key) { + return (line.clone(), None); } let started = Instant::now(); - let version = Command::new(cc) - .arg("--version") + let line = Command::new(cmd) + .args(args) .output() .ok() + .filter(|output| output.status.success()) .and_then(|output| String::from_utf8(output.stdout).ok()) .and_then(|output| output.lines().next().map(str::trim).map(str::to_string)) - .filter(|version| !version.is_empty()) - .unwrap_or_else(|| "unavailable".to_string()); + .filter(|line| !line.is_empty()); let elapsed = started.elapsed(); - versions.insert(cc.to_string(), version.clone()); - drop(versions); - (version, Some(elapsed)) + lines.insert(key, line.clone()); + drop(lines); + (line, Some(elapsed)) } -fn runtime_object_toolchain_context(cc: &str, args: &[String]) -> (String, Option) { - let (version, probe_time) = cc_version(cc); - let mut context = format!( - "target={}\0cc={cc}\0cc-version={}\0", - env!("PRISM_TARGET"), - version - ); - for arg in args { - context.push_str(arg); - context.push('\0'); +/// The cost of the toolchain probes one link makes. +#[derive(Default)] +struct ProbeCost { + count: usize, + time: Duration, +} + +impl ProbeCost { + /// Runs a probe and folds what it cost into the running total. A memoized + /// answer costs nothing and is not counted. + fn line(&mut self, cmd: &str, args: &[&str]) -> Option { + let (line, elapsed) = probe_line(cmd, args); + if let Some(elapsed) = elapsed { + self.count += 1; + self.time += elapsed; + } + line + } +} + +/// The absolute path the driver would launch for a linker program. +/// +/// This is a pure driver query: it builds no link job, needs no input file, and +/// a toolchain that does not have the program simply echoes the bare name back, +/// which is not a path and so reads as absent. +fn linker_path(cc: &str, program: &str, cost: &mut ProbeCost) -> Option { + let query = format!("{PRINT_PROG_NAME_FLAG}{program}"); + let path = cost.line(cc, &[&query])?; + let found = Path::new(&path).is_absolute() && Path::new(&path).is_file(); + found.then_some(path) +} + +/// Picks the linker for the final link, and names it for the fingerprint. +/// +/// `ThinLTO` defers code generation and final layout to the linker, so a build's +/// bytes are a function of its inputs only when the linker is one too. `ld.lld` +/// ships with the same LLVM the rest of the pipeline is pinned to and lays its +/// inputs out in a fixed order; the Linux default is whichever linker owns `ld` +/// on the host, which may be a parallel linker whose output permutes between +/// otherwise identical runs. So pin lld wherever the toolchain has it, and fall +/// back rather than fail where it does not: an installed release must keep +/// linking on a host that never shipped lld, and naming the fallback here keeps +/// any artifact from crossing between the two. Apple platforms keep `ld64`, +/// which orders its output deterministically already and is the linker their +/// SDK is built against. +fn resolve_linker(cc: &str, cost: &mut ProbeCost) -> (Option<&'static str>, String) { + if cfg!(target_os = "macos") { + return (None, PLATFORM_LINKER.to_string()); + } + let lld = linker_path(cc, LLD_PROGRAM, cost); + let flag = lld.is_some().then_some(LLD_FLAG); + let Some(path) = lld.or_else(|| linker_path(cc, DEFAULT_LINKER_PROGRAM, cost)) else { + return (flag, PLATFORM_LINKER.to_string()); + }; + // The banner moves when the linker is upgraded in place; the path does not, + // so prefer it and keep the path only as the coarser fallback. + let name = cost.line(&path, &[VERSION_FLAG]).unwrap_or(path); + (flag, name) +} + +/// The external C toolchain one native link runs through. +/// +/// Resolved once per link so the fingerprint that keys cached objects and the +/// command line that produces them can never disagree about which tools ran. +struct Toolchain { + cc: String, + cc_version: String, + /// Selects the linker, absent when the platform default is taken. + linker_flag: Option<&'static str>, + linker: String, + probes: usize, + probe_time: Duration, +} + +impl Toolchain { + /// Probes the compiler, and the linker the final link will hand its work to. + fn resolve() -> Self { + let cc = cc(); + let mut cost = ProbeCost::default(); + let cc_version = cost + .line(&cc, &[VERSION_FLAG]) + .unwrap_or_else(|| UNKNOWN_COMPONENT.to_string()); + let (linker_flag, linker) = resolve_linker(&cc, &mut cost); + Self { + cc, + cc_version, + linker_flag, + linker, + probes: cost.count, + probe_time: cost.time, + } + } + + /// The compile flags plus whatever selects the linker. + fn link_args(&self, args: &[String]) -> Vec { + let mut link = args.to_vec(); + link.extend(self.linker_flag.map(ToString::to_string)); + link + } + + /// Fingerprint of the toolchain behind a compiled object. + /// + /// Under `ThinLTO` an object holds bitcode and the linker is what turns it + /// into machine code, so the linker is named here alongside the compiler. + /// It does not itself change an object's bytes; naming it anyway is the + /// conservative reading, and it costs only a rebuild of a handful of small + /// runtime objects on the rare occasion a linker moves. What it buys is + /// that a cached object cannot quietly outlive the toolchain it was made + /// for, without anyone having to keep proving that bitcode is linker-blind. + fn object_context(&self, args: &[String]) -> String { + let Self { + cc, + cc_version, + linker, + .. + } = self; + let mut context = format!( + "target={}\0cc={cc}\0cc-version={cc_version}\0linker={linker}\0", + env!("PRISM_TARGET"), + ); + for arg in args { + context.push_str(arg); + context.push('\0'); + } + context } - (context, probe_time) } pub(super) fn cc_link( @@ -236,17 +371,16 @@ pub(super) fn cc_link_many( let first_ir = ir.first().ok_or_else(|| { Error::CodegenBackend("cannot link an empty backend artifact set".to_string()) })?; - let cc = cc(); + let toolchain = Toolchain::resolve(); + let cc = &toolchain.cc; let args = cc_args(cfg); - let (runtime_toolchain, probe_time) = runtime_object_toolchain_context(&cc, &args); + let runtime_toolchain = toolchain.object_context(&args); let rt_dir = out.with_extension("prism_rt.d"); let sources = write_runtime_for(&rt_dir, runtime_profile)?; let libm_archive = write_libm_archive(&rt_dir)?; let mut stats = CcLinkStats::default(); - if let Some(elapsed) = probe_time { - stats.probe_invocations += 1; - stats.probe_time += elapsed; - } + stats.probe_invocations += toolchain.probes; + stats.probe_time += toolchain.probe_time; // Program shards are independent compiler subprocesses writing distinct // objects, so they run under the bounded scheduler; results fold back in @@ -259,7 +393,7 @@ pub(super) fn cc_link_many( let object = rt_dir.join(format!("{name}.o")); let ir_bytes = fs::read(input)?; let cache = NativeArtifactCache::for_native_object(&name, &ir_bytes, cfg)?; - let object_stats = compile_object(&cc, &args, input, &object, cache.as_ref(), cfg)?; + let object_stats = compile_object(cc, &args, input, &object, cache.as_ref(), cfg)?; Ok((object, object_stats)) }, ); @@ -285,15 +419,14 @@ pub(super) fn cc_link_many( &runtime_toolchain, cfg, )?; - let object_stats = - compile_runtime_object(&cc, &args, source, &object, cache.as_ref(), cfg)?; + let object_stats = compile_runtime_object(cc, &args, source, &object, cache.as_ref(), cfg)?; stats.record_object(object_stats, true); runtime_objects.push(object); } let link_started = Instant::now(); - let result = Command::new(&cc) - .args(&args) + let result = Command::new(cc) + .args(toolchain.link_args(&args)) .args(&program_objects) .args(&runtime_objects) .arg(&libm_archive) @@ -313,7 +446,7 @@ pub(super) fn cc_link_many( } Ok(stats) } else { - Err(ir_failure(&cc, first_ir, &cc_out.stderr)) + Err(ir_failure(cc, first_ir, &cc_out.stderr)) } } diff --git a/src/driver/report.rs b/src/driver/report.rs index 124bccab..5e77261b 100644 --- a/src/driver/report.rs +++ b/src/driver/report.rs @@ -10,8 +10,7 @@ use std::collections::BTreeMap; use std::fmt::Write as _; use std::path::Path; -use crate::core::fbip::borrow_sigs; -use crate::core::{elaborate_typed, insert_rc, pp_core_pretty, reuse, Digest, ElaboratedCore}; +use crate::core::{elaborate_typed, insert_rc, pp_core_pretty, reuse, Digest}; use crate::error::Error; use crate::eval::{run, Rv}; use crate::lex::lex; @@ -19,6 +18,8 @@ use crate::parse::{parse, ParseResult}; use crate::resolve::{default_roots, Root}; use crate::types::Checked; +#[cfg(feature = "native")] +use crate::core::fbip::borrow_sigs; #[cfg(feature = "native")] use crate::core::{fip_annots, hash_program}; #[cfg(feature = "native")] @@ -33,7 +34,9 @@ use super::query::strip_target; use super::verify::{fip_check, replayable_check}; #[cfg(feature = "native")] use super::{finish_lowered, hash_meta, lower_opt}; -use super::{frontend, Config}; +// The rendered `fbip (rc)` section is not native-gated, so its borrow masks are +// needed on every target. +use super::{frontend, rc_borrow_sigs, validated_elaborated_core, Config}; pub(super) fn types_section(checked: &Checked) -> String { let mut s = String::new(); @@ -103,7 +106,13 @@ pub fn report_on(src: &str, roots: &[Root], cfg: &Config) -> String { } }; let (core, typed, verify_env) = elaboration.into_parts(); - let core = ElaboratedCore::new(core); + let core = match validated_elaborated_core(core) { + Ok(core) => core, + Err(error) => { + section(&mut out, "core (cbpv)", &render(error)); + return out; + } + }; section(&mut out, "core (cbpv)", pp_core_pretty(&core).trim_end()); if let Err(e) = fip_check(&program, &checked, &core) { @@ -116,7 +125,7 @@ pub fn report_on(src: &str, roots: &[Root], cfg: &Config) -> String { return out; } - let sigs = borrow_sigs(&program); + let sigs = rc_borrow_sigs(&program, &checked, &core, cfg); section( &mut out, "fbip (rc)", @@ -136,7 +145,12 @@ pub fn report_on(src: &str, roots: &[Root], cfg: &Config) -> String { finish_lowered(lowered, &sigs, cfg).map(|core| (core, ctors)) }) { Ok((lowered, ctors)) => { - let hashes = hash_program(&core, &hash_meta(&checked, &sigs, &fip_annots(&program))); + // Identity reads the declared masks; the augmented `sigs` above are + // a reference-count decision and never part of the program's hash. + let hashes = hash_program( + &core, + &hash_meta(&checked, &borrow_sigs(&program), &fip_annots(&program)), + ); match native_kont_table_of(&hashes, roots, cfg, NativeKontIdentityRows::Portable) .and_then(|native_kont_table| { emit_llvm_with_native_kont_table( diff --git a/src/driver/stable_lock.rs b/src/driver/stable_lock.rs index 3e3f5654..df788b25 100644 --- a/src/driver/stable_lock.rs +++ b/src/driver/stable_lock.rs @@ -55,9 +55,8 @@ pub fn derive(full: &str, roots: &[Root]) -> Result { Ok(derive_with_spans(full, roots)?.0) } -// Derive the manifest and, alongside it, each locked family's declaration span, -// so a drift diagnostic can point at the block. The span map is not serialized; it -// exists only to place the error. +// Derive the manifest and each locked family's declaration span. The span map is +// used only to place drift diagnostics. fn derive_with_spans( full: &str, roots: &[Root], diff --git a/src/driver/timing.rs b/src/driver/timing.rs index 6868d5d7..9283cca9 100644 --- a/src/driver/timing.rs +++ b/src/driver/timing.rs @@ -25,13 +25,15 @@ //! LLVM bitcode); //! 7. trailing `k=v` counts, emitted only when real and already cheap at that phase. -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Write as _; #[cfg(feature = "native")] use std::path::Path; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; +use crate::core::work::{self, WorkCounts}; + // The literal first field, the anchor a reader greps for to find a timing row. const ROW_TAG: &str = "phase"; // Width, in hex characters, of the abbreviated digest shown in an artifact key. @@ -175,6 +177,14 @@ pub(crate) enum CountKey { /// How many per-SCC bitcode shards the sharded backend emitted. #[cfg(feature = "native")] SccShards, + /// Core nodes this phase entered through the shared descent. + CoreVisits, + /// Core nodes this phase reconstructed through the shared descent. + RebuiltNodes, + /// The deepest descent observed so far in this compile. Unlike the two above + /// it is a running whole-compile maximum, not a per-phase delta, because a + /// maximum over a subrange is not a property anyone can act on. + MaxDepth, } impl CountKey { @@ -205,6 +215,9 @@ impl CountKey { Self::RuntimeObjectMisses => "runtime_object_misses", #[cfg(feature = "native")] Self::SccShards => "scc_shards", + Self::CoreVisits => "core_visits", + Self::RebuiltNodes => "rebuilt_nodes", + Self::MaxDepth => "max_depth", } } } @@ -216,6 +229,11 @@ impl CountKey { pub(crate) struct RowExtras { out: Option<(ArtifactKind, String)>, counts: Vec<(CountKey, usize)>, + // The same structural work the count fields display, kept unformatted so the + // sink can accumulate it. The row shows one invocation's delta; a tally wants + // to add them up, and re-parsing the rendered field to do so would make the + // display the source of truth. + work: WorkCounts, } impl RowExtras { @@ -234,6 +252,34 @@ impl RowExtras { self } + /// Attach the structural work this phase did, as the difference between two + /// whole-compile readings. + /// + /// A difference rather than a reset, so a phase that contains another phase + /// reports its own work including the inner one's, which is what containment + /// means. Zero visits are left off the row entirely: the front end descends no + /// Core, and a row of zeros reads as a measurement when it is an absence. + #[must_use] + fn work(mut self, before: WorkCounts, after: WorkCounts) -> Self { + self.work = WorkCounts { + visits: after.visits.saturating_sub(before.visits), + rebuilt: after.rebuilt.saturating_sub(before.rebuilt), + // Not a delta: a maximum over a subrange is not a property anyone can + // act on, so this stays the whole-compile reading. + max_depth: after.max_depth, + }; + if self.work.is_silent() { + return self; + } + let clamp = |n: u64| usize::try_from(n).unwrap_or(usize::MAX); + self.counts.extend([ + (CountKey::CoreVisits, clamp(self.work.visits)), + (CountKey::RebuiltNodes, clamp(self.work.rebuilt)), + (CountKey::MaxDepth, clamp(self.work.max_depth)), + ]); + self + } + #[cfg(feature = "native")] #[must_use] pub(crate) fn cc_link_stats(mut self, stats: super::native::CcLinkStats) -> Self { @@ -254,13 +300,45 @@ impl RowExtras { } } +/// Everything a phase accumulated across one whole compile. +/// +/// A phase can run more than once (a re-elaboration repeats the front end), and +/// only the first run is printed, so this is the only place the repeats survive. +/// That difference is the point: the row answers "what did this phase look like", +/// the tally answers "how many times did it happen and to what total", and a +/// receipt that quoted the row for the second question would silently drop every +/// repeat. +#[derive(Clone, Copy, Debug, Default)] +pub struct PhaseTally { + /// How many times the phase ran, printed or not. + pub invocations: usize, + /// Summed wall time over those runs. A reading of the machine, not a property + /// of the compilation: unlike the work counts it will differ run to run. + pub wall: Duration, + /// Summed structural work over those runs, with `max_depth` kept as a + /// maximum rather than a sum, since it already is one. + pub work: WorkCounts, +} + +impl PhaseTally { + fn add(&mut self, dt: Duration, work: WorkCounts) { + self.invocations += 1; + self.wall += dt; + self.work.visits += work.visits; + self.work.rebuilt += work.rebuilt; + self.work.max_depth = self.work.max_depth.max(work.max_depth); + } +} + // The mutable state a sink guards: the source digest (computed once, on the first -// phase that carries the source) and the set of phases already emitted (so a -// re-elaboration on the same compile does not double-print a phase). +// phase that carries the source), the set of phases already emitted (so a +// re-elaboration on the same compile does not double-print a phase), and the +// per-phase tallies, which unlike the emitted set keep counting past the first. #[derive(Debug, Default)] struct Inner { src_digest: Option, emitted: BTreeSet<&'static str>, + tallies: BTreeMap<&'static str, PhaseTally>, } /// The per-compile timing sink, installed on the CLI's [`Config`](super::Config). @@ -280,6 +358,21 @@ impl TimingSink { Self::default() } + /// What each phase accumulated, keyed by the stable phase name the timing + /// rows print in field 2 (`parse`, `opt.pre`, `rc`, and the rest). + /// + /// The structured read of what the rows only sample. A caller building a + /// receipt takes this rather than parsing stderr, so the row schema stays a + /// display and never becomes an interchange format. + #[must_use] + pub fn tallies(&self) -> BTreeMap<&'static str, PhaseTally> { + self.0 + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .tallies + .clone() + } + // The abbreviated source key, computing (once) the digest from the first // non-empty source seen. Later phases pass an empty source and read the cached // digest. @@ -308,13 +401,19 @@ impl TimingSink { status: CacheStatus, extras: &RowExtras, ) { - // First sight of this phase? A re-elaboration on the same compile repeats - // phases; the guard is released before any formatting or stderr write. + // Tally first, then ask whether to print: every invocation counts, only the + // first prints. A re-elaboration on the same compile repeats phases; the + // guard is released before any formatting or stderr write. let first = { let mut inner = self .0 .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); + inner + .tallies + .entry(phase.label()) + .or_default() + .add(dt, extras.work); inner.emitted.insert(phase.label()) }; if !first { @@ -363,12 +462,26 @@ pub(crate) fn timed_res( match timing { None => f(), Some(sink) => { + let before = work::snapshot(); let start = Instant::now(); let result = f(); let dt = start.elapsed(); + let after = work::snapshot(); match &result { - Ok(value) => sink.record(phase, src, dt, CacheStatus::Cold, &ok_extras(value)), - Err(_) => sink.record(phase, src, dt, CacheStatus::Cold, &RowExtras::default()), + Ok(value) => sink.record( + phase, + src, + dt, + CacheStatus::Cold, + &ok_extras(value).work(before, after), + ), + Err(_) => sink.record( + phase, + src, + dt, + CacheStatus::Cold, + &RowExtras::default().work(before, after), + ), } result } @@ -388,12 +501,26 @@ pub(crate) fn timed_res_status( match timing { None => f(), Some(sink) => { + let before = work::snapshot(); let start = Instant::now(); let result = f(); let dt = start.elapsed(); + let after = work::snapshot(); match &result { - Ok(value) => sink.record(phase, src, dt, status, &ok_extras(value)), - Err(_) => sink.record(phase, src, dt, status, &RowExtras::default()), + Ok(value) => sink.record( + phase, + src, + dt, + status, + &ok_extras(value).work(before, after), + ), + Err(_) => sink.record( + phase, + src, + dt, + status, + &RowExtras::default().work(before, after), + ), } result } diff --git a/src/eval/builtin.rs b/src/eval/builtin.rs index 5affd61f..0224bdd4 100644 --- a/src/eval/builtin.rs +++ b/src/eval/builtin.rs @@ -14,6 +14,7 @@ use std::{env, fs}; use num_bigint::{BigInt, Sign}; use crate::core::builtins::{Builtin, FloatOp}; +use crate::core::effect_abi::{TQCONS, TQNIL}; use crate::core::{CoreOp, NegLane}; use crate::store::bridge; use crate::types::{CONS, NIL}; @@ -31,6 +32,74 @@ const DEFAULT_BYTE: u8 = 0; const BUFFER_INDEX_ERROR: &str = "buffer index out of bounds"; const TBUF_NEGATIVE_LENGTH_ERROR: &str = "tbuf_new: negative length"; +// The type-aligned continuation queue's interior cells, mirroring the C +// runtime's persistent tree (`runtime/prism_effect.c`): the empty queue is +// `Unit`, one arrow is a leaf, and snoc/concat build interior nodes in O(1). +// These cells are evaluator-internal; lowered Core only ever inspects the +// `TQNil`/`TQCons` view that `taq_uncons` returns, so the names carry a `$` +// sigil no surface constructor can spell. +const TAQ_LEAF: &str = "taq$leaf"; +const TAQ_NODE: &str = "taq$node"; + +const fn taq_is_empty(q: &Rv) -> bool { + matches!(q, Rv::Unit) +} + +fn taq_node(l: Rv, r: Rv) -> Rv { + Rv::Data(TAQ_NODE.into(), vec![l, r].into()) +} + +fn taq_snoc(q: &Rv, arrow: &Rv) -> Rv { + let leaf = Rv::Data(TAQ_LEAF.into(), vec![arrow.clone()].into()); + if taq_is_empty(q) { + leaf + } else { + taq_node(q.clone(), leaf) + } +} + +fn taq_concat(q1: &Rv, q2: &Rv) -> Rv { + if taq_is_empty(q1) { + return q2.clone(); + } + if taq_is_empty(q2) { + return q1.clone(); + } + taq_node(q1.clone(), q2.clone()) +} + +/// The leftmost arrow and the remaining queue as `TQCons(head, tail)`, or +/// `TQNil` for the empty queue. Walks the left spine re-associating +/// `Node(Node(a, b), c)` toward `Node(a, Node(b, c))`, sharing every leaf, so +/// a queue built by repeated snoc drains in amortized O(1) per element and a +/// shared queue survives for another resumption. +fn taq_uncons(q: &Rv) -> Result { + if taq_is_empty(q) { + return Ok(Rv::Data(TQNIL.into(), Vec::new().into())); + } + let mut cur = q.clone(); + let mut tail = Rv::Unit; + loop { + cur = match cur { + Rv::Data(name, fields) if name.as_str() == TAQ_NODE && fields.len() == 2 => { + let l = fields[0].clone(); + let r = fields[1].clone(); + tail = if taq_is_empty(&tail) { + r + } else { + taq_node(r, tail) + }; + l + } + Rv::Data(name, fields) if name.as_str() == TAQ_LEAF && fields.len() == 1 => { + let head = fields[0].clone(); + return Ok(Rv::Data(TQCONS.into(), vec![head, tail].into())); + } + other => return Err(format!("taq_uncons: not a queue: {}", other.kind())), + }; + } +} + #[expect(clippy::cast_sign_loss)] const fn low_byte(value: i64) -> u8 { (value & BYTE_MASK) as u8 @@ -69,7 +138,7 @@ fn file_result(r: std::io::Result<()>) -> Rv { #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] // Genuine unary negation per lane. Int reuses the exact `0 - x` subtract path // (immediate/bignum promotion included) so the result is identical to the old -// lowering; I64 is the wrapping fixed-width subtract from zero; Float is a real +// lowering. I64 is the wrapping fixed-width subtract from zero. Float is a real // sign-bit flip (`-f`, not `-0.0 - f`) so it preserves signed zero and matches // the native `fneg` bit for bit. pub(super) fn neg_rv(lane: NegLane, v: &Rv) -> Result { @@ -381,6 +450,21 @@ pub(super) fn str_builtin(b: Builtin, vals: &[Rv], args: &[String]) -> Result { + let b = s.as_bytes(); + let lo = usize::try_from(*lo).unwrap_or(0).min(b.len()); + let hi = usize::try_from(*hi).unwrap_or(0).min(b.len()); + if lo >= hi { + Ok(Rv::Str(String::new())) + } else { + Ok(Rv::Str(String::from_utf8_lossy(&b[lo..hi]).into_owned())) + } + } (B::CharAt, [Rv::Str(s), Rv::Int(i)]) => { Ok(Rv::Int(usize::try_from(*i).map_or(-1, |idx| { s.chars().nth(idx).map_or(-1, |c| i64::from(c as u32)) @@ -601,6 +685,15 @@ pub(super) fn str_builtin(b: Builtin, vals: &[Rv], args: &[String]) -> Result { + let mut next = v.to_vec(); + next.extend_from_slice(s.as_bytes()); + Ok(Rv::Buf(Rc::new(next))) + } // Typed buffers (f64 and i64 elements): one raw-word storage, routed to // its own dispatcher below. ( @@ -721,6 +814,14 @@ pub(super) fn str_builtin(b: Builtin, vals: &[Rv], args: &[String]) -> Result { Err("arena region hooks are native-only and have no interpreter form".into()) } + // The type-aligned continuation queue the free-monad lowerings thread + // through `ebind`/`qApply`. The interpreter never emits these (it runs + // pre-lowering Core), but the lowered-Core observation harness replays + // every forced tier's output, so the queue must evaluate here with the + // exact semantics of the native cells. + (B::TaqSnoc, [q, arrow]) => Ok(taq_snoc(q, arrow)), + (B::TaqConcat, [q1, q2]) => Ok(taq_concat(q1, q2)), + (B::TaqUncons, [q]) => taq_uncons(q), (op, _) => Err(format!("str builtin {}: wrong args", op.name())), } } diff --git a/src/eval/kont.rs b/src/eval/kont.rs index 71e9b6ee..dbf9ae1d 100644 --- a/src/eval/kont.rs +++ b/src/eval/kont.rs @@ -53,11 +53,13 @@ //! //! # Totality //! -//! [`decode_kont`](crate::eval::kont::decode_kont) never panics on hostile bytes: every varint is byte-capped, +//! [`decode_kont`](crate::eval::kont::decode_kont) never panics on hostile bytes. +//! Every varint is byte-capped, //! every length is bounded, the scheme, kind and bundle are checked before the //! body, child indices are range-checked against the already-parsed prefix, //! reconstruction runs against an expansion budget, and trailing bytes are -//! rejected. [`encode_kont`](crate::eval::kont::encode_kont) is fallible: a value that cannot cross the suspend +//! rejected. [`encode_kont`](crate::eval::kont::encode_kont) is fallible. A value +//! that cannot cross the suspend //! boundary (a graph nested past the suspendable depth, the fingerprint of an //! unserializable capture or a cycle) is refused by name rather than encoded. @@ -192,8 +194,8 @@ pub fn portable_value_type(ty: &Type) -> bool { // One discriminant for every runtime shape the table can hold, across all six // domains (value, node, atom, frame, environment, handler record). Encoded as a -// uvarint; the array below is the single source of truth for the numbering, so -// encode (`as u8`) and decode (index into the array) cannot drift. A reference is +// uvarint; the array below defines the numbering used by both encode (`as u8`) +// and decode (index into the array). A reference is // an untyped index; the builder validates the referent's tag against the domain // it is used in, so a cross-domain reference in a hostile frame is rejected rather // than misread. @@ -1553,7 +1555,7 @@ mod tests { // A continuation exercising every value, node, atom, frame, pattern and // handler shape the codec must round-trip, so the idempotence and totality - // checks below cover the whole table, not just the common cases. + // checks below cover the whole table, including uncommon cases. fn kitchen_sink() -> Kont { let inner_body = cmp(Node::Prim( CoreOp::Add, diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 1db55fc5..9fe8ce99 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -763,7 +763,7 @@ impl<'a> Machine<'a> { } // Perform one capability read of the given kind. Under `Live` the real read - // runs; under `Record` it runs and its result is logged; under `Replay` the + // runs. Under `Record` it runs and its result is logged. Under `Replay` the // recorded value is served and the real read is skipped. Reaching a replay // budget sets `halted` and returns a placeholder the unwinding discards. fn observe( @@ -1235,6 +1235,31 @@ impl<'a> Machine<'a> { // Region brackets: no regions in the verifier, so enter yields a // placeholder token and exit is the identity on the activation's // result, both unobservable (the native contract). + // + // Modeling regions by value identity is correct and deliberate: the + // region policy is a resource decision and the interpreter is the + // value oracle. The consequence is that differential parity against + // this interpreter is not coverage of any of the following, each of + // which is a resource property with no value counterpart here, and + // each of which is therefore checked natively instead, by the region + // and promotion counters the runtime reports: + // + // - Region allocation. `Bump` yields unit, so no cell exists and + // nothing distinguishes a region cell from a heap cell; a silent + // fallback to per-cell malloc is invisible on this side. + // - Escape promotion. `ArenaExit` is the identity, so nothing is + // copied; a result that natively costs one deep copy per + // reachable cell costs zero here. + // - Sharing across promotion. Identity preserves sharing for free, + // so a native walk that expands a shared sub-DAG into a tree, + // which is exponential in the sharing depth and still yields the + // identical value, produces no divergence to observe. + // - Reclamation. Nothing was allocated, so the wholesale teardown + // at the activation's return has no counterpart, and neither does + // failing to perform it. + // - Region identity and nesting. Enter yields one constant token + // rather than a distinct region, so nested activations and the + // depth at which a cell was allocated are indistinguishable. Node::ArenaEnter => State::Ret(Rv::Int(0)), Node::ArenaExit(args) => match atoms(&env, args)?.as_slice() { [Rv::Int(_), v] => State::Ret(v.clone()), diff --git a/src/hir/lint.rs b/src/hir/lint.rs index 0d7af7ab..948b190a 100644 --- a/src/hir/lint.rs +++ b/src/hir/lint.rs @@ -167,18 +167,6 @@ pub fn lint_hir(hir: &CheckedHir<'_>) -> Vec { } } } - // The REPL override table carries a re-inferred expression's own evidence - // against fresh NodeIds; judge it by the same dictionary rules. Match the - // private field directly (the lint is a child module of `hir`). - if let Some(table) = &hir.evidence_override { - for (i, fact) in table.iter().enumerate() { - if let Some(dicts) = fact { - for d in dicts { - check_dict(hir.checked, i, d, &mut out); - } - } - } - } // Handler residual family: the marker and fact tables must agree exactly, // and both operation lists are canonical, duplicate-free names from the // checked effect environment. The forwarded body uses are necessarily a diff --git a/src/hir/mod.rs b/src/hir/mod.rs index d8ffc3c3..581f32e4 100644 --- a/src/hir/mod.rs +++ b/src/hir/mod.rs @@ -13,8 +13,8 @@ //! zonked node types, and operation-local handler residual witnesses. //! //! There is deliberately no parallel elaboration path: elaboration constructs -//! its `CheckedHir` through [`build`] (whole programs) or [`build_for_expr`] -//! (the REPL's re-inferred expressions, which carry their own evidence). +//! its `CheckedHir` through [`build`] (whole programs) or `build_for_expr` +//! (the REPL's re-inferred expressions, which carry all of their own facts). pub mod lint; @@ -123,9 +123,9 @@ impl HandlerResidual { /// The single cross-phase carrier of node-keyed checked state: resolution /// facts, dictionary evidence at dispatch sites, the concrete numeric lane a /// literal or operator site fixed to, and the zonked type synthesized for a -/// node. Constructed once by the checker ([`NodeFacts::from_tables`]) and +/// node. Constructed once by the checker (`NodeFacts::from_tables`) and /// read only through a [`CheckedHir`]. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug)] pub struct NodeFacts { res: Vec>, evidence: Vec>>, @@ -192,6 +192,18 @@ struct NodeFactWire { } impl NodeFacts { + pub(crate) const fn empty() -> Self { + Self { + res: Vec::new(), + evidence: Vec::new(), + lane: Vec::new(), + ty: Vec::new(), + tooltip: Vec::new(), + handler_nodes: Vec::new(), + handler_residual: Vec::new(), + } + } + pub(crate) fn to_json(&self) -> Result { let rows = self .iter() @@ -218,7 +230,7 @@ impl NodeFacts { if !rows.windows(2).all(|pair| pair[0].id < pair[1].id) { return Err("checked HIR facts are not in canonical node order".to_string()); } - let mut facts = Self::default(); + let mut facts = Self::empty(); for row in rows { let index = row.id as usize; place_dense(&mut facts.res, index, row.res); @@ -286,7 +298,7 @@ impl NodeFacts { clippy::too_many_arguments, reason = "the sole checked-HIR conversion keeps each inference fact family explicit" )] - pub fn from_tables( + pub(crate) fn from_tables( field_res: BTreeMap, unboxed_field: BTreeMap, path_res: PathRes, @@ -338,19 +350,25 @@ impl NodeFacts { pub(crate) fn tooltip(&self, id: NodeId) -> Option<&str> { self.tooltip.get(id.0 as usize).and_then(Option::as_deref) } + + /// Adopt the presentation-only tooltip table from an instrumented check of + /// the same program, keeping every semantic fact of this judgment. Node + /// identities are a pure function of the source, so the two checks agree on + /// which node each string describes. + pub(crate) fn adopt_tooltips(&mut self, from: Self) { + self.tooltip = from.tooltip; + } } /// The checked HIR over one checked program. /// -/// The only view through which elaboration reads per-node facts. For the -/// REPL's re-inferred expressions, an evidence override carries the -/// expression's own dictionaries (its fresh `NodeId`s are disjoint from the -/// session program's). +/// The only view through which elaboration reads per-node facts. A REPL +/// expression supplies its own complete fact artifact; it never overlays one +/// fact family on the session program's numerically colliding node identities. #[derive(Debug)] pub struct CheckedHir<'a> { pub checked: &'a Checked, facts: &'a NodeFacts, - evidence_override: Option>>>, } impl<'a> CheckedHir<'a> { @@ -363,11 +381,8 @@ impl<'a> CheckedHir<'a> { /// The dictionary evidence recorded at a dispatch site. #[must_use] pub fn evidence(&self, id: NodeId) -> Option<&[Dict]> { - let table = self - .evidence_override - .as_ref() - .unwrap_or(&self.facts.evidence); - table + self.facts + .evidence .get(id.0 as usize) .and_then(Option::as_ref) .map(Vec::as_slice) @@ -431,22 +446,17 @@ pub fn build(checked: &Checked) -> CheckedHir<'_> { linted(CheckedHir { checked, facts: &checked.facts, - evidence_override: None, }) } /// Build the checked HIR for a single re-inferred expression (the REPL). /// -/// The session program's facts, with the expression's own dictionary evidence -/// overriding the evidence table (its fresh `NodeId`s are meaningless against -/// the session's). +/// Every node fact comes from the expression's own inference. Expression and +/// session `NodeId`s share a numeric namespace, so consulting any session fact +/// here could accept a stale resolution or numeric lane before a fallback runs. #[must_use] -pub fn build_for_expr<'a>(checked: &'a Checked, dicts: &DictTable) -> CheckedHir<'a> { - linted(CheckedHir { - checked, - facts: &checked.facts, - evidence_override: Some(dense(dicts.clone())), - }) +pub(crate) fn build_for_expr<'a>(checked: &'a Checked, facts: &'a NodeFacts) -> CheckedHir<'a> { + linted(CheckedHir { checked, facts }) } #[cfg(test)] @@ -481,6 +491,29 @@ mod tests { .any(|r| matches!(r, NodeRes::Field(ctor, 0, 2) if ctor == "Point"))); } + #[test] + fn expression_facts_do_not_fall_through_to_colliding_session_ids() { + let mut c = checked(SRC); + let id = NodeId(1); + place_dense(&mut c.facts.lane, id.0 as usize, Some(Type::Int)); + place_dense( + &mut c.facts.res, + id.0 as usize, + Some(NodeRes::Field("Point".into(), 0, 2)), + ); + let mut expression = NodeFacts::empty(); + place_dense(&mut expression.lane, id.0 as usize, Some(Type::Bool)); + place_dense( + &mut expression.res, + id.0 as usize, + Some(NodeRes::Field("Point".into(), 1, 2)), + ); + + let hir = build_for_expr(&c, &expression); + assert_eq!(hir.lane(id), Some(&Type::Bool)); + assert_eq!(hir.res(id), Some(&NodeRes::Field("Point".into(), 1, 2))); + } + // Negative tests for the validation transition: a fabricated fact that // does not check against the constructor environment must be reported. #[test] @@ -488,12 +521,11 @@ mod tests { let c = checked(SRC); let facts = NodeFacts { res: vec![Some(NodeRes::Field("NoSuchCtor".into(), 0, 2))], - ..NodeFacts::default() + ..NodeFacts::empty() }; let hir = CheckedHir { checked: &c, facts: &facts, - evidence_override: None, }; assert_eq!(lint::lint_hir(&hir).len(), 1); } @@ -510,12 +542,11 @@ mod tests { // An update path with an empty chain. Some(NodeRes::Paths(vec![vec![]])), ], - ..NodeFacts::default() + ..NodeFacts::empty() }; let hir = CheckedHir { checked: &c, facts: &facts, - evidence_override: None, }; assert_eq!(lint::lint_hir(&hir).len(), 3); } @@ -525,12 +556,11 @@ mod tests { let c = checked(SRC); let facts = NodeFacts { evidence: vec![Some(vec![Dict::Global("NoSuchInst".into(), vec![])])], - ..NodeFacts::default() + ..NodeFacts::empty() }; let hir = CheckedHir { checked: &c, facts: &facts, - evidence_override: None, }; assert_eq!(lint::lint_hir(&hir).len(), 1); } @@ -554,12 +584,11 @@ mod tests { "Ord".into(), 5, )])], - ..NodeFacts::default() + ..NodeFacts::empty() }; let hir = CheckedHir { checked: &c, facts: &facts, - evidence_override: None, }; assert_eq!(lint::lint_hir(&hir).len(), 1); } @@ -573,12 +602,11 @@ mod tests { let facts = NodeFacts { lane: vec![Some(Type::Exist(0))], ty: vec![Some(Type::Exist(1))], - ..NodeFacts::default() + ..NodeFacts::empty() }; let hir = CheckedHir { checked: &c, facts: &facts, - evidence_override: None, }; assert!(lint::lint_hir(&hir).is_empty()); } @@ -608,7 +636,6 @@ fn run() : Int ! {} = let hir = CheckedHir { checked: &c, facts: &missing, - evidence_override: None, }; assert!(lint::lint_hir(&hir) .iter() @@ -619,7 +646,6 @@ fn run() : Int ! {} = let hir = CheckedHir { checked: &c, facts: &stale, - evidence_override: None, }; assert!(lint::lint_hir(&hir) .iter() @@ -647,7 +673,6 @@ fn run() : Int ! {} = let hir = CheckedHir { checked: &c, facts: &facts, - evidence_override: None, }; assert!(lint::lint_hir(&hir) .iter() @@ -663,14 +688,13 @@ fn run() : Int ! {} = let source = NodeFacts { handler_nodes: c.facts.handler_nodes.clone(), handler_residual: c.facts.handler_residual.clone(), - ..NodeFacts::default() + ..NodeFacts::empty() }; let json = source.to_json().expect("serialize handler facts"); let facts = NodeFacts::from_json(&json).expect("deserialize handler facts"); let hir = CheckedHir { checked: &c, facts: &facts, - evidence_override: None, }; assert!(lint::lint_hir(&hir).is_empty()); assert!(facts.handler_residual.iter().flatten().any(|fact| { diff --git a/src/index/build.rs b/src/index/build.rs index fa40a840..d5933fcb 100644 --- a/src/index/build.rs +++ b/src/index/build.rs @@ -58,11 +58,7 @@ pub struct IndexInput<'a> { pub fn build(input: IndexInput<'_>) -> Result { let production = addressable_surface(input.source, input.roots)?; - // Walk each module's own source for what the author wrote, keyed by module. - // A module that does not parse is carried with its diagnostic instead of - // failing the build: one broken file — a scratch buffer, a fixture that - // exists to be invalid — must not take the index of everything else down - // with it. The same posture the test layer takes, for the same reason. + // Preserve parse failures on individual modules without failing the index. let mut walked = Vec::new(); for m in input.modules { walked.push((m, surface::walk(&m.source, m.is_prelude))); @@ -330,9 +326,7 @@ fn attach_tokens(defs: &mut [Def]) -> Vec { let mut classes: Vec = Vec::new(); for def in defs.iter_mut() { def.tokens = pack_tokens(&def.source, &mut classes); - // A rendered type and effect row are not source, but they are written in - // the language's own syntax, so the same lexer classifies them and the - // consumer needs no second one. + // Classify rendered types and effect rows with the language lexer too. def.ty_tokens = def .ty .as_ref() @@ -380,10 +374,8 @@ fn pack_tokens(text: &str, classes: &mut Vec) -> String { // Add a reference for each type name written in a definition's own text. // -// The renamer cannot supply these: `Ty` carries no spans, so a type name resolved -// there has no position to report (see `occurrences`). But a written type is -// exactly what a reader wants to click — `d : Doc` should reach `Doc` — so the -// positions are recovered by lexing the definition's source. +// `Ty` carries no name spans, so recover written type positions by lexing each +// definition's source. // // Lexing rather than searching, because only the lexer knows what is code: a // `Doc` inside a comment or a string literal is not a reference, and a substring @@ -472,12 +464,7 @@ fn attach_type_refs( }; for def in defs.iter_mut() { - // A declaration's own member sites are not references. `Tip` and `Bin` - // inside `type Map = Tip | Bin(..)` are where those members come into - // being — `attach_members` has already recorded them, and a viewer sends - // them to the member's users. Resolving them here instead would turn each - // into a link from the declaration back to itself, and list the - // declaration among its own members' users. + // Do not turn member declarations into links back to their owner. let blocked: Vec<(usize, usize)> = def .refs .iter() @@ -547,11 +534,8 @@ fn named_in( // Record where each declaration names its own members. // -// The names come from the parsed declaration, so the list is complete and -// authoritative — every constructor, method and operation, including the ones -// nothing uses. That completeness is the point: recovering members from -// occurrences finds only the used ones, and an effect's operations are performed -// by *programs*, so a library index of `Output` would list none of them. +// Read names from declarations so unused constructors, methods, and operations +// remain discoverable. // // The positions come from lexing the declaration's own text, because the AST has // no span for a member's name (`ClassDecl::methods` is `(String, Ty)`, `EffOp` and @@ -620,12 +604,8 @@ fn attach_members( // A name that resolves to something with no declaration of its own, mapped to the // declaration a reader should navigate to instead. // -// A constructor is written inside a `type`, an operation inside an `effect`, a -// method inside a `class`. None is a definition in its own right, so a reference -// to one resolves to a name the index has no entry for — and `Cons`, `Some`, and -// `Err` are among the most frequently written names in any program. This is the -// same retarget `edges` applies to a lowered instance method, for the same reason: -// send the reader where the source actually is. +// Constructors, operations, and methods resolve to the declarations that own +// their source. // // Only an unambiguous mapping counts. The renamer canonicalizes constructors but // leaves operation and method names bare (they are not module binders), so two @@ -863,10 +843,8 @@ fn is_global(scope: Scope<'_>, decl: &surface::Decl) -> bool { // The canonical spellings a declaration could have, most specific first. // -// A global declaration has exactly one: its bare name. Anything else is either -// exported (`Data.Map.insert`) or module-private (`Data.Map@helper`), and which -// one it is could be read off the `pub` marker — but it is read off the layer -// instead, by probing both, so the index cannot disagree with Core about a name. +// A global declaration has one candidate: its bare name. Probe both exported and +// private layer names for module declarations so the index agrees with Core. // The bare form is deliberately *not* a candidate for a non-global module: it // would silently match an unrelated same-named definition in the entry module. fn candidates(module: &str, name: &str, global: bool) -> Vec { diff --git a/src/index/diff.rs b/src/index/diff.rs index 50a0eca3..022c650a 100644 --- a/src/index/diff.rs +++ b/src/index/diff.rs @@ -1,15 +1,9 @@ //! Diffing two indexes: what changed between one revision and another. //! -//! A pure function of two artifacts. No compiler runs, because the comparison is -//! between content addresses that a compiler already computed, which is also why -//! this can answer questions a text diff cannot. +//! This compares two existing artifacts without running the compiler. //! -//! The classification is the point. Content addressing folds a definition's -//! dependencies into its hash, so *most* of what moves in a real change moved -//! only because something underneath it did. A review tool that cannot separate -//! those from the definitions whose own text the author edited is unusable on any -//! change of size — the signal drowns. Comparing the hash and the source text -//! together separates them exactly: +//! Content addressing folds dependencies into each definition's hash. Comparing +//! the hash with the source distinguishes direct edits from dependent rehashes: //! //! | hash | source | meaning | //! |------|--------|---------| @@ -18,17 +12,8 @@ //! | differs | differs | [`Status::Changed`]: the author edited this | //! | differs | same | [`Status::Cone`]: only a dependency moved | //! -//! With one carve-out: equal hashes prove equal *executable behavior*, and a -//! definition carries review-facing facts the hash never sees. A claims edit -//! (`total fn` to `assume total fn` replaces a proof with a trust root), a -//! visibility change, a doc or deprecation edit — each is erased before the -//! layer that is hashed, and a doc comment sits outside [`Def::source`] -//! entirely. Those are compared separately and classified as authored, because -//! calling a new trust root "cosmetic" (or not listing it at all) is exactly -//! the misreport a reviewer cannot afford. -//! -//! The last row is the one worth having. It is the difference between "47 things -//! changed" and "you edited 3 things, and 44 more re-hashed underneath them". +//! Claims, visibility, documentation, and deprecation are compared separately +//! because they do not affect executable hashes. Changes to them are authored. //! //! A rename is likewise free: a definition whose bytes are unchanged but whose //! canonical name moved keeps its hash, so it appears as [`Status::Moved`] rather @@ -101,15 +86,9 @@ pub struct Entry { #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct Side { pub title: String, - /// That revision's namespace root, so a consumer can tell which two programs - /// were compared without reading a single entry — and can refuse to overlay - /// this diff on an index it was not made against. + /// That revision's namespace root, used to validate the diff's inputs. pub contract: String, - /// The highlight classes this side's carried [`Def::tokens`] index. The - /// entry records were copied out of their index, whose shared tables did not - /// come with them; without this a consumer cannot decode the old side's - /// spans at all, and the two revisions' tables can order classes - /// differently, so "borrow the new index's table" paints the wrong colors. + /// The highlight classes indexed by this side's [`Def::tokens`]. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub token_classes: Vec, /// The rendered types this side's carried [`Def::types`] index, for the @@ -329,11 +308,8 @@ pub fn diff(old: &Index, new: &Index) -> Result { // guess. fn classify(old: &Def, new: &Def) -> Option { let same_text = old.source == new.source; - // Review-facing facts the behavior hash never sees. A claim is erased before - // the layer that is hashed — `total fn` to `assume total fn` swaps a proof - // for a trust root without moving a single hashed byte — and a doc comment - // sits outside `source`, so a doc-only edit moves neither the hash nor the - // text. Both are authored, and both are exactly what a reviewer reads. + // Claims and other review metadata do not affect the behavior hash. Doc + // comments also sit outside `source`, so compare all metadata explicitly. let same_meta = old.claims == new.claims && old.vis == new.vis && old.doc == new.doc diff --git a/src/index/edges.rs b/src/index/edges.rs index eec977f2..43b54b87 100644 --- a/src/index/edges.rs +++ b/src/index/edges.rs @@ -1,25 +1,17 @@ //! Deriving the relationship set. //! -//! Every edge here is read off something the compiler already computed, so none -//! of it can go stale against the code: +//! Every edge comes from facts the compiler already computed: //! -//! - `calls` is the Core dependency adjacency (`core::DepGraph`) — the same -//! relation the content hasher walks for its Merkle substitution and -//! `prism store query callers` answers one name at a time. +//! - `calls` is the Core dependency adjacency (`core::DepGraph`). //! - `performs` is the checked effect row, read as a set rather than matched as //! text, so it is exact. //! - `uses-type` is a structural walk over the checked type and over the types //! written into a declaration's own signature, keyed on the resolved symbol. -//! Deliberately not the token rule `prism store query uses-type` applies: that -//! query matches a name a human typed, where looseness is convenient, while an -//! edge between canonical identities has to be exact. +//! This is stricter than the token rule used by `prism store query uses-type`. //! - `instance-of` is the resolved instance's class. //! - `tests` is a test's transitive dependency closure in the test-mode graph. //! -//! Behavioral equivalence is deliberately *not* an edge kind: two definitions are -//! interchangeable exactly when their [`super::Def::hash`] fields are equal, so a -//! consumer groups by hash and the artifact carries no redundant (and potentially -//! quadratic) edge set. +//! Consumers group equal [`super::Def::hash`] values for behavioral equivalence. use std::collections::{BTreeMap, BTreeSet}; @@ -384,19 +376,12 @@ fn declared_type_refs(sources: &Sources<'_>) -> BTreeMap, out: &mut BTreeSet) { // `tests`: from each test to every indexed definition in its transitive // dependency closure. // -// Transitive rather than direct on purpose. "The tests that exercise this -// function" must include a test that reaches it through a helper, which is the -// common case; a direct-only edge set would quietly answer "none" for most -// definitions. Restricting the targets to indexed definitions is what keeps the -// closure from dragging in the whole prelude on every test. +// Use the transitive closure so tests reached through helpers are included. Limit +// targets to indexed definitions to avoid pulling in the whole prelude. fn tests(sources: &Sources<'_>, out: &mut BTreeSet) { let Some(graph) = &sources.test_graph else { return; diff --git a/src/index/mod.rs b/src/index/mod.rs index 1b39df9f..eaa13df4 100644 --- a/src/index/mod.rs +++ b/src/index/mod.rs @@ -1,32 +1,19 @@ //! `prism index`: the whole-codebase index a program viewer reads. //! -//! One deterministic artifact per revision, holding the two things a reviewer -//! navigates by: **definitions** (each with the content address the compiler -//! gives it, its inferred type and effect row, its doc comment, and the exact -//! source range of its body) and **relationships** between them. It is a -//! projection of facts the compiler already computes, not a new analysis: the -//! addresses are the namespace layers (`driver::namespace_layers`), the `calls` -//! edges are the Core dependency graph (`core::DepGraph`, the same adjacency -//! `prism store query callers` answers and the content hasher walks), and the -//! `performs` edges are the checked effect rows. +//! One deterministic artifact per revision, containing definitions and their +//! relationships. It projects existing compiler facts: namespace addresses, +//! Core dependency edges, and checked effect rows. //! -//! Two properties make it a viewer substrate rather than a second doc generator: +//! Two properties make it suitable for a viewer: //! -//! - **Addressed, not located.** A definition's identity is its canonical name -//! plus its content hash, so a bookmark, a note, or a review mark survives -//! reformatting and file moves, and two revisions can be aligned by identity. -//! Source ranges are carried too, but as rendering data, not identity. -//! - **Whole-set edges.** `callers`/`dependents` answer one question at a time -//! from the CLI; the index carries the full edge set, so a viewer can traverse -//! in either direction without re-running the compiler. +//! - A canonical name and content hash identify each definition. Source ranges +//! are rendering data. +//! - The complete edge set supports traversal in either direction without +//! re-running the compiler. //! -//! Addressing every definition means compiling more than a build does: "everything -//! the entry point reaches" is the wrong set for a reader, because a library -//! package's modules are not reachable from its `[bin]` entry and are most of what -//! someone opened a viewer to read. The caller therefore supplies a `source` that -//! reaches every module it lists (`cli::index` appends one qualified import per -//! module to the build's own input); a module the program does not reach is still -//! indexed, but carries no address. +//! `cli::index` imports every listed module so library code outside the binary's +//! reachability set also receives an address. Unreachable modules remain in the +//! index without one. //! //! The artifact is a pure function of the indexed source (it is taken over the //! identity surface, pre-optimizer elaborated Core), so `--check` can gate a @@ -54,14 +41,10 @@ pub use occurrences::{Occurrences, OCCURRENCES_FORMAT}; /// Schema tag for the index artifact. pub const INDEX_FORMAT: &str = "prism-index-v1"; -/// The self-describing header: what this artifact is, what produced it, and the -/// one digest that names the exact program it describes. +/// Identifies the artifact, its producer, and the indexed program. /// -/// `contract` is the indexed program's namespace root, so a consumer can tell two -/// indexes apart (and a stale one from a current one) without reading a single -/// definition. It names the program *the index describes*, which includes every -/// listed module, so for a project whose entry point does not reach all of them it -/// is deliberately not the same root a build or a package tag would publish. +/// `contract` is the namespace root of all listed modules. It may differ from a +/// build or package root whose entry point reaches fewer modules. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct Envelope { pub format: String, @@ -113,12 +96,8 @@ pub struct IndexModule { /// `index --no-source`, for a consumer that reads the working tree itself. #[serde(default, skip_serializing_if = "Option::is_none")] pub source: Option, - /// The front end's diagnostic when this module's source does not parse. Its - /// declarations are then absent from the definition layer, and this is what - /// keeps that absence from reading as "an empty module": the same honesty - /// [`TestLayer::Unavailable`] gives a test layer that could not be built. - /// One broken file — a scratch buffer, a fixture that exists to be invalid — - /// must not take the index of everything else down with it. + /// The front-end diagnostic when this module does not parse. Other modules + /// remain indexed. #[serde(default, skip_serializing_if = "Option::is_none")] pub error: Option, } @@ -192,9 +171,7 @@ impl Vis { } } -/// A checked claim a definition carries. Each is erased before executable Core, -/// so it is a property of the declaration rather than of its behavior hash; -/// a reviewer wants them on the definition card. +/// A checked claim carried separately because claims are erased before Core. #[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] #[serde(rename_all = "kebab-case")] pub enum Claim { @@ -223,9 +200,8 @@ pub struct Span { /// One name written inside a definition's [`Def::source`], and what it means. /// -/// Offsets are into `source` itself, so a consumer renders a navigable body by -/// slicing between them — no file access, no second coordinate system, no -/// knowledge of where the definition sat in whatever the compiler compiled. +/// Offsets are relative to `source`, so consumers need no source file or compiler +/// coordinates. /// /// `target` may name a definition the index does not contain, exactly as an edge /// endpoint may; a builtin or a prelude function a project calls is still worth @@ -254,9 +230,7 @@ pub struct Def { /// behavior hash for a term, a shape digest for a type or effect, an /// interface digest for a class, an identity digest for an instance. /// - /// Two definitions with equal hashes are interchangeable by construction, so a - /// consumer groups by this field to find behavioral duplicates; the artifact - /// carries no separate equivalence edge set. + /// Equal hashes identify behaviorally equivalent definitions. /// /// Absent for the kinds that have no independent address: a synonym and a row /// alias erase into the types that mention them, a `pattern` lowers to hidden @@ -271,11 +245,8 @@ pub struct Def { pub ty: Option, /// Highlight spans over `ty`, packed exactly like [`Def::tokens`]. /// - /// A rendered type is not source — no file holds it, so it has no occurrence - /// rows and no lexer has run over it. But it is written in the language's own - /// type syntax, so running the compiler's lexer across the rendered string - /// classifies it correctly, and the alternative is a second tokenizer in the - /// consumer that would disagree with this one about what a name is. + /// Rendered types are lexed by the compiler so consumers need no second type + /// tokenizer. #[serde(default, skip_serializing_if = "String::is_empty")] pub ty_tokens: String, /// Every name in `ty` that resolves to a definition. A signature is the part of @@ -314,31 +285,19 @@ pub struct Def { /// the same packed form as `tokens`, where `index` selects from /// [`Index::type_table`]. /// - /// Variables only. Every subterm has a type, and carrying all of them would - /// multiply the payload and nest spans inside one another — the whole - /// `Row(gap, children)` contains `gap` — while a reader hovering a body is - /// asking what the names in it are. Names cannot overlap, so a consumer merges - /// these with the other span sets as flat intervals. + /// Contains variables only. Their spans do not overlap, so consumers can + /// merge them with the other span sets as flat intervals. #[serde(default, skip_serializing_if = "String::is_empty")] pub types: String, /// Highlight spans over `source`: whitespace-separated `gap length class` /// triples, where `gap` is the bytes since the previous span's end and `class` /// indexes [`Index::token_classes`]. /// - /// A string of numbers rather than an array of them, which looks like the - /// wrong shape and is not. The artifact is pretty-printed, so a JSON array - /// spends a newline and six spaces of indent on every element: over the - /// standard library that is 167,000 elements and about 1.3 MB of whitespace, - /// for data no one reads element-wise. One object per token - /// (`{start, end, class}`) would cost 2 MB — most of the artifact again, to - /// colour text. Unstyled spans (an ordinary lowercase identifier) are omitted, - /// which is why the gap is needed rather than assuming spans abut. + /// The numeric string keeps pretty-printed JSON compact. Unstyled spans are + /// omitted, so each encoded span includes its gap from the previous one. /// - /// Baked rather than computed in the browser for two reasons. The spans come - /// from the compiler's own lexer, so highlighting cannot disagree with the - /// compiler about what a token is; and highlighting is wanted at first paint, - /// while the wasm compiler sits behind a worker boundary and would arrive - /// after it. + /// Spans come from the compiler lexer and are ready before the browser's wasm + /// worker starts. #[serde(default, skip_serializing_if = "String::is_empty")] pub tokens: String, /// The names this declaration introduces inside itself, in source order: a @@ -346,14 +305,9 @@ pub struct Def { /// a declaration that introduces none. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub members: Vec, - /// Every name written in `source` that resolves to a definition, in source - /// order. This is what makes a rendered body navigable rather than merely - /// readable: the edges say what a definition depends on, these say *where*. + /// Every name in `source` that resolves to a definition, in source order. /// - /// Only names the AST gives a span of their own appear — an expression - /// variable, an effect-row label; see - /// [`occurrences`] for why a constructor pattern - /// and an instance's class do not. + /// Only names with their own AST span appear. See [`occurrences`]. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub refs: Vec, } @@ -361,14 +315,8 @@ pub struct Def { /// A name a declaration introduces inside itself: a data constructor, a class /// method, an effect operation. /// -/// None of these is a definition in its own right — a reference to one resolves -/// to the declaration that owns it — so without this they exist in the artifact -/// only as text inside a `source` field. That is enough to read and not enough to -/// *find*: `Cons`, `Nil` and `pure` are among the most written names in any Prism -/// program, and a consumer searching by name could not turn one up. Recovering -/// them from occurrences would find only the ones something happens to use, which -/// is exactly backwards for `Output`'s operations, performed by programs this -/// index does not contain. +/// Members resolve to their owning declaration. Recording every declared member +/// also makes unused constructors, methods, and operations searchable. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct Member { pub name: String, @@ -379,10 +327,8 @@ pub struct Member { /// A relationship between two definitions. /// -/// Every kind here is *derived*: the compiler already knows it, so it cannot go -/// stale against the code. Author-asserted relations (`equivalent`, `replaces`) -/// are a separate, later layer; the two are deliberately not mixed, because a -/// derived edge is a fact and an asserted one is a claim. +/// These are derived compiler facts. Author-asserted relations belong to a +/// separate layer. #[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] #[serde(rename_all = "kebab-case")] pub enum EdgeKind { @@ -405,12 +351,8 @@ pub enum EdgeKind { /// `from` has a handler clause for an operation of the effect `to`; reversed, /// "what interprets this effect". /// - /// The other half of `Performs`, and not derivable from it: handling an effect - /// *removes* it from the row, so the definition that gives an effect its - /// meaning is exactly the one whose inferred row no longer mentions it. In the - /// standard library `Output` is performed by nothing and handled four times — - /// programs perform it, the library interprets it — so without this an effect - /// declaration relates to nothing at all in either direction. + /// Complements `Performs`. Handling removes an effect from the inferred row, + /// so this relation is collected from handler clauses. Handles, /// `from` is an instance of the class `to`. InstanceOf, @@ -447,11 +389,8 @@ pub struct Index { /// The compiler's own primitives. /// /// A reference to one resolves to no definition because there is none: it is - /// implemented in the compiler rather than in Prism, so there is nothing to - /// navigate to and nothing missing. Carried so a consumer can say *that* - /// instead of reporting the name as absent from the artifact, which reads as - /// an incomplete index when it is nothing of the kind — `byte_at` and - /// `buf_push` are not gaps, they are `ByteAt` and `BufPush`. + /// implemented in the compiler rather than in Prism. This table lets a + /// consumer distinguish primitives from missing definitions. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub builtins: Vec, /// The highlight categories [`Def::tokens`] indexes, so the class names are @@ -468,9 +407,7 @@ pub struct Index { /// One compiler primitive: what source calls it, and its type where the compiler /// records one. /// -/// The signature is what makes a primitive readable rather than merely named. A -/// reader hovering `byte_at` wants `(String, Int) -> Int`; that it has no Prism -/// definition is the less interesting half. +/// Includes a signature when the compiler records one. #[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] pub struct Primitive { pub name: String, diff --git a/src/index/occurrences.rs b/src/index/occurrences.rs index 9cacd706..0eadc7f2 100644 --- a/src/index/occurrences.rs +++ b/src/index/occurrences.rs @@ -1,19 +1,12 @@ //! The occurrence export: every resolved reference, as a versioned document. //! -//! `prism dump occurrences` is the canonical producer. The facts come from the -//! renamer itself (`resolve::Occurrence`), not from a second walk over the AST, -//! so what this reports is by construction what the compiler decided a name -//! means. Read forward it is goto-definition; grouped by target it is +//! `prism dump occurrences` exports the renamer's `resolve::Occurrence` facts. +//! Read forward they support goto-definition; grouped by target they support //! find-references. //! //! A reference appears only where the AST records a span for the name itself: //! today an expression variable, and an effect-row label. Every other resolution -//! site — a constructor pattern, an instance's class, a record literal's type — -//! carries the span of the construct *around* the name, which is the right thing -//! to underline in a diagnostic and the wrong thing to make clickable, since -//! reporting it would turn a whole declaration into one link. Those wait on a span -//! per name in `Ty`, which is the single change that would widen this export; the -//! collection itself needs none. +//! site carries the enclosing construct's span, which is too broad for a link. use serde::{Deserialize, Serialize}; diff --git a/src/index/tests.rs b/src/index/tests.rs index 85ba3a8e..aa3ae1e0 100644 --- a/src/index/tests.rs +++ b/src/index/tests.rs @@ -202,8 +202,7 @@ fn effect_rows_are_reported_and_pure_definitions_carry_none() { assert!(targets(&index, EdgeKind::Performs, "double").is_empty()); } -// A viewer renders `source` directly, so the slice must be the declaration -// exactly as written — signature, body, and nothing of its neighbours. +// `source` contains exactly the declaration a viewer renders. #[test] fn source_slices_and_doc_comments_are_exact() { let index = index_of(SIMPLE); @@ -239,9 +238,7 @@ fbip fn drop_it(c: Color): Unit = () fn ask_twice(): Int ! {Ask} = ask() + ask() "; -// Every surface declaration kind must appear, addressed in the namespace layer -// that owns it, so a viewer's module page is the module — not the subset of it -// that happens to be a function. +// Every surface declaration kind uses its owning namespace layer. #[test] fn every_declaration_kind_is_indexed_and_addressed_in_its_own_layer() { let index = index_of(KINDS); @@ -354,10 +351,7 @@ fn every_occurrence_span_is_exactly_the_written_identifier() { } } -// A parameter that shadows a top-level name refers to the binder, not the -// definition, so it must not be recorded — the failure mode a hand-written walk -// would produce, and the reason collection lives in the renamer, which already -// carries the scope stack. +// A parameter that shadows a top-level name must not be recorded as a reference. #[test] fn a_local_shadowing_a_global_is_not_an_occurrence() { let full = with_prelude(SHADOWED); @@ -376,10 +370,7 @@ fn a_local_shadowing_a_global_is_not_an_occurrence() { "the shadowing parameter was recorded as a reference: {:?}", of("apply_twice") ); - // The same spelling one function later, unshadowed, is the real reference — - // and it resolves to the canonical name the prelude's glob import gives it, - // not the bare spelling, so a consumer can link it without guessing which - // module it came from. + // The unshadowed use resolves to the prelude's canonical name. assert_eq!(of("use_global"), vec!["Data.List.map"]); } @@ -429,10 +420,8 @@ fn the_occurrence_document_round_trips_and_is_reproducible() { .is_err()); } -// The edges say what a definition depends on; the refs say where in its text. A -// consumer renders a navigable body by slicing `source` at these offsets, so they -// have to index `source` itself — not the compiled program the renamer walked, -// which for the root module begins with the whole prelude. +// Reference offsets are relative to `source`, not the compiled program with its +// prepended prelude. #[test] fn refs_are_offsets_into_the_definitions_own_source() { let index = index_of(SIMPLE); @@ -444,8 +433,7 @@ fn refs_are_offsets_into_the_definitions_own_source() { .collect::>(), vec!["Int", "Int", "double", "double"], ); - // Every definition in the index, not just this one: a ref must slice the name - // it resolves to out of the text it claims to be in. + // Every reference range must contain a source name. for d in &index.defs { for r in &d.refs { let written = &d.source[r.start..r.end]; @@ -504,11 +492,7 @@ fn ding(): Unit ! {Chime} = ring() fn main(): Unit ! {IO} = print(show(str_len(\"x\"))) "; -// A constructor, an operation, and a method are written *inside* another -// declaration and are not definitions in their own right, so a reference to one -// used to resolve to nothing — and `Cons`, `Some`, and `Err` are among the most -// written names in any program. Each now lands on the declaration its source is -// in, the same retarget a lowered instance method gets. +// Constructor and operation references resolve to their owning declarations. #[test] fn a_constructor_or_operation_reference_lands_on_its_declaration() { let index = index_of(MEMBERS); @@ -524,9 +508,7 @@ fn a_constructor_or_operation_reference_lands_on_its_declaration() { assert_eq!(target_of("ding", "ring"), Some("Chime")); } -// A primitive is not a link and not a gap. The distinction has to survive into -// the artifact, or a consumer can only report the name as missing — which reads as -// an incomplete index when the truth is that the compiler implements it. +// Compiler primitives must be distinguishable from missing definitions. #[test] fn primitives_are_named_as_such_rather_than_left_unexplained() { let index = index_of(MEMBERS); @@ -621,9 +603,7 @@ fn two(): Unit ! {Ask, Chirp(Int)} = chirp(ask()) fn main(): Unit = () "; -// A written effect row is the axis a Prism reviewer navigates by, so a label in -// one is a reference like any other name — and it must cover the label's name and -// not its argument list, or the link would swallow `Emit(Int)` entire. +// An effect-row reference covers the label without its argument list. #[test] fn effect_row_labels_are_occurrences_over_the_label_name_alone() { let full = with_prelude(EFFECT_REFS); @@ -766,10 +746,7 @@ fn uses_type_distinguishes_same_named_types_from_different_modules() { ); } -// One broken file must not take the index of everything else down with it: a -// scratch buffer, or a fixture that exists to be invalid, is carried with its -// diagnostic — the same posture the test layer takes — and every other module -// is indexed exactly as if the broken one were absent. +// A broken module carries its diagnostic without blocking other modules. #[test] fn a_module_that_does_not_parse_is_carried_with_its_diagnostic() { let modules = vec![ @@ -855,9 +832,7 @@ fn main(): Unit ! {IO} = () .any(|r| singleton.source[r.start..r.end] == *"Node" && r.target == "Tree")); } -// An effect-row alias is a type-like name. Written inside another alias — the -// composition `alias App = {Boom, Tick}` is what row aliases are for — it must -// link to its declaration exactly as an effect written there does. +// Effect-row aliases link to aliases referenced in their definitions. #[test] fn a_row_alias_links_to_the_aliases_it_mentions() { let index = index_of( @@ -949,9 +924,7 @@ fn a_rename_is_a_move_rather_than_an_add_and_a_delete() { assert_eq!(d.envelope.counts.cone, 0); } -// Reformatting moves text without moving behavior. Separating that from a real -// edit is what keeps a formatting pass from reading as a semantic change — and -// nothing above it re-hashes, which a text diff cannot tell you. +// Reformatting is cosmetic and does not create a dependent cone. #[test] fn a_reformat_is_cosmetic_and_causes_no_cone() { use super::Status; @@ -1084,12 +1057,7 @@ fn decoding_refuses_a_foreign_format_and_a_dangling_edge_source() { .contains("nowhere")); } -// A signature is not source: no file holds it, and the typechecker rendered the -// string. So the renamer has no occurrence in it and no lexer has run over it, -// and a consumer that wanted `List` in a type to be the same link it is in a body -// had to tokenize the rendered string itself — a second tokenizer, drifting from -// this one about what a name is. The compiler renders the type, so the compiler -// lexes it too. +// The compiler lexes rendered signatures because they have no source occurrences. #[test] fn a_rendered_type_carries_its_own_links_and_highlighting() { let index = index_of(MEMBERS); @@ -1169,11 +1137,8 @@ fn an_imported_type_keeps_its_canonical_reference_outside_this_unit() { ); } -// Handling an effect is what *removes* it from a row, so the definition that -// gives an effect its meaning is precisely the one whose inferred effects no -// longer mention it. Read the rows alone and an effect nobody in this unit -// performs relates to nothing at all — which is the standard library's `Output`, -// performed by programs and handled four times. +// Handler edges are collected separately because handling removes an effect from +// the inferred row. #[test] fn an_effect_reaches_the_definitions_that_handle_it() { let index = index_of(HANDLED); @@ -1204,11 +1169,7 @@ fn silence(): Unit = handle ding() with ring() resume k => k(()) "; -// A declaration's members have to come from the declaration, not from what -// happens to reference them. `Output`'s operations are performed by *programs*, so -// an index of the library that declares it would recover none of them from -// occurrences — and a reader searching for `out_print` would be told the name does -// not exist. +// Members come from their declaration so unused operations remain indexed. #[test] fn a_declaration_records_the_members_it_introduces() { let index = index_of(MEMBERS); @@ -1242,12 +1203,7 @@ fn a_declaration_records_the_members_it_introduces() { ); } -// Elaboration lifts each instance method to its own top-level function -// (`i@showInt@show`), so an instance has no Core node of its own. Asking the -// dependency graph about the instance's own name therefore answered nothing, and -// not one of the standard library's 100 instances had a single outgoing edge — -// a card for an instance could show the class it implements and nothing about the -// functions plainly written in its body. +// Instance dependencies are collected from their lifted method functions. #[test] fn an_instance_calls_what_its_methods_call() { let index = index_of(INSTANCE_BODY); diff --git a/src/index/typed.rs b/src/index/typed.rs index 975132d6..7efa08d7 100644 --- a/src/index/typed.rs +++ b/src/index/typed.rs @@ -1,33 +1,16 @@ //! Hover types for the names inside a definition. //! -//! A reader looking at `fn row(gap : Int, children : List(Pict))` wants to know -//! what `gap` is where it is *used*, not only where it is declared, and the -//! signature two lines up answers that only for the simplest bodies. The -//! typechecker already knows: it stamps every expression node with an identity and -//! records the zonked type against it, which is what `prism dump typespans` and -//! the book's typed tooltips read. +//! The typechecker records a zonked type for each expression identity. This +//! module maps those types to the names in each definition's source. //! -//! This joins the same two tables — a node's span from the AST, its type from the -//! checked facts — for every module rather than only the entry one, and rebases -//! the spans onto each definition's own source. It costs no extra pass: the index's -//! own elaboration asks for the type strings (`FrontRequest::IdentityTooltips`), -//! which only fills side tables, so the Core every address is taken over is -//! byte-identical to the one without them. +//! Spans come from the AST and types from checked facts. The index requests these +//! side tables with `FrontRequest::IdentityTooltips`; Core remains unchanged. //! -//! Names only, deliberately: the variables a body mentions and the binders a -//! pattern introduces. Every subterm has a type, and emitting all of them would -//! multiply the payload and nest spans inside each other — the whole -//! `Row(gap, children)` contains `gap` — while a reader hovering a body is asking -//! about the names in it. Names also cannot overlap, which keeps the consumer's -//! painting a flat merge of intervals rather than a tree. +//! Only variable uses and pattern binders are emitted. Their spans do not +//! overlap, so consumers can merge them as flat intervals. //! -//! A pattern binder costs one thing extra. `y` in `Cons(y, rest)` is a `Pattern`, -//! not an expression, and identity is what the type tables are keyed by — so -//! `desugar::ids` stamps arm patterns alongside the expressions around them, and -//! `check_pat` records each binder's type where it already computes it. Over the -//! standard library that is 1,786 further names for 14 kB, the payload growing far -//! more slowly than the span count because a binder's type is nearly always one -//! the table already holds. +//! Pattern binders receive identities in `desugar::ids`; `check_pat` records +//! their types. use std::collections::BTreeMap; use std::fmt::Write as _; @@ -50,9 +33,7 @@ type Header<'a> = (Vec<&'a str>, Vec<&'a Type>); /// Attach every definition's hover types, returning the table they index. /// -/// Interned because the same type is written over and over — a handful of distinct -/// types account for most occurrences — and a rendered `forall` repeated a thousand -/// times would be most of what the payload weighs. +/// Types are interned because many occurrences share the same rendering. pub(super) fn attach_types(defs: &mut [Def], production: &AddressableSurface) -> Vec { let hir = crate::hir::build(&production.checked); let checked: BTreeMap<&str, &Type> = production @@ -123,21 +104,8 @@ fn bare(ty: &Type) -> &Type { /// Every variable in an expression tree, with the type the checker synthesized. fn variables(e: &S>, hir: &crate::hir::CheckedHir<'_>, out: &mut Vec) { if matches!(e.node, Expr::Var(_)) { - // The presentable string, not the raw node type. The latter still carries - // the checker's own inference variables, which are unreadable and, since - // every definition numbers them afresh, defeat interning entirely: dropping - // the filter below takes the standard library's table from 2036 entries to - // 2758. The fallback is the node's own term, and only when that resolved to - // something readable, since an unsolved existential prints as `?846`, which - // is worse than silence. - // - // An argument was long assumed to be missing here, on the reasoning that it - // is checked against its parameter rather than synthesized. It is not: a - // call reconciles head and arguments by unification, so both carry a type, - // and pushing the checked type from `check` was measured against the whole - // standard library and produced zero additional spans. What made the gap - // look real was a coordinate bug in `rebase_onto`, which dropped every body - // span whenever a prelude had been prepended. + // Prefer the presentable tooltip over the raw node type, which may contain + // unstable inference variables. The fallback omits unsolved existentials. out.extend(typed_at(e.id, e.span, hir)); } // Patterns are not expressions, so the structural walk does not reach them. @@ -180,17 +148,8 @@ fn typed_at( }) } -// Move a declaration's spans onto the declaration itself, subtracting where the -// compiler placed it rather than where the index did. -// -// The two are not the same coordinate, and this is the whole difficulty. A body -// node is reported at its position in the compiled source, which begins with the -// prelude; the index holds the declaration at its position in its own file. The -// difference between a node and its owner is the same in either, so subtracting -// the owner here yields an offset both agree on — the same bargain `attach_refs` -// makes, and it has to be made in the same coordinate system the spans came from. -// Subtracting the index's own `Def::span` instead silently drops every body type -// whenever a prelude was prepended, which is every single-file index. +// Rebase compiler-source spans onto the declaration. Compiler coordinates include +// the prepended prelude, while index coordinates refer to the module file. fn rebase_onto(spans: &mut Vec, owner: usize) { spans.retain_mut(|t| { let (Some(start), Some(end)) = (t.start.checked_sub(owner), t.end.checked_sub(owner)) @@ -216,18 +175,8 @@ fn rebase(t: &Typed, def: &Def) -> Option { /// The parameters at their binding site, which have no node of their own. /// -/// These are rendered from the declaration's generalized scheme, so they use the -/// same variable names as the signature above them. A body node agrees with them -/// because the checker now renders every span of a declaration under that same -/// scheme (`generalize_seeded`) rather than canonicalizing each node on its own: -/// `map`'s `xs` read `List(b)` here and `List(a)` two lines down until it did. -/// -/// A parameter is not an expression, so it carries neither an identity nor a span, -/// and the header is exactly where a reader looks first. The names come from the -/// declaration and their types from the domains of its checked type, so only the -/// position is recovered from the text — the same bargain the member list makes, -/// and safe for the same reason: only a token equal to a name this declaration -/// actually binds is considered. +/// Types come from the generalized scheme, and positions are recovered from the +/// declaration text because parameters have no expression identity or span. fn header_names(def: &Def, header: Option<&Header<'_>>) -> Vec { let Some((names, doms)) = header else { return Vec::new(); diff --git a/src/lib.rs b/src/lib.rs index 8b53d387..dfdfbe8c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,8 +2,8 @@ //! //! # Supported surface //! -//! Most of the compiler is exposed so that tools can build ON it, not just call -//! it. The essential guarantee is that **merged Core is the semantic +//! Most of the compiler is exposed so that tools can build on its components. +//! The essential guarantee is that **merged Core is the semantic //! authority**, so the surface is organized around consuming and producing Core: //! //! - [`core`]: the call-by-push-value Core IR and its content-addressed @@ -140,12 +140,12 @@ pub use driver::{ CutReport, CutTarget, DeltaReport, DurableRun, EvidenceTier, FetchReport, ImpactReport, InterfaceDelta, ModuleCheckReport, ModuleGraph, ModuleGraphNode, ModuleInterface, ModuleInterfaceEntry, ModuleInvalidation, ModuleInvalidationCause, NamespaceIdentity, - NamespaceLayers, PatchRefusal, PatchRefusalBody, PatchRefusalSubject, PublicDef, RecordedRun, - RehydratedModuleInterface, Scheduler, SessionStats, StagedPatch, StdlibHash, StepRuler, - StepRulerRow, SuspendAtCut, SuspendCut, SuspendResult, TimingSink, MODULE_GRAPH_FORMAT, - MODULE_INTERFACE_FORMAT, PATCH_BEHAVIOR_CORPUS_FORMAT, PATCH_BEHAVIOR_FORMAT, - PATCH_DELTA_FORMAT, PATCH_FETCH_FORMAT, PATCH_IMPACT_FORMAT, PATCH_REFUSAL_FORMAT, - PATCH_STAGE_FORMAT, STEP_RULER_FORMAT, + NamespaceLayers, PatchRefusal, PatchRefusalBody, PatchRefusalSubject, PhaseTally, PublicDef, + RecordedRun, RehydratedModuleInterface, Scheduler, SessionStats, StagedPatch, StdlibHash, + StepRuler, StepRulerRow, SuspendAtCut, SuspendCut, SuspendResult, TimingSink, + MODULE_GRAPH_FORMAT, MODULE_INTERFACE_FORMAT, PATCH_BEHAVIOR_CORPUS_FORMAT, + PATCH_BEHAVIOR_FORMAT, PATCH_DELTA_FORMAT, PATCH_FETCH_FORMAT, PATCH_IMPACT_FORMAT, + PATCH_REFUSAL_FORMAT, PATCH_STAGE_FORMAT, STEP_RULER_FORMAT, }; #[cfg(feature = "native")] pub use driver::{ diff --git a/src/lineage/run.rs b/src/lineage/run.rs index 237dae2b..9d035648 100644 --- a/src/lineage/run.rs +++ b/src/lineage/run.rs @@ -193,7 +193,7 @@ fn output_payload(bytes: &[u8]) -> OutputPayload { } // Assemble a run graph. Roots, argv, and observed environment/file reads are the -// run's inputs; the compiler identifies it; the trace digest, stdout, and file +// run's inputs. The compiler identifies it. The trace digest, stdout, and file // writes are what it produced. Every edge fans out from the request node. fn assemble_run(run: &RunLineage) -> LineageGraph { let request_id = graph::request_node_id(&run.request); diff --git a/src/pkg/trust.rs b/src/pkg/trust.rs index be3d5d75..3bf69340 100644 --- a/src/pkg/trust.rs +++ b/src/pkg/trust.rs @@ -20,7 +20,7 @@ //! Signing is done by an external tool behind a narrow seam ([`sign`], //! [`verify_signature`]), so no cryptographic dependency enters the compiler. The //! default is `ssh-keygen -Y sign`/`-Y verify` (namespaced signatures, present -//! wherever OpenSSH is); `minisign` is an alternative behind the same seam; and an +//! wherever OpenSSH is). `minisign` is an alternative behind the same seam. An //! explicit unsigned mode is a development escape hatch that [`audit`] refuses //! unless the operator allows it. @@ -76,6 +76,18 @@ pub const INDEX_KIND_NAMESPACE: &str = crate::driver::NAMESPACE_ARTIFACT_KIND; /// Signed-index kind for a store-served source bundle. pub const INDEX_KIND_SOURCE: &str = "source-bundle"; +// A published package's root hash is reachable only through the signed index, +// which the store's own query/index layers know nothing about. `store gc` +// only ever spares a hash it can see reachability for (see +// `prism_store::disk::gc`), so a published root additionally gets a `refs` +// entry pointing at itself: durable, gc-visible proof that this object is a +// package root and not stale query-cache scratch. +const PKG_ROOT_REF_PREFIX: &str = "pkg-root-"; + +fn pkg_root_ref(root: &str) -> String { + format!("{PKG_ROOT_REF_PREFIX}{root}") +} + /// One signed pointer. /// /// A package `origin` exposes a human `name` at a git `tag` that resolves to a @@ -1286,6 +1298,7 @@ pub fn publish_source_cmd( let bundle = encode_source_bundle([(name, user_src)]); let root = Digest::from(blake3::hash(&bundle).to_hex().to_string()); store.put(&root, &bundle)?; + store.set_ref(&pkg_root_ref(root.as_str()), root.as_str())?; let dst = DiskTransport::open(&store_root)?; let log = store_log(&store_root); let row = IndexRow { diff --git a/src/repl/mod.rs b/src/repl/mod.rs index 112fbe27..cd355a23 100644 --- a/src/repl/mod.rs +++ b/src/repl/mod.rs @@ -33,9 +33,10 @@ use crate::sym::Sym; use crate::syntax::ast::{ClassDecl, Core, Expr, ImportDecl, Program, S}; use crate::syntax::desugar::{desugar, desugar_expr}; use crate::syntax::reflect::{splice, splice_expr}; +use crate::tc::infer_checked_expr; use crate::types::{ - check, check_allow_holes, infer_expr, infer_expr_dicts, infer_expr_dicts_allow_holes, - show_effects, show_type_with_effects, Checked, CtorInfo, Type, + check, check_allow_holes, infer_expr, show_effects, show_type_with_effects, Checked, CtorInfo, + Type, }; // Canonical commands. Any unambiguous prefix resolves to one (`:lo` -> :load, @@ -508,17 +509,12 @@ impl Session { let mut surface = parse_expr(&text)?; built.front(&mut surface)?; let e = desugar_expr(&surface)?; - let (ty, eff, dicts) = if self.flags.holes { - let (ty, eff, dicts, _) = infer_expr_dicts_allow_holes(&built.checked, &e)?; - (ty, eff, dicts) - } else { - infer_expr_dicts(&built.checked, &e)? - }; + let inferred = infer_checked_expr(&built.checked, &e, self.flags.holes)?; let (comp, synthesized) = elaborate_expr_defs( &built.checked, &e, &built.arity, - Some(&dicts), + Some(&inferred.facts), &built.consts, )?; // Elaboration synthesizes structural `show` helpers on demand. A @@ -541,7 +537,11 @@ impl Session { }; drop(out); drop(input); - Ok((v.repr(), ty.show(), show_effects(&eff))) + Ok(( + v.repr(), + inferred.ty.show(), + show_effects(&inferred.effects), + )) } } @@ -1795,6 +1795,56 @@ mod tests { ); } + #[test] + fn expression_resolution_wins_when_node_ids_collide_with_the_session() { + let session = Session::probe( + vec![Seg::Text( + "type Point = Point { x: Int, y: Int }".to_string(), + )], + Vec::new(), + ); + let (_, mut built) = session.build().expect("record declaration type checks"); + + // Program and prompt ids are allocated independently and both begin at + // one. Seed the session table at this prompt's field-access id with the + // wrong (but otherwise valid) field. A partial overlay would silently + // elaborate `.y` as `.x` and return 11. + let mut surface = parse_expr("(Point { x = 11, y = 22 }).y").expect("expression parses"); + built.front(&mut surface).expect("expression resolves"); + let expression = desugar_expr(&surface).expect("expression desugars"); + let mut session_fields = BTreeMap::new(); + session_fields.insert(expression.id, ("Point".to_string(), 0, 2)); + built.checked.facts = crate::hir::NodeFacts::from_tables( + session_fields, + BTreeMap::new(), + BTreeMap::new(), + BTreeMap::new(), + BTreeMap::new(), + BTreeMap::new(), + BTreeMap::new(), + BTreeSet::new(), + BTreeMap::new(), + ); + + let (value, ty, _) = session + .eval_chained(&built, "(Point { x = 11, y = 22 }).y") + .expect("checked prompt facts elaborate independently"); + assert_eq!(value, "22"); + assert_eq!(ty, "Int"); + } + + #[test] + fn standalone_expression_runs_the_or_null_representation_check() { + let (session, built) = fresh(); + let error = session + .eval_chained(&built, "This(())") + .expect_err("the zero word cannot inhabit the optimized nullable"); + let Error::Type(error) = error else { + panic!("expected a type error, got {error}"); + }; + assert_eq!(error.code(), Some("E1019")); + } + // Elaborating a structural print/interpolation synthesizes a `_show_*` // helper on demand. A whole-program compile folds those into its Core; the // REPL evaluates one expression at a time against a pre-built environment, diff --git a/src/resolve/mod.rs b/src/resolve/mod.rs index 2951b023..bda6baf9 100644 --- a/src/resolve/mod.rs +++ b/src/resolve/mod.rs @@ -1603,11 +1603,9 @@ impl<'a> Rw<'a> { /// [`Self::value`] where `span` is the written identifier itself, recording /// the reference as an [`Occurrence`]. /// - /// Deliberately separate from `value`. Some resolution sites pass the span of - /// an *enclosing* node — the whole instance declaration, the whole - /// `Cons(p, rest)` pattern, the whole record literal — because the name they - /// resolve has no span of its own and there is nowhere narrower to point. Those - /// spans are right for a diagnostic, which underlines the construct, and wrong + /// Separate from `value`. Some resolution sites pass an enclosing node's span + /// because the resolved name has no span of its own. Those spans suit a + /// diagnostic, which underlines the construct, but are wrong /// for a link, which must cover the name and nothing else. Recording them would /// silently turn a whole declaration into one clickable region, so a site opts /// in here only when its span is exact: an expression variable, and an diff --git a/src/scheme_canon.rs b/src/scheme_canon.rs index 51af591e..f9bb14e0 100644 --- a/src/scheme_canon.rs +++ b/src/scheme_canon.rs @@ -1,38 +1,22 @@ -//! The canonical-scheme contract: one versioned spelling for "these two -//! checkers agree on this declaration's type". +//! Versioned canonical spelling used to compare checker output. //! -//! The bootstrap workbench compares the authoritative Rust checker against the -//! self-hosted shadow checker declaration by declaration. Each side prints a -//! scheme; agreement is string equality. That only means anything if both sides -//! print the *same function* of the type, so the function is pinned here as a -//! named, versioned contract rather than an ad-hoc convention: +//! Rust and the self-hosted checker compare schemes as strings under this +//! contract: //! -//! - The input is the Rust checker's stable type spelling (the `tc-facts` -//! `scheme` field). -//! - The only transformation is alpha-normalization of the binders introduced -//! by a single leading `forall`: the i-th binder becomes `$i`, in both the -//! binder list and the body. Occurrences are matched at identifier-token -//! boundaries (maximal runs of ASCII alphanumerics and `_`), so `a` never -//! rewrites part of `Maybe` or `a1`. Every other byte of the spelling is -//! preserved. +//! - Input is the stable `tc-facts` scheme spelling. +//! - A single leading `forall` is alpha-normalized by renaming its binders to +//! `$0`, `$1`, and so on at identifier boundaries. //! - A spelling with no leading `forall` is already canonical. //! -//! The shadow checker renders the same form structurally (`canon_scheme` in -//! `packages/tc/src/Bootstrap.pr`), and the two implementations are held -//! together by the version handshake in the bootstrap wire protocol and by the -//! end-to-end parity fixture. Any change to either side's output is a new -//! contract: bump [`SCHEME_CANON_CONTRACT`] and move both implementations in -//! the same commit. Downstream artifacts that state expected schemes -//! (structured refusals, pinned goal schemes) must speak this contract and -//! carry its identifier, never a raw checker spelling. +//! The bootstrap protocol and parity fixture pin both implementations. Changes +//! require a [`SCHEME_CANON_CONTRACT`] bump in both checkers. use std::collections::HashMap; /// Version identifier for the canonical scheme spelling. /// /// Stamped into the bootstrap report and demanded of the shadow checker's -/// protocol header, so a drifted normalization fails loudly instead of -/// quietly changing what "agrees" means. +/// protocol header, so a drifted normalization is rejected. pub const SCHEME_CANON_CONTRACT: &str = "prism-scheme-canon-v1"; const FORALL_PREFIX: &str = "forall "; @@ -103,6 +87,10 @@ mod tests { "forall a e. (a) -> Unit ! {IO | e}", "forall $0 $1. ($0) -> Unit ! {IO | $1}", ), + ( + "forall e a. ((a) -> a ! {Tick, e}, a) -> a ! {Tick, e}", + "forall $0 $1. (($1) -> $1 ! {Tick, $0}, $1) -> $1 ! {Tick, $0}", + ), ( "forall a. (List(a), (a) -> Bool) -> List(a)", "forall $0. (List($0), ($0) -> Bool) -> List($0)", @@ -123,6 +111,7 @@ mod tests { "(Int) -> Bool", "forall a b. (a, Maybe(b)) -> a", "forall a e. (a) -> Unit ! {IO | e}", + "forall e a. ((a) -> a ! {Tick, e}, a) -> a ! {Tick, e}", "forall $0. ($0) -> $0", ]; for spelling in spellings { diff --git a/src/store/cert.rs b/src/store/cert.rs index 3c6ca15f..951dfbab 100644 --- a/src/store/cert.rs +++ b/src/store/cert.rs @@ -53,7 +53,8 @@ //! //! # Totality //! -//! [`decode_cert`](crate::store::cert::decode_cert) never panics on hostile bytes: every varint is byte-capped and +//! [`decode_cert`](crate::store::cert::decode_cert) never panics on hostile bytes. +//! Every varint is byte-capped and //! every length is bounded (the shared `def`-codec reader), the scheme and kind are //! checked before the body, and trailing bytes are rejected. Decode is a `Result`. @@ -80,6 +81,11 @@ pub const CLAIM_LEAN_CHECKED: u64 = 1; // the number. const CLAIM_REPLAY_VERIFIED: u64 = 2; const CLAIM_LINEAGE_VERIFIED: u64 = 3; +/// The shadow parser reproduced the authoritative parser's judgment. +/// +/// Made over a comparison identity rather than a core hash or a sidecar digest, +/// and the fourth member of the one global claim number space. +pub const CLAIM_SHADOW_PARSE_AGREED: u64 = 4; /// The human-facing name of the one live claim. pub const CLAIM_PARITY_PASSED_NAME: &str = "parity-passed"; @@ -89,6 +95,8 @@ pub const CLAIM_LEAN_CHECKED_NAME: &str = "lean-checked"; pub const CLAIM_REPLAY_VERIFIED_NAME: &str = "replay-verified"; /// The human-facing name of the artifact/edge rehash claim. pub const CLAIM_LINEAGE_VERIFIED_NAME: &str = "lineage-verified"; +/// The human-facing name of the shadow-parser agreement claim. +pub const CLAIM_SHADOW_PARSE_AGREED_NAME: &str = "shadow-parse-agreed"; // The evidence-row keys a lineage certificate carries: one home for the family so // a minter and a reader never retype a key. A row is a `key = value` string pair, @@ -147,6 +155,11 @@ impl Claim { fn reserved_claim_name(n: u64) -> String { if n == CLAIM_LEAN_CHECKED { CLAIM_LEAN_CHECKED_NAME.to_string() + } else if n == CLAIM_SHADOW_PARSE_AGREED { + // Named, not verified, from a parity or lineage reader's seat: the claim + // belongs to another family, so those readers report it as recognized + // rather than pretending it is theirs to check. + CLAIM_SHADOW_PARSE_AGREED_NAME.to_string() } else { format!("reserved-claim-{n}") } @@ -234,6 +247,29 @@ pub fn decode_cert(bytes: &[u8]) -> Result { }) } +/// The subject and claim of any `cert`-kind envelope, read without committing to +/// a claim family's body layout. +/// +/// Every family shares one header (the scheme tag, the kind varint, the subject) +/// and puts its claim discriminant immediately after it. That is what makes a +/// single global claim number space useful rather than decorative: a reader that +/// cannot parse a body can still say which family the frame belongs to, and +/// report it as recognized rather than as corruption. +/// +/// # Errors +/// A foreign scheme, a non-cert kind, or a truncated header. +pub fn envelope_head(bytes: &[u8]) -> Result<(String, u64), CodecError> { + let mut r = Reader::new(bytes); + if r.string()? != HASH_SCHEME { + return Err(CodecError::Scheme); + } + if r.uvarint()? != u64::from(WireKind::Cert.varint()) { + return Err(CodecError::Kind); + } + let subject = r.string()?; + Ok((subject, r.uvarint()?)) +} + /// Write `cert` into the store, keyed by its subject. Idempotent: re-emitting the /// same certificate is a [`Written::Hit`]. /// @@ -262,8 +298,8 @@ pub enum CertStatus { /// Read and verify the certificate a subject carries, if any. /// /// A decode failure, a subject that does not match, or a foreign scheme is a named -/// [`CertStatus::Failed`]; an absent certificate is [`CertStatus::Absent`]; a -/// reserved claim is [`CertStatus::Unverifiable`]; the one live claim under the +/// [`CertStatus::Failed`]. An absent certificate is [`CertStatus::Absent`]. A +/// reserved claim is [`CertStatus::Unverifiable`]. The one live claim under the /// current scheme is [`CertStatus::Verified`]. #[must_use] pub fn check_cert(store: &Store, subject: &str) -> CertStatus { @@ -272,33 +308,46 @@ pub fn check_cert(store: &Store, subject: &str) -> CertStatus { Ok(None) => return CertStatus::Absent, Err(e) => return CertStatus::Failed(format!("certificate unreadable: {e}")), }; - let cert = match decode_cert(&bytes) { - Ok(c) => c, + // Classify from the shared header before parsing a body. A frame minted by + // another claim family has a layout this reader does not know, and calling + // that corruption would turn a neighbour's valid certificate into an audit + // failure. + let (stored_subject, claim) = match envelope_head(&bytes) { + Ok(head) => head, Err(e) => return CertStatus::Failed(format!("corrupt certificate ({e})")), }; - if cert.subject.as_str() != subject { + if stored_subject != subject { return CertStatus::Failed(format!( "certificate subject {} does not match root {}", - short(&cert.subject), + short(&stored_subject), short(subject) )); } + if claim != CLAIM_PARITY_PASSED { + return CertStatus::Unverifiable(format!( + "{} (claim is recognized but unverified by this build)", + reserved_claim_name(claim) + )); + } + let cert = match decode_cert(&bytes) { + Ok(c) => c, + Err(e) => return CertStatus::Failed(format!("corrupt certificate ({e})")), + }; if cert.scheme != HASH_SCHEME { return CertStatus::Failed(format!( "certificate made under foreign scheme {:?}; this build speaks {HASH_SCHEME:?}", cert.scheme )); } - match cert.claim { - Claim::ParityPassed => CertStatus::Verified(format!( - "{CLAIM_PARITY_PASSED_NAME}@{} by {}", - cert.scheme, cert.compiler - )), - Claim::Reserved(n) => CertStatus::Unverifiable(format!( - "{} (claim is recognized but unverified by this build)", - reserved_claim_name(n) - )), + // The claim was settled from the header, so this is the parity claim or the + // two readings of the same bytes disagree, which is corruption. + if cert.claim != Claim::ParityPassed { + return CertStatus::Failed("certificate header and body disagree on the claim".to_string()); } + CertStatus::Verified(format!( + "{CLAIM_PARITY_PASSED_NAME}@{} by {}", + cert.scheme, cert.compiler + )) } // A short hash prefix for human-facing lines, matching the store's display habit. @@ -447,35 +496,50 @@ pub fn lineage_cert( } } -/// Serialize a lineage certificate to its `cert`-kind envelope. The bytes are its -/// identity. -#[must_use] -pub fn encode_lineage_cert(cert: &LineageCert) -> Vec { +/// The decoded fields of a row-body `cert` envelope, before a claim family +/// interprets the discriminant. +pub(crate) struct RowBody { + pub(crate) subject: String, + pub(crate) claim: u64, + pub(crate) scheme: String, + pub(crate) compiler: String, + pub(crate) rows: Vec, +} + +/// Serialize the row-body `cert` envelope: the fixed header, then a claim +/// discriminant, the scheme and compiler, and a capped list of `key = value` +/// rows. +/// +/// Every claim family whose evidence is rows rather than a fixed tuple shares +/// these bytes. That is what lets the claim discriminants stay one global number +/// space: a reader that does not know a family still decodes the envelope and +/// reports the claim as recognized-but-untrusted, which it could not do if each +/// family invented its own layout. +pub(crate) fn encode_row_body( + subject: &str, + claim: u64, + scheme: &str, + compiler: &str, + rows: &[CertRow], +) -> Vec { let mut out = Vec::new(); put_str(&mut out, HASH_SCHEME); put_uvarint(&mut out, u64::from(WireKind::Cert.varint())); - put_str(&mut out, &cert.subject); - put_uvarint(&mut out, cert.claim.to_varint()); - put_str(&mut out, &cert.scheme); - put_str(&mut out, &cert.compiler); - put_uvarint( - &mut out, - u64::try_from(cert.evidence.len()).unwrap_or(u64::MAX), - ); - for r in &cert.evidence { + put_str(&mut out, subject); + put_uvarint(&mut out, claim); + put_str(&mut out, scheme); + put_str(&mut out, compiler); + put_uvarint(&mut out, u64::try_from(rows.len()).unwrap_or(u64::MAX)); + for r in rows { put_str(&mut out, &r.key); put_str(&mut out, &r.value); } out } -/// Decode a lineage `cert`-kind envelope. Total: a `Result`, never a panic, header -/// checked before the body, the evidence count capped, trailing bytes rejected. -/// -/// # Errors -/// A foreign scheme, a non-cert kind, a truncated or oversized field, an -/// over-count of evidence rows, or trailing bytes. -pub fn decode_lineage_cert(bytes: &[u8]) -> Result { +/// Decode a row-body `cert` envelope. Total: a `Result`, never a panic, header +/// checked before the body, the row count capped, trailing bytes rejected. +pub(crate) fn decode_row_body(bytes: &[u8]) -> Result { let mut r = Reader::new(bytes); if r.string()? != HASH_SCHEME { return Err(CodecError::Scheme); @@ -484,28 +548,58 @@ pub fn decode_lineage_cert(bytes: &[u8]) -> Result { return Err(CodecError::Kind); } let subject = r.string()?; - let claim = LineageClaim::from_varint(r.uvarint()?); + let claim = r.uvarint()?; let scheme = r.string()?; let compiler = r.string()?; let count = r.uvarint()?; if count > MAX_EVIDENCE_ROWS { return Err(CodecError::TooLarge); } - let mut evidence = Vec::new(); + let mut rows = Vec::new(); for _ in 0..count { let key = r.string()?; let value = r.string()?; - evidence.push(CertRow { key, value }); + rows.push(CertRow { key, value }); } if !r.at_end() { return Err(CodecError::TrailingBytes); } - Ok(LineageCert { - subject: Digest::from(subject), + Ok(RowBody { + subject, claim, scheme, compiler, - evidence, + rows, + }) +} + +/// Serialize a lineage certificate to its `cert`-kind envelope. The bytes are its +/// identity. +#[must_use] +pub fn encode_lineage_cert(cert: &LineageCert) -> Vec { + encode_row_body( + &cert.subject, + cert.claim.to_varint(), + &cert.scheme, + &cert.compiler, + &cert.evidence, + ) +} + +/// Decode a lineage `cert`-kind envelope. Total: a `Result`, never a panic, header +/// checked before the body, the evidence count capped, trailing bytes rejected. +/// +/// # Errors +/// A foreign scheme, a non-cert kind, a truncated or oversized field, an +/// over-count of evidence rows, or trailing bytes. +pub fn decode_lineage_cert(bytes: &[u8]) -> Result { + let body = decode_row_body(bytes)?; + Ok(LineageCert { + subject: Digest::from(body.subject), + claim: LineageClaim::from_varint(body.claim), + scheme: body.scheme, + compiler: body.compiler, + evidence: body.rows, }) } diff --git a/src/store/codec.rs b/src/store/codec.rs index f615b8bb..a71e3397 100644 --- a/src/store/codec.rs +++ b/src/store/codec.rs @@ -161,9 +161,8 @@ impl Decoded { } // The node-table tag: one discriminant for every Core value and computation -// shape. Encoded as a uvarint; the array below is the single source of truth for -// the numbering, so encode (`as u8`) and decode (index into the array) cannot -// drift. +// shape. Encoded as a uvarint; the array below defines the numbering used by both +// encode (`as u8`) and decode (index into the array). #[derive(Clone, Copy, PartialEq, Eq)] #[repr(u8)] enum Tag { @@ -1308,8 +1307,7 @@ pub fn decode_def(bytes: &[u8]) -> Result { for _ in 0..member_count { let param_count = r.bounded_len()?; let dict_arity = r.bounded_len()?; - // dict_arity is not part of the hash, but a valid entry never claims more - // dictionary params than it has parameters. + // Although excluded from the hash, dict_arity cannot exceed param_count. if dict_arity > param_count { return Err(CodecError::Malformed); } diff --git a/src/store/mod.rs b/src/store/mod.rs index b63e6cdd..0d8fb942 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -17,8 +17,6 @@ /// /// A digest that attests a property of another digest. The minimal certificate is /// a parity-passed record keyed by hash. -/// The Incr durable-snapshot bridge: a named blob rides the store's object layer -/// (keyed by content hash) with a ref for the caller tag; see [`bridge`]. pub mod cert; pub mod codec; /// Store-level instance coherence. @@ -29,6 +27,11 @@ pub mod coherence; /// Committing an elaborated program's definitions into the store. pub mod commit; pub use commit::commit_program; +/// The shadow-parser comparison receipt: the deterministic half attested as a +/// certificate, the machine readings recorded as a decision; see [`receipt`]. +pub mod receipt; +/// The Incr durable-snapshot bridge: a named blob rides the store's object layer +/// (keyed by content hash) with a ref for the caller tag; see [`bridge`]. pub use prism_store::bridge; /// The on-disk two-layer store that holds the codec's bytes; see [`disk::Store`]. pub use prism_store::disk; diff --git a/src/store/receipt.rs b/src/store/receipt.rs new file mode 100644 index 00000000..923b4e61 --- /dev/null +++ b/src/store/receipt.rs @@ -0,0 +1,498 @@ +//! The shadow-parser comparison receipt. +//! +//! What a shadow run proved, kept apart from what it merely measured. +//! +//! A shadow run parses a corpus twice, once with the authoritative parser and +//! once with the parser written in Prism, and compares the results. The receipt +//! is the record of that comparison. It is split across two store layers, and +//! the split is the design rather than an implementation detail: +//! +//! - the **certificate** carries the facts that are a function of the inputs: +//! the two parser identities, the corpus identity, both syntax hashes, the +//! diagnostic verdict, the downstream Core hash, and the compiler-work +//! counters. Certificates are immutable on write, so re-running an unchanged +//! comparison must produce byte-identical bytes or the store refuses it. That +//! refusal is the point: it turns a second run into a reproduction check that +//! nobody has to remember to perform. +//! - the **decision** carries the readings that are a function of the machine: +//! wall time per phase. Those legitimately differ between two correct runs, so +//! they go to the last-write-wins layer. Putting them in the certificate would +//! make an honest re-run a corruption error, and the only way to keep the +//! certificate writable would be to stop attesting anything. +//! +//! Both are keyed by the same subject, so a reader holding the comparison +//! identity finds the proof and the reading together without a second index. +//! +//! **Anti-vacuity.** A receipt may not claim a phase it did not exercise. +//! [`ShadowReceipt::new`](crate::store::receipt::ShadowReceipt::new) rejects a +//! phase whose Core visits are zero, so "the +//! optimizer ran and cost nothing" cannot be recorded as a success; it is either +//! a phase that did not run or an instrument that did not observe it, and both +//! are reasons to refuse rather than to attest. The check is on visits alone: +//! rebuilt nodes are legitimately zero for a read-only phase. + +use std::collections::BTreeMap; +use std::fmt::Write as _; +use std::io; +use std::time::Duration; + +use prism_common::digest::Digest; + +use crate::core::HASH_SCHEME; +use crate::driver::PhaseTally; +use crate::store::cert::{ + decode_row_body, encode_row_body, CertRow, CertStatus, CLAIM_SHADOW_PARSE_AGREED, + CLAIM_SHADOW_PARSE_AGREED_NAME, +}; +use crate::store::disk::{Store, Written}; +use crate::store::CodecError; + +// The attesting compiler version, the one source of truth being the crate version. +const COMPILER_VERSION: &str = env!("CARGO_PKG_VERSION"); + +// The decision layer's kind for a shadow run's machine readings. Lowercase and +// hyphens only, which is what the decision path validator accepts. +const TIMING_KIND: &str = "shadow-parse-timing"; +// The versioned tag the timing decision's bytes lead with, so a reader that finds +// an older shape rejects it instead of misreading it. +const TIMING_FORMAT: &str = "prism-shadow-parse-timing-v1"; + +// The certificate's evidence-row keys. One home for the family: a minter and a +// reader never retype a key, and adding a fact means adding it here. +const ROW_AUTHORITY: &str = "authority"; +const ROW_SHADOW: &str = "shadow"; +const ROW_CORPUS: &str = "corpus"; +const ROW_CORPUS_FILES: &str = "corpus-files"; +const ROW_SYNTAX_HASH_AUTHORITY: &str = "syntax-hash-authority"; +const ROW_SYNTAX_HASH_SHADOW: &str = "syntax-hash-shadow"; +const ROW_DIAGNOSTICS: &str = "diagnostics"; +const ROW_CORE_HASH: &str = "core-hash"; +const ROW_MAX_DEPTH: &str = "max-depth"; + +// Per-phase rows are `phase..`, one family built from these parts so +// the spelling has a single home on both the minting and the reading side. +const PHASE_PREFIX: &str = "phase."; +const PHASE_INVOCATIONS: &str = "invocations"; +const PHASE_VISITS: &str = "visits"; +const PHASE_REBUILT: &str = "rebuilt"; + +// The diagnostic verdict's two spellings. A divergence count rides the second, so +// a reader never has to infer disagreement from a missing row. +const DIAGNOSTICS_AGREED: &str = "agreed"; +const DIAGNOSTICS_DIVERGED_PREFIX: &str = "diverged:"; + +/// Why a receipt could not be built. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ReceiptError { + /// A phase was offered with no Core visits recorded. Either it did not run or + /// the instrument did not observe it; a receipt may claim neither. + VacuousPhase(String), + /// No phases were offered at all, which would attest a comparison that + /// exercised nothing. + NoPhases, + /// The comparison covered no corpus files. + EmptyCorpus, +} + +impl std::fmt::Display for ReceiptError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::VacuousPhase(phase) => write!( + f, + "phase {phase:?} recorded no Core visits; a receipt cannot claim to have \ + exercised a phase whose counter stayed zero" + ), + Self::NoPhases => f.write_str("a receipt must record at least one exercised phase"), + Self::EmptyCorpus => f.write_str("a receipt must cover at least one corpus file"), + } + } +} + +impl std::error::Error for ReceiptError {} + +/// What the two parsers were run over, and what they were. +/// +/// Every field is an identity rather than a path, so a receipt names what was +/// compared in terms a later reader can re-derive. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Comparison { + /// The authoritative parser's artifact identity. + pub authority: String, + /// The shadow parser's artifact identity. + pub shadow: String, + /// The corpus's source-tree identity. + pub corpus: String, + /// How many sources the comparison covered. + pub corpus_files: usize, + /// The syntax hash the authoritative parser produced. + pub syntax_hash_authority: String, + /// The syntax hash the shadow parser produced. Equal to the authority's on an + /// agreeing run; recorded separately so a divergent run says where it split. + pub syntax_hash_shadow: String, + /// The Core hash the compared syntax elaborated to, which is the downstream + /// consequence a parser change would show up in. + pub core_hash: String, + /// How many sources the two parsers disagreed on. Zero is the agreeing case. + pub divergences: usize, +} + +/// A shadow-run comparison receipt: its deterministic half. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ShadowReceipt { + /// The comparison's own identity, derived from what was compared. Both store + /// layers key on it. + pub subject: Digest, + /// The hash scheme the receipt's identity is under; a scheme bump retires it. + pub scheme: String, + /// The attesting compiler version. + pub compiler: String, + /// What was compared. + pub comparison: Comparison, + /// The deepest traversal any phase reached during the run. + pub max_depth: u64, + /// Per-phase invocations and structural work, keyed by phase label. + pub phases: BTreeMap, +} + +/// One phase's contribution to a receipt: the deterministic part of a +/// [`PhaseTally`], with the wall time left behind. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct PhaseWork { + /// How many times the phase ran. + pub invocations: usize, + /// Core nodes the phase entered. + pub visits: u64, + /// Core nodes the phase reconstructed. + pub rebuilt: u64, +} + +impl ShadowReceipt { + /// Build a receipt from a comparison and the phase tallies the run produced. + /// + /// Phases with no recorded structural work are dropped rather than attested: + /// the front end works on the AST and never charges the Core counters, so + /// including it would put a row of zeros next to rows that mean something. + /// A phase offered explicitly through `exercised` must have work, and does + /// not silently drop. + /// + /// # Errors + /// An `exercised` phase with no Core visits, an empty corpus, or a run that + /// exercised nothing. + pub fn new( + comparison: Comparison, + tallies: &BTreeMap<&'static str, PhaseTally>, + exercised: &[&str], + ) -> Result { + if comparison.corpus_files == 0 { + return Err(ReceiptError::EmptyCorpus); + } + for phase in exercised { + let visits = tallies.get(*phase).map_or(0, |t| t.work.visits); + if visits == 0 { + return Err(ReceiptError::VacuousPhase((*phase).to_string())); + } + } + let phases: BTreeMap = tallies + .iter() + .filter(|(_, tally)| !tally.work.is_silent()) + .map(|(phase, tally)| { + ( + (*phase).to_string(), + PhaseWork { + invocations: tally.invocations, + visits: tally.work.visits, + rebuilt: tally.work.rebuilt, + }, + ) + }) + .collect(); + if phases.is_empty() { + return Err(ReceiptError::NoPhases); + } + let max_depth = tallies + .values() + .map(|t| t.work.max_depth) + .max() + .unwrap_or(0); + Ok(Self { + subject: Digest::from(subject_of(&comparison)), + scheme: HASH_SCHEME.to_string(), + compiler: COMPILER_VERSION.to_string(), + comparison, + max_depth, + phases, + }) + } + + /// Whether the two parsers agreed on every source. + #[must_use] + pub const fn agreed(&self) -> bool { + self.comparison.divergences == 0 + } + + // The evidence rows, in the order the envelope carries them. Sorted within the + // per-phase family by the map's own order, so two runs of the same comparison + // emit the same bytes. + fn rows(&self) -> Vec { + let c = &self.comparison; + let diagnostics = if self.agreed() { + DIAGNOSTICS_AGREED.to_string() + } else { + format!("{DIAGNOSTICS_DIVERGED_PREFIX}{}", c.divergences) + }; + let mut rows = vec![ + row(ROW_AUTHORITY, &c.authority), + row(ROW_SHADOW, &c.shadow), + row(ROW_CORPUS, &c.corpus), + row(ROW_CORPUS_FILES, &c.corpus_files.to_string()), + row(ROW_SYNTAX_HASH_AUTHORITY, &c.syntax_hash_authority), + row(ROW_SYNTAX_HASH_SHADOW, &c.syntax_hash_shadow), + row(ROW_DIAGNOSTICS, &diagnostics), + row(ROW_CORE_HASH, &c.core_hash), + row(ROW_MAX_DEPTH, &self.max_depth.to_string()), + ]; + for (phase, work) in &self.phases { + rows.push(row( + &phase_key(phase, PHASE_INVOCATIONS), + &work.invocations.to_string(), + )); + rows.push(row( + &phase_key(phase, PHASE_VISITS), + &work.visits.to_string(), + )); + rows.push(row( + &phase_key(phase, PHASE_REBUILT), + &work.rebuilt.to_string(), + )); + } + rows + } +} + +fn row(key: &str, value: &str) -> CertRow { + CertRow { + key: key.to_string(), + value: value.to_string(), + } +} + +fn phase_key(phase: &str, field: &str) -> String { + format!("{PHASE_PREFIX}{phase}.{field}") +} + +// The comparison's identity: a hash over exactly the facts that decide whether +// two runs are the same comparison. Deliberately not over the whole receipt, +// which carries the counters: a counter change with the same inputs must collide +// with the stored certificate and be reported, not quietly land under a new +// subject where nobody would look for it. +fn subject_of(c: &Comparison) -> String { + let mut h = blake3::Hasher::new(); + for field in [ + &c.authority, + &c.shadow, + &c.corpus, + &c.syntax_hash_authority, + &c.syntax_hash_shadow, + &c.core_hash, + ] { + h.update(&(field.len() as u64).to_le_bytes()); + h.update(field.as_bytes()); + } + // Full hex, not the display prefix: this is an identity two store layers key + // on, and a truncation that reads well in a terminal is not a reason to lose + // collision resistance in a directory name. + h.finalize().to_hex().to_string() +} + +/// Serialize a receipt to its `cert`-kind envelope. The bytes are its identity. +#[must_use] +pub fn encode(receipt: &ShadowReceipt) -> Vec { + encode_row_body( + &receipt.subject, + CLAIM_SHADOW_PARSE_AGREED, + &receipt.scheme, + &receipt.compiler, + &receipt.rows(), + ) +} + +/// Decode a receipt's `cert`-kind envelope. +/// +/// # Errors +/// A foreign scheme, a non-cert kind, a claim from another family, a truncated or +/// oversized field, an over-count of rows, trailing bytes, or a row whose value +/// does not parse as the number its key promises. +pub fn decode(bytes: &[u8]) -> Result { + let body = decode_row_body(bytes)?; + if body.claim != CLAIM_SHADOW_PARSE_AGREED { + return Err(CodecError::Kind); + } + let mut fields: BTreeMap<&str, &str> = BTreeMap::new(); + let mut phases: BTreeMap = BTreeMap::new(); + for r in &body.rows { + if let Some(rest) = r.key.strip_prefix(PHASE_PREFIX) { + let (phase, field) = rest.rsplit_once('.').ok_or(CodecError::Malformed)?; + let entry = phases.entry(phase.to_string()).or_default(); + match field { + PHASE_INVOCATIONS => entry.invocations = parse_num(&r.value)?, + PHASE_VISITS => entry.visits = parse_num(&r.value)?, + PHASE_REBUILT => entry.rebuilt = parse_num(&r.value)?, + _ => return Err(CodecError::Malformed), + } + } else { + fields.insert(r.key.as_str(), r.value.as_str()); + } + } + let take = |key: &str| fields.get(key).copied().unwrap_or_default().to_string(); + let diagnostics = take(ROW_DIAGNOSTICS); + let divergences = match diagnostics.strip_prefix(DIAGNOSTICS_DIVERGED_PREFIX) { + Some(n) => parse_num(n)?, + None if diagnostics == DIAGNOSTICS_AGREED => 0, + None => return Err(CodecError::Malformed), + }; + Ok(ShadowReceipt { + subject: Digest::from(body.subject), + scheme: body.scheme, + compiler: body.compiler, + comparison: Comparison { + authority: take(ROW_AUTHORITY), + shadow: take(ROW_SHADOW), + corpus: take(ROW_CORPUS), + corpus_files: parse_num(&take(ROW_CORPUS_FILES))?, + syntax_hash_authority: take(ROW_SYNTAX_HASH_AUTHORITY), + syntax_hash_shadow: take(ROW_SYNTAX_HASH_SHADOW), + core_hash: take(ROW_CORE_HASH), + divergences, + }, + max_depth: parse_num(&take(ROW_MAX_DEPTH))?, + phases, + }) +} + +// A row value that promises to be a number. Hostile bytes get a codec error, not +// a panic and not a silent zero. +fn parse_num(s: &str) -> Result { + s.parse().map_err(|_| CodecError::Malformed) +} + +/// Write a receipt's deterministic half into the store, keyed by its subject. +/// +/// Idempotent by construction: an unchanged comparison re-emits the same bytes +/// and answers [`Written::Hit`]. A comparison whose inputs match but whose +/// counters moved is a byte mismatch, which the certificate layer reports as +/// corruption. That is the intended alarm and not a bug to work around: the same +/// parser over the same corpus doing a different amount of work is exactly the +/// event the receipt exists to catch. +/// +/// # Errors +/// A filesystem error, or a byte mismatch against a receipt already stored for +/// the subject. +pub fn emit(store: &Store, receipt: &ShadowReceipt) -> io::Result { + store.put_cert(&receipt.subject, &encode(receipt)) +} + +/// Read the receipt stored for a comparison subject, if any. +/// +/// # Errors +/// A filesystem error. +pub fn get(store: &Store, subject: &str) -> io::Result>> { + Ok(store.get_cert(subject)?.map(|bytes| decode(&bytes))) +} + +/// Check the receipt stored for a subject. +/// +/// The same discipline as the parity and lineage readers: a decode failure or a +/// foreign scheme is a named failure, an absent receipt is not a failure, and a +/// recorded divergence is reported as unverifiable rather than dressed up as a +/// pass. +#[must_use] +pub fn check(store: &Store, subject: &str) -> CertStatus { + let receipt = match get(store, subject) { + Ok(Some(Ok(r))) => r, + Ok(Some(Err(e))) => return CertStatus::Failed(format!("corrupt shadow receipt ({e})")), + Ok(None) => return CertStatus::Absent, + Err(e) => return CertStatus::Failed(format!("shadow receipt unreadable: {e}")), + }; + if receipt.subject.as_str() != subject { + return CertStatus::Failed(format!( + "shadow receipt vouches for {}, not the requested {subject}", + receipt.subject + )); + } + if receipt.scheme != HASH_SCHEME { + return CertStatus::Failed(format!( + "shadow receipt made under foreign scheme {:?}; this build speaks {HASH_SCHEME:?}", + receipt.scheme + )); + } + if !receipt.agreed() { + return CertStatus::Unverifiable(format!( + "{CLAIM_SHADOW_PARSE_AGREED_NAME} recorded {} divergence(s) over {} file(s)", + receipt.comparison.divergences, receipt.comparison.corpus_files + )); + } + CertStatus::Verified(format!( + "{CLAIM_SHADOW_PARSE_AGREED_NAME}@{} by {} over {} file(s)", + receipt.scheme, receipt.compiler, receipt.comparison.corpus_files + )) +} + +/// Record a run's per-phase wall times under the comparison's subject. +/// +/// The last-write-wins half. Two correct runs of the same comparison disagree +/// here and that is not an error, which is precisely why these readings are kept +/// out of the certificate. +/// +/// # Errors +/// A filesystem error, or a subject the decision layer rejects as a locator. +pub fn put_timing( + store: &Store, + subject: &str, + tallies: &BTreeMap<&'static str, PhaseTally>, +) -> io::Result<()> { + let mut out = format!("{TIMING_FORMAT}\n"); + for (phase, tally) in tallies { + // Nanoseconds as an integer, not the row's rounded milliseconds: the + // stderr row is a display and may round, a stored reading has no reason + // to lose precision on its way through a file. + let _ = writeln!( + out, + "{phase}\t{}\t{}", + tally.invocations, + tally.wall.as_nanos() + ); + } + store.put_decision(TIMING_KIND, subject, out.as_bytes()) +} + +/// Read back a run's recorded per-phase wall times, as `(phase, invocations, +/// wall)` triples in phase-label order. +/// +/// # Errors +/// A filesystem error, a subject the decision layer rejects, or bytes in an +/// unknown format. +pub fn get_timing(store: &Store, subject: &str) -> io::Result> { + let Some(bytes) = store.get_decision(TIMING_KIND, subject)? else { + return Ok(Vec::new()); + }; + let text = String::from_utf8(bytes) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; + let mut lines = text.lines(); + if lines.next() != Some(TIMING_FORMAT) { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "shadow timing decision has an unknown format", + )); + } + let mut out = Vec::new(); + for line in lines.filter(|l| !l.is_empty()) { + let mut parts = line.split('\t'); + let bad = || io::Error::new(io::ErrorKind::InvalidData, "malformed shadow timing row"); + let phase = parts.next().ok_or_else(bad)?.to_string(); + let invocations = parts.next().ok_or_else(bad)?.parse().map_err(|_| bad())?; + let nanos: u128 = parts.next().ok_or_else(bad)?.parse().map_err(|_| bad())?; + let nanos = u64::try_from(nanos).map_err(|_| bad())?; + out.push((phase, invocations, Duration::from_nanos(nanos))); + } + Ok(out) +} diff --git a/src/store/verify.rs b/src/store/verify.rs index 00d6f691..99f3221e 100644 --- a/src/store/verify.rs +++ b/src/store/verify.rs @@ -10,8 +10,10 @@ //! incremental pair backstops: equal hash implies an already-verified artifact //! only under the identity that produced the verdict. //! -//! This is the logic over the raw record I/O ([`Store::put_verified`](crate::store::disk::Store::put_verified) / -//! [`Store::verified`](crate::store::disk::Store::verified)): the canonical `check-kind` names in one place, and the +//! This is the logic over the raw record I/O +//! ([`Store::put_verified`](crate::store::disk::Store::put_verified) and +//! [`Store::verified`](crate::store::disk::Store::verified)): canonical +//! `check-kind` names in one place, and the //! "does a passing record exist under the hash scheme in force" query the raw //! `Vec` does not answer. Only passes are recorded, mirroring //! the record layer's design: a failure is the absence of a pass, so a failing diff --git a/src/syntax/desugar/derive.rs b/src/syntax/desugar/derive.rs index 7b334f64..0d617a05 100644 --- a/src/syntax/desugar/derive.rs +++ b/src/syntax/desugar/derive.rs @@ -112,7 +112,7 @@ pub(super) fn derive_instances( function.name.clone(), ) })); - // Library constructors too, not just functions: a derived JSON encoder names + // Include library constructors: a derived JSON encoder names // `JObj`/`JStr`, which are constructors of an opt-in module. In module mode // `external_values` already carries them; in whole-program mode every library // declaration is merged into `prog.types`, so they must be picked up here or @@ -139,7 +139,7 @@ pub(super) fn derive_instances( // `deriving (Lens)` synthesizes the accessor pair alone. let mk_lens = value_canon.get(names::OPTIC_MK_LENS).cloned(); // Every data declaration in the merged program, by canonical name. A derived - // `Plate` reads the whole set, not just the derived type: seeing through the + // `Plate` reads the whole set because seeing through the // carrier records an AST holds its nodes in (a match arm, a spanned wrapper) // is the difference between one derived traversal and one hand-written match // per carrier. @@ -882,8 +882,8 @@ fn derive_serialize(d: &DataDecl, class: &str, lib: &impl Fn(&str) -> String) -> // declared field names; a positional one's are its argument positions (`_0`, // `_1`), which is the only name the declaration offers. A sum additionally // carries its constructor's bare name under `$`, a key no field name can spell, -// so a document names the variant it holds rather than an index that quietly -// changes meaning when a constructor is inserted. A single-constructor type has +// so a document names the variant it holds rather than an unstable numeric index. +// A single-constructor type has // nothing to discriminate and carries no tag. // // Order is the declaration's throughout, so one value has one tree; the canonical @@ -1295,42 +1295,18 @@ fn derive_arbitrary(d: &DataDecl, class: &str, lib: &impl Fn(&str) -> String) -> ) } -// `deriving (Plate)` writes, once, the match that every hand-written "children of -// this node" traversal writes by hand: `children(x)` is the list of `x`'s -// immediate subvalues *of the derived type itself*, in constructor-declaration -// order and then field order. That order is a function of the declaration alone, -// so the derived Core is the same every run, and every whole-tree traversal -// (universe, fold, count, rewrite) is written once against `children` rather than -// once per constructor. +// `deriving (Plate)` emits `children` and `rebuild` over immediate subvalues of +// the derived type. Child order follows constructor and field declaration order. // -// Whether a field contributes is decided structurally, and it cannot be decided -// from the field type alone: `List(Arm)` never names the derived type, yet an -// `Arm` may hold one. So the derivation takes every field apart as far as it can -// (the derived type itself is a child; a list, optional, or tuple yields its -// components; a data type declared in this program yields its own fields), then -// takes the least fixpoint of "holds a child" over the resulting graph. That is -// what lets the traversal see through the carrier records an AST holds its nodes -// in (a match arm, a spanned wrapper, a qualifier) without anyone writing a -// second match for them. Each distinct shape becomes one accumulator-threading -// helper, shared by every field that walks it, so the emitted code is linear in -// the shapes rather than in the fields. +// A least fixpoint over declared field shapes finds nested children through +// lists, options, tuples, and user data types. Each distinct shape shares one +// accumulator-threading helper. // -// Anything the derivation cannot look inside is rejected rather than silently -// skipped: a traversal that quietly drops a subterm is a wrong answer, not a -// smaller one. That covers function types, containers with no declaration in this -// program, and a recursive occurrence at different type arguments (whose children -// would not have the type the instance head promises). "Could it have held a -// child" is asked of the whole reachable set, not of the printed type, so an -// opaque container is rejected when it is applied to anything that leads back to -// the derived type, not only when it spells that type out. +// Reject opaque shapes that could contain a child, function types, and +// non-regular recursive occurrences. // -// `rebuild` is the same walk read backwards, and the two are derived from one -// shape graph so they cannot drift: every position `children` yields is a -// position `rebuild` fills, in the same order. Each shape gets a second helper -// taking the replacements it needs off the front of the list and handing back -// what is left, exactly as the wire codec's decoder threads its remaining bytes, -// so a replacement list of the wrong length is a `Fail` at the end rather than a -// silently padded or truncated value. +// `rebuild` consumes replacements in the same shape order. A length mismatch +// performs `Fail`. // The number of distinct field shapes one derivation may walk into. Only a // non-regular recursion (a `T(a)` reached through `C(T(List(a)))`) can expand @@ -1394,9 +1370,8 @@ struct Plate<'a> { // The declared types from which the target is reachable: the target itself, and // any type one of whose fields names a type already known to reach it. Purely -// syntactic and therefore conservative, which is the direction that fails -// closed: it is the guard on shapes the traversal cannot take apart, where the -// question is not "does this hold a child" but "could it". +// syntactic and conservative. It guards shapes the traversal cannot inspect by +// asking whether each shape could hold a child. fn reaching_set(target: &str, decls: &BTreeMap<&str, &DataDecl>) -> BTreeSet { let mut set = BTreeSet::from([target.to_string()]); let mut grew = true; diff --git a/src/syntax/desugar/effects/escape.rs b/src/syntax/desugar/effects/escape.rs index c0dce428..a6f832e5 100644 --- a/src/syntax/desugar/effects/escape.rs +++ b/src/syntax/desugar/effects/escape.rs @@ -265,10 +265,9 @@ fn token_free_in(e: &S>, token: &str) -> bool { // perform them; here the token is a first-class value, so a bare `Var(token)` in // value position is itself the escape, and a lambda whose body uses the token // free is a carrier (the closure could outlive the call). Shares `escapes`'s -// documented hole: a non-constructor call result is opaque, so a callee that -// smuggles its argument back out is not caught; this is a focused early -// diagnostic for the directly expressible escapes (returned, embedded in data, -// aliased, captured), not a full soundness boundary. +// documented hole: a non-constructor call result is opaque, so a callee can +// smuggle its argument back out. This early diagnostic covers directly +// expressible escapes (returned, embedded in data, aliased, or captured). pub(in crate::syntax::desugar) fn token_escapes( e: &S>, token: &str, diff --git a/src/syntax/desugar/ids.rs b/src/syntax/desugar/ids.rs index 54f5647c..86f797b1 100644 --- a/src/syntax/desugar/ids.rs +++ b/src/syntax/desugar/ids.rs @@ -1,6 +1,6 @@ //! Stamp every expression node, and every pattern a match arm binds, with a -//! unique `NodeId` — the identity under which the typechecker records a node's -//! resolution for the elaborator to read back. +//! unique `NodeId`. The typechecker records each node's resolution under this +//! identity for the elaborator to read back. //! //! Run as the last step of desugar, so identity is fixed on the exact tree both //! the typechecker and the elaborator traverse. Decoupling identity from `Span` diff --git a/src/tc/classes.rs b/src/tc/classes.rs index 27e9ad89..43d35bc5 100644 --- a/src/tc/classes.rs +++ b/src/tc/classes.rs @@ -4,7 +4,7 @@ use marginalia::Span; use super::env::{collect_row_vars, collect_type_vars, convert_data, wrap_forall}; use super::{ - Canon, ClassInfo, CtorInfo, DataInfo, Dict, Env, HeadKey, InstInfo, InstKeys, Tc, + Canon, ClassInfo, CtorInfo, DataInfo, Dict, Env, HeadKey, InstInfo, InstKeys, NominalRepr, Tc, TypecheckSeed, Wanted, Warning, WarningOrigin, }; use crate::error::suggest; @@ -578,6 +578,7 @@ pub(super) fn build_classes( params: vec![c.param.clone()], param_kinds: vec![Kind::Type], ctors: vec![dname.clone()], + repr: NominalRepr::BoxedCell, }, ); ctors.insert( diff --git a/src/tc/context.rs b/src/tc/context.rs index 4a400bca..9624d266 100644 --- a/src/tc/context.rs +++ b/src/tc/context.rs @@ -83,19 +83,25 @@ impl Tc<'_> { }) } - pub(super) fn solve_row(&mut self, v: u32, r: EffRow) -> Result<(), TcErr> { - // Absence of the row existential is a row-scope escape, not an internal - // fault. Unlike the type context, the row context does not keep every - // solution strictly left-referencing, so a later truncation can strand a - // row variable that a unification still references. That is a real (if - // rare) typing failure of a user program, so surface it as a user - // diagnostic rather than a compiler ICE. `Keep` so this precise reason - // survives a caller's coarse expected/got rewrite. - let i = self - .ctx - .iter() - .position(|e| matches!(e, Entry::ExRow(w) | Entry::SolvedRow(w, _) if *w == v)) + pub(super) fn solve_row(&mut self, v: u32, r: &EffRow) -> Result<(), TcErr> { + if self.solved_row(v).is_some() { + return Err(TcErr::Ice(format!("solve_row: ^{v} is already solved"))); + } + let owner = self + .index_unsolved_ex_row(v) .ok_or_else(|| TcErr::Keep(ROW_ESCAPES_SCOPE.into()))?; + + // A row solution can carry both kinds of existential in its label + // arguments. Apply existing solutions before deciding which variables + // cross the owner's level; otherwise an alias can hide the actual young + // variable until after its marker has been dropped. + let mut r = self.apply_row(r); + let row_ty = Type::Row(r.clone()); + let mut row_exs = BTreeSet::new(); + row_ty.free_exist_row(&mut row_exs); + if row_exs.contains(&v) { + return Err(TcErr::Fail("recursive effect row".into())); + } if let Some(sk) = self.row_skolem_escaping(v, &r) { // A user program reaches this: a closure created outside a // row-polymorphic boundary whose effects can only be satisfied by @@ -106,6 +112,89 @@ impl Tc<'_> { r.show() ))); } + if let Some(sk) = self.type_skolem_escaping_in_row(v, &r) { + return Err(TcErr::Keep(format!( + "effect row `{}` would capture the rigid type `{sk}`: `{sk}` is bound by an inner `forall`, and a row introduced outside that `forall` cannot depend on it", + r.show() + ))); + } + + // Lower every flexible variable introduced to the owner's right to one + // same-kind representative immediately to its left. The young variable + // is solved toward that representative while its marker is still live; + // the surviving row then mentions only entries that survive with it. + // One representative per original id preserves sharing across repeated + // and nested label arguments. + let mut type_exs = BTreeSet::new(); + row_ty.free_exist(&mut type_exs); + let mut young_types = Vec::new(); + for old in type_exs { + let at = self.index_ex(old).ok_or_else(|| { + TcErr::Ice(format!( + "solve_row: proposed solution for ^{v} references escaped type existential ^{old}" + )) + })?; + if at > owner { + young_types.push(old); + } + } + let mut young_rows = Vec::new(); + for old in row_exs { + let at = self.index_ex_row(old).ok_or_else(|| { + TcErr::Ice(format!( + "solve_row: proposed solution for ^{v} references escaped row existential ^{old}" + )) + })?; + if at > owner { + young_rows.push(old); + } + } + + let mut type_proxies = Vec::with_capacity(young_types.len()); + for old in young_types { + type_proxies.push((old, self.fresh_id())); + } + let mut row_proxies = Vec::with_capacity(young_rows.len()); + for old in young_rows { + let new = self.fresh_id(); + // The proxy takes over the young variable's identity, so any side + // classification keyed by id must follow it: a tooltip scaffold tail + // rebased here is still a scaffold, not a genuinely open row. + if self.tooltip_row_scaffolds.contains(&old) { + self.tooltip_row_scaffolds.insert(new); + } + row_proxies.push((old, new)); + } + if !type_proxies.is_empty() || !row_proxies.is_empty() { + let mut entries = Vec::with_capacity(type_proxies.len() + row_proxies.len()); + entries.extend(type_proxies.iter().map(|(_, new)| Entry::Ex(*new))); + entries.extend(row_proxies.iter().map(|(_, new)| Entry::ExRow(*new))); + self.ctx.splice(owner..owner, entries); + + for (old, new) in &type_proxies { + r = r.map_args(&|arg| arg.subst_exist(*old, &Type::Exist(*new))); + } + for (old, new) in &row_proxies { + r = r.subst_row_exist(*old, &EffRow::Exist(*new)); + } + + for (old, new) in type_proxies { + self.equate(&Type::Exist(old), &Type::Exist(new))?; + } + for (old, new) in row_proxies { + self.unify_row(&EffRow::Exist(old), &EffRow::Exist(new))?; + } + } + + if !self.well_formed_row_before(v, &r) { + return Err(TcErr::Ice(format!( + "solve_row: solution `{}` for ^{v} references a forward or out-of-scope variable", + r.show() + ))); + } + let i = self + .index_unsolved_ex_row(v) + .ok_or_else(|| TcErr::Ice(format!("solve_row: ^{v} escaped during rebasing")))?; self.ctx[i] = Entry::SolvedRow(v, r); Ok(()) } @@ -120,15 +209,14 @@ impl Tc<'_> { &mut self, a: u32, new_rows: &[u32], - solved: EffRow, + solved: &EffRow, ) -> Result<(), TcErr> { let pos = self - .index_ex_row(a) + .index_unsolved_ex_row(a) .ok_or_else(|| TcErr::Ice(format!("splice_solved_row: ^{a} not in context")))?; - let mut repl: Vec = new_rows.iter().map(|r| Entry::ExRow(*r)).collect(); - repl.push(Entry::SolvedRow(a, solved)); - self.ctx.splice(pos..=pos, repl); - Ok(()) + let repl = new_rows.iter().map(|r| Entry::ExRow(*r)); + self.ctx.splice(pos..pos, repl); + self.solve_row(a, solved) } pub(super) fn apply_row(&self, r: &EffRow) -> EffRow { @@ -159,6 +247,18 @@ impl Tc<'_> { .position(|e| matches!(e, Entry::ExRow(w) | Entry::SolvedRow(w, _) if *w == v)) } + fn index_unsolved_ex(&self, v: u32) -> Option { + self.ctx + .iter() + .position(|e| matches!(e, Entry::Ex(w) if *w == v)) + } + + fn index_unsolved_ex_row(&self, v: u32) -> Option { + self.ctx + .iter() + .position(|e| matches!(e, Entry::ExRow(w) if *w == v)) + } + // Position of a rigid type-variable (skolem) in the context, the `Uni` // analogue of `index_ex`. Leftmost, matching `drop_uni`'s truncation point, // so the scope test agrees with the entry that actually gets dropped. @@ -175,77 +275,109 @@ impl Tc<'_> { .position(|e| matches!(e, Entry::RowUni(w) if *w == n)) } - fn solved(&self, v: u32) -> Option { + pub(super) fn solved(&self, v: u32) -> Option { self.ctx.iter().find_map(|e| match e { Entry::Solved(w, t) if *w == v => Some(t.clone()), _ => None, }) } - pub(super) fn solve(&mut self, v: u32, t: Type) { - if let Some(i) = self.index_ex(v) { - // Scope guard at the origin: the solution may only reference entries - // to the left of `v`, so truncation never strands a referenced var - // and the downstream `index_ex` lookups can never miss. A forward or - // out-of-scope reference here is a compiler bug, caught at its cause. - debug_assert!( - self.well_formed_before(v, &t), - "solve: solution references a forward or out-of-scope variable" - ); - self.ctx[i] = Entry::Solved(v, t); + pub(super) fn solve(&mut self, v: u32, t: Type) -> Result<(), TcErr> { + if self.solved(v).is_some() { + return Err(TcErr::Ice(format!("solve: ^{v} is already solved"))); } + let i = self + .index_unsolved_ex(v) + .ok_or_else(|| TcErr::Ice(format!("solve: ^{v} escaped scope")))?; + // Scope guard at the origin: the solution may only reference entries + // to the left of `v`, so truncation never strands a referenced var and + // downstream lookups cannot miss. This is required for sound output in + // release builds, not a debug-only developer check. + if !self.well_formed_before(v, &t) { + return Err(TcErr::Ice(format!( + "solve: solution `{}` for ^{v} references a forward or out-of-scope variable", + t.show() + ))); + } + self.ctx[i] = Entry::Solved(v, t); + Ok(()) } - // Truncating to `i` drops every entry in `ctx[i..]`. `solve` keeps every type - // solution strictly left-referencing (the `well_formed_before` guard), so a - // surviving solution (in `ctx[..i]`) never names a dropped *type existential*; - // this asserts that at the boundary, the compiler bug the downstream `index_ex` - // `expect`s guard against. Existentials carry globally-unique fresh ids, so the - // disjointness test is exact. + // Truncating to `i` drops every entry in `ctx[i..]`. Type and row solution + // installers keep both kinds of existential strictly left-facing; audit + // that invariant at the scope boundary. Existentials carry globally unique + // fresh ids, so the disjointness tests are exact. // - // Skolems (`Uni`/`RowUni`) are deliberately not asserted here, for the same - // reason row existentials are not: the check would have no sound formulation at - // this boundary. A skolem is pushed under its raw forall-bound name (see the + // Skolems (`Uni`/`RowUni`) are deliberately not asserted here. A skolem is + // pushed under its raw forall-bound name (see the // `Forall`/`RowForall` arms of `subtype`/`inst`), not a fresh one, so skolem // names are not globally unique. An *ambient* rigid variable a surviving // solution legitimately references (a class parameter, an outer signature's // `forall`) can share a `Sym` with an unrelated in-context skolem being dropped, // and a name-based disjointness test cannot tell the two apart, so it false- // positives. Skolem escape is prevented soundly at its origin instead: - // `well_formed_before` checks *index positions* at solve time, while scopes are - // correctly nested, so a re-check on names at the drop boundary adds no coverage - // the origin guard lacks. Compiled out of release builds. - fn assert_no_escape(&self, i: usize) { - if !cfg!(debug_assertions) { - return; - } + // `well_formed_before`/`well_formed_row_before` check *index positions* at + // solve time, while scopes are correctly nested, so a re-check on names at + // the drop boundary adds no coverage the origin guard lacks. + fn scope_escape(&self, i: usize) -> Option<&'static str> { let mut dropped_ex = BTreeSet::new(); + let mut dropped_rows = BTreeSet::new(); for e in &self.ctx[i..] { - if let Entry::Ex(w) | Entry::Solved(w, _) = e { - dropped_ex.insert(*w); + match e { + Entry::Ex(w) | Entry::Solved(w, _) => { + dropped_ex.insert(*w); + } + Entry::ExRow(w) | Entry::SolvedRow(w, _) => { + dropped_rows.insert(*w); + } + Entry::Uni(_) | Entry::RowUni(_) | Entry::Marker(_) => {} } } for e in &self.ctx[..i] { - if let Entry::Solved(_, t) = e { - let mut ex = BTreeSet::new(); - t.free_exist(&mut ex); - debug_assert!( - ex.is_disjoint(&dropped_ex), - "context truncation strands a type existential referenced by a surviving solution" - ); + let ty = match e { + Entry::Solved(_, ty) => Some(ty.clone()), + Entry::SolvedRow(_, row) => Some(Type::Row(row.clone())), + _ => None, + }; + if let Some(ty) = ty { + let mut exs = BTreeSet::new(); + ty.free_exist(&mut exs); + if !exs.is_disjoint(&dropped_ex) { + return Some( + "context truncation strands a type existential referenced by a surviving solution", + ); + } + let mut rows = BTreeSet::new(); + ty.free_exist_row(&mut rows); + if !rows.is_disjoint(&dropped_rows) { + return Some( + "context truncation strands a row existential referenced by a surviving solution", + ); + } } } + None } - pub(super) fn drop_marker(&mut self, m: u32) { + fn assert_no_escape(&self, i: usize) { + if cfg!(debug_assertions) { + let escaped = self.scope_escape(i); + debug_assert!(escaped.is_none(), "scope escape: {escaped:?}"); + } + } + + pub(super) fn drop_marker(&mut self, m: u32) -> Result<(), TcErr> { if let Some(i) = self .ctx .iter() .position(|e| matches!(e, Entry::Marker(w) if *w == m)) { - self.assert_no_escape(i); + if let Some(msg) = self.scope_escape(i) { + return Err(TcErr::Ice(msg.into())); + } self.ctx.truncate(i); } + Ok(()) } pub(super) fn drop_uni(&mut self, n: Sym) { @@ -309,7 +441,7 @@ impl Tc<'_> { // A candidate solution for existential `a` is well-scoped only if every free // variable it names is bound to `a`'s left, so a later truncation that drops // `a`'s right neighbours never strands a reference. The guard closes the whole - // variable class, not just existentials: a `Uni`/`RowUni` skolem introduced + // variable class, including skolems: a `Uni`/`RowUni` introduced // under an inner `forall` sits to `a`'s right, so solving an outer `a` to it // would let the skolem escape its quantifier (the fast path in `inst` trusts // exactly this predicate). An existential must be in the context (its absence @@ -324,12 +456,46 @@ impl Tc<'_> { }; let mut exs = BTreeSet::new(); t.free_exist(&mut exs); + let mut row_exs = BTreeSet::new(); + t.free_exist_row(&mut row_exs); let mut uvars = BTreeSet::new(); t.free_ty_vars(&mut uvars); let mut rvars = BTreeSet::new(); t.free_row_vars(&mut rvars); exs.iter() .all(|e| self.index_ex(*e).is_some_and(|i| i < ai)) + && row_exs + .iter() + .all(|e| self.index_ex_row(*e).is_some_and(|i| i < ai)) + && uvars + .iter() + .all(|n| self.index_uni(*n).is_none_or(|i| i < ai)) + && rvars + .iter() + .all(|n| self.index_row_uni(*n).is_none_or(|i| i < ai)) + } + + // The row-solution dual of `well_formed_before`. A row can carry type and + // row variables recursively inside label arguments, so all four variable + // classes are checked against the row existential's position. + pub(super) fn well_formed_row_before(&self, a: u32, r: &EffRow) -> bool { + let Some(ai) = self.index_ex_row(a) else { + return false; + }; + let row_ty = Type::Row(r.clone()); + let mut exs = BTreeSet::new(); + row_ty.free_exist(&mut exs); + let mut row_exs = BTreeSet::new(); + row_ty.free_exist_row(&mut row_exs); + let mut uvars = BTreeSet::new(); + row_ty.free_ty_vars(&mut uvars); + let mut rvars = BTreeSet::new(); + row_ty.free_row_vars(&mut rvars); + exs.iter() + .all(|e| self.index_ex(*e).is_some_and(|i| i < ai)) + && row_exs + .iter() + .all(|e| self.index_ex_row(*e).is_some_and(|i| i < ai)) && uvars .iter() .all(|n| self.index_uni(*n).is_none_or(|i| i < ai)) @@ -354,6 +520,15 @@ impl Tc<'_> { .find(|n| self.index_row_uni(*n).is_some_and(|i| i >= ai)) } + fn type_skolem_escaping_in_row(&self, a: u32, r: &EffRow) -> Option { + let ai = self.index_ex_row(a)?; + let row_ty = Type::Row(r.clone()); + let mut vars = BTreeSet::new(); + row_ty.free_ty_vars(&mut vars); + vars.into_iter() + .find(|n| self.index_uni(*n).is_some_and(|i| i >= ai)) + } + pub(super) fn articulate( &mut self, a: u32, @@ -371,7 +546,7 @@ impl Tc<'_> { // It surfaces as a structured ICE rather than a raw panic, matching the // rest of the context/row machinery. let pos = self - .index_ex(a) + .index_unsolved_ex(a) .ok_or_else(|| TcErr::Ice(format!("articulate: ^{a} escaped scope")))?; let mut repl: Vec = arg_exs.iter().map(|e| Entry::Ex(*e)).collect(); repl.push(Entry::ExRow(row)); @@ -390,7 +565,7 @@ impl Tc<'_> { // Same invariant as `articulate`: a live existential is always in // context, so absence is a compiler bug surfaced as a structured ICE. let pos = self - .index_ex(a) + .index_unsolved_ex(a) .ok_or_else(|| TcErr::Ice(format!("splice_solved: ^{a} escaped scope")))?; let mut repl: Vec = new_exs.iter().map(|e| Entry::Ex(*e)).collect(); repl.push(Entry::Solved(a, solved)); @@ -398,38 +573,31 @@ impl Tc<'_> { Ok(()) } - // Generalization is unconditional: a `let` binding generalizes its inferred - // type with no value restriction, even for a syntactic non-value such as - // `let xs = array_empty()`. This is sound here and stays sound by design, - // not by accident. The polymorphic-reference hazard the value restriction - // exists to plug needs a generalizable binding that aliases a mutable cell; - // Prism has no such thing and never will. There is no ML-style `ref`: the - // only mutable binding is `var`, which desugars to a private, monomorphic - // State effect (writing two element types into one `var` is a type error), - // and `Array`/`HashMap`/`String` are copy-on-write value types with no - // shared identity, so a functional allocator can never introduce aliasing. - // A first-class polymorphic mutable reference is deliberately outside the - // language, so a value restriction would only reject sound programs. Do not - // add one (and please leave this note for the next reader who wonders). + // Articulate a type existential as a row type with a fresh row + // existential to its left. This is the mixed-kind analogue of + // `splice_solved`: the solution remains valid when a later marker closes. + pub(super) fn articulate_row_type(&mut self, a: u32, row: u32) -> Result<(), TcErr> { + let pos = self + .index_unsolved_ex(a) + .ok_or_else(|| TcErr::Ice(format!("articulate_row_type: ^{a} escaped scope")))?; + self.ctx.splice( + pos..=pos, + [ + Entry::ExRow(row), + Entry::Solved(a, Type::Row(EffRow::Exist(row))), + ], + ); + Ok(()) + } + + // Generalization is unconditional because Prism has no first-class mutable + // references. `var` lowers to a private monomorphic State effect, and the + // mutable containers are copy-on-write values without shared identity. // - // What generalization does NOT do: it never quantifies class constraints - // into a scheme. There is no surface syntax for a constraint on a `let` - // binding (only top-level `fn`s carry `given C(a)`), and a constrained - // local scheme would need a dictionary lambda at the binder plus - // dictionary application at every local use, machinery reserved for - // top-level declarations. Instead, a local binding whose body incurs a - // dictionary obligation keeps the obligated existential monomorphic: - // `generalize_local` excludes every existential still mentioned by a - // pending obligation from the quantifier set, so the obligation stays - // attached to a live existential that later use sites ground - // (`let f = \(x) -> show(x)` followed by `f(1)` resolves `Show(Int)` at - // the declaration boundary) or the enclosing declaration's `given` - // context discharges. A binding that is never grounded still surfaces - // as the standard unresolved-constraint diagnostic ("cannot infer the - // type for constraint ...", `head_key` in classes.rs), and one used at - // two different constrained types is a type mismatch; both lift to a - // top-level `fn ... given C(a)`. Quantifying constraints locally is - // intentionally not implemented. + // Local schemes do not quantify class constraints. `generalize_local` + // excludes existentials mentioned by pending obligations, allowing later use + // sites or an enclosing `given` context to ground them. Ungrounded + // obligations produce the standard unresolved-constraint diagnostic. pub(super) fn generalize(&self, env: &Env, ty: &Type) -> Type { self.generalize_map(env, ty).0 } @@ -550,7 +718,7 @@ impl Tc<'_> { // canonicalized when it is generalized, so `fn at_map(m : Map(k, v), …)` is // published as `forall a b. (Map(a, b), a) -> b` and that is what a reader // sees in the signature. Preserving `k` and `v` in the body was measured and - // made things worse — 105 inconsistent definitions became 188 — because it + // increased inconsistent definitions from 105 to 188 because it // put the body in one convention and the rendered signature in the other. let reserved: BTreeSet = seed .into_iter() diff --git a/src/tc/env.rs b/src/tc/env.rs index e5b2636e..7d0befc9 100644 --- a/src/tc/env.rs +++ b/src/tc/env.rs @@ -4,7 +4,7 @@ use std::collections::{BTreeMap, BTreeSet}; use marginalia::Span; -use super::{CtorInfo, DataInfo, EffOpInfo, Env, Tc}; +use super::{CtorInfo, DataInfo, EffOpInfo, Env, NominalRepr, Tc}; use crate::core::builtins::{Builtin, FloatOp, OUTPUT_BUILTINS}; use crate::error::suggest; use crate::error::{ErrKind, TypeError}; @@ -12,6 +12,7 @@ use crate::kw; use crate::names; use crate::sym::Sym; use crate::syntax::ast::{self, Core, Decl, Program}; +use crate::types::is_or_null_element_in; use crate::types::ty::{EffRow, Kind, Label, Type, BUF, CANONICAL, FLOAT_BUF, INT_BUF}; // Effects the compiler knows without an `effect` declaration: the IO/Exn @@ -118,7 +119,7 @@ impl Tc<'_> { } .at(span)); }; - if !or_null_ast_element_ok(elem) { + if !self.or_null_ast_element_ok(elem) { return Err(ErrKind::OrNullBadElement { found: or_null_element_desc(elem), } @@ -156,9 +157,8 @@ impl Tc<'_> { } .at(span)); }; - // A `Row`- or `Nat`-kinded position is not merely "some type of - // that kind": only a row literal or variable can be lowered as a - // row, and only a dimension literal or variable as a `Nat`. + // Row positions accept only row literals or variables. `Nat` + // positions accept only dimension literals or variables. // Anything else (an application, constructor, tuple, ...) has no // representation there and was silently erased to the empty row // or a fresh dimension before. A bare variable stays legal (its @@ -436,6 +436,39 @@ impl Tc<'_> { .collect(); EffRow::canonical(labels, base) } + + // Map the representation-relevant written forms into semantic types and + // ask the shared policy/physical authority. A newtype-shaped or opaque + // nominal is not assumed to keep a wrapper after representation lowering. + fn or_null_ast_element_ok(&self, t: &ast::Ty) -> bool { + let semantic = match t { + ast::Ty::Int => Type::Int, + ast::Ty::I64 => Type::I64, + ast::Ty::U64 => Type::U64, + ast::Ty::Bool => Type::Bool, + ast::Ty::Str => Type::Str, + ast::Ty::Tuple(_) => Type::Tuple(Vec::new()), + ast::Ty::Con(n, _) if n != kw::TY_OR_NULL => Type::Con(Sym::from(n), Vec::new()), + _ => return false, + }; + self.or_null_element_ok(&semantic) + } + + /// Whether a semantic type has source permission and a proof that its + /// runtime element is one non-zero word. + pub(super) fn or_null_element_ok(&self, ty: &Type) -> bool { + is_or_null_element_in(ty, |name| self.nominal_is_boxed(name)) + } + + // Constructor shape is not representation evidence: ordinary one-field + // datatypes allocate, while source newtypes with that shape are erased. + // Imported opaque declarations keep this explicit fact even when their + // constructors are hidden. + fn nominal_is_boxed(&self, name: Sym) -> bool { + self.data + .get(name.as_str()) + .is_some_and(|data| data.repr == NominalRepr::BoxedCell) + } } // Predicativity at the source: a type-constructor argument ranges over @@ -454,29 +487,6 @@ fn no_polytype_args(args: &[ast::Ty], head: &str, span: Span) -> Result<(), Type Ok(()) } -// The AST dual of `types::is_or_null_element`: whether `t` is a written type whose -// values occupy a single value word that is never the machine zero word. Heap -// datatypes (any `Con`/`App`/`Tuple`) and the tagged scalars qualify. `Unit` is the -// zero word, `Float`/`Char` are excluded, `OrNull` makes the null word ambiguous, -// and a bare type variable may instantiate to `Unit`, so all are rejected. Keep -// this in lockstep with the `Type`-level predicate. -fn or_null_ast_element_ok(t: &ast::Ty) -> bool { - match t { - // Tagged scalars (odd words) and heap datatypes, tuples, and applied - // datatype spines are all non-zero single words. - ast::Ty::Int - | ast::Ty::I64 - | ast::Ty::U64 - | ast::Ty::Bool - | ast::Ty::Str - | ast::Ty::Tuple(_) - | ast::Ty::App(..) => true, - // A user datatype qualifies, but a nested `OrNull` does not. - ast::Ty::Con(n, _) => n != kw::TY_OR_NULL, - _ => false, - } -} - // A short description of a rejected `OrNull` element, for the diagnostic. fn or_null_element_desc(t: &ast::Ty) -> String { match t { @@ -1043,6 +1053,7 @@ pub(super) fn build_data(prog: &Program) -> Result) -> Result) -> Result) -> Result) -> Result) -> Result) -> Result = BTreeMap::new(); - for c in &dd.ctors { - let Some(fs) = c.fields.as_ref() else { - continue; - }; - for ((n, _), arg) in fs.iter().zip(&c.args) { - let arg_ty = saturate_field(convert_data_rp(arg, &row_params), &data); - match seen.get(n) { - Some(prev) if prev != &arg_ty => { - return Err(ErrKind::ConflictingFieldType { - type_name: dd.name.clone(), - field: n.clone(), - first: prev.show(), - second: arg_ty.show(), - } - .at(dd.span)); - } - Some(_) => {} - None => { - seen.insert(n.clone(), arg_ty); - } - } - } - } } let mut eff_ops: BTreeMap = BTreeMap::new(); for eff_decl in &prog.effects { @@ -1284,8 +1272,92 @@ fn max_state_ex(t: &Type, hi: &mut Option) { #[cfg(test)] mod tests { + #[cfg(feature = "native")] + use crate::core::builtins::{AbiArg, AbiResult, Builtin}; use crate::sym::Sym; use crate::types::ty::{EffRow, Label, Type}; + #[cfg(feature = "native")] + use crate::types::{layout_of_type, RcBehavior, Repr, ZeroPossibility}; + + // The tagged-word protocol behind `AbiArg::Immediate` and + // `AbiResult::RetagImmediate`: the layout authority plans a non-pointer + // word whose payload lives above the tag bit, so the emitter may untag an + // argument with a shift and retag a bare-integer result. Bool and Char are + // immediate outright; Int qualifies through its tagged-word plan. + #[cfg(feature = "native")] + fn tagged_word_protocol(ty: &Type) -> bool { + let layout = layout_of_type(ty); + matches!( + (layout.local(), layout.zero(), layout.rc()), + (Repr::Immediate, ZeroPossibility::Never, RcBehavior::Trivial) + | ( + Repr::NonNullValue, + ZeroPossibility::Never, + RcBehavior::RuntimeWord + ) + ) + } + + // A convention table row and a signature table row describe the same call, + // so the two must agree: an argument the ABI untags must be declared with + // a tagged-word type, an argument it unboxes must be declared as one of + // the one-word payload cells (`AbiArg::BoxedFloat` is named for its first + // user but unboxes any such cell), and a retagged result must be declared + // tagged-word. A red run here is representation drift between the tables; + // fixing it changes emitted calls, so it belongs under the full gate, + // never behind a loosened assertion. + #[cfg(feature = "native")] + #[test] + fn builtin_abi_agrees_with_declared_signatures() { + for builtin in Builtin::ALL { + let Some(sig) = builtin.signature() else { + continue; + }; + let name = builtin.name(); + let (mut ty, _) = super::parse_sig(name, sig).expect("builtin signature parses"); + while let Type::Forall(_, inner) | Type::RowForall(_, inner) = ty { + ty = *inner; + } + let Type::Fun(params, _, result) = ty else { + panic!("builtin {name} signature is not a function type"); + }; + let abi = builtin.abi(); + assert!( + abi.args_within(params.len()), + "builtin {name} tags an argument its signature does not declare" + ); + for (index, param) in params.iter().enumerate() { + match abi.arg(index) { + AbiArg::Immediate => assert!( + tagged_word_protocol(param), + "builtin {name} untags argument {index}, but its declared type \ + {} is not a tagged word", + param.show() + ), + AbiArg::BoxedFloat => { + assert!( + matches!(param, Type::Float | Type::I64 | Type::U64), + "builtin {name} unboxes argument {index}, but its declared \ + type {} is not a one-word payload cell", + param.show() + ); + let layout = layout_of_type(param); + assert_eq!(layout.local(), &Repr::NonNullValue); + assert_eq!(layout.rc(), RcBehavior::Managed); + } + AbiArg::Raw => {} + } + } + if abi.result() == AbiResult::RetagImmediate { + assert!( + tagged_word_protocol(&result), + "builtin {name} retags its result, but its declared result type \ + {} is not a tagged word", + result.show() + ); + } + } + } #[test] fn builtin_signatures_parse() { diff --git a/src/tc/infer.rs b/src/tc/infer.rs index 071a7d77..39400df7 100644 --- a/src/tc/infer.rs +++ b/src/tc/infer.rs @@ -14,7 +14,6 @@ use crate::kw; use crate::names; use crate::sym::Sym; use crate::syntax::ast::{self, Core, Expr, HandlerArm, HandlerMode, NodeId, S}; -use crate::types::is_or_null_element; use crate::types::ty::{EffRow, Label, Type, LIST, NUM_CLASS, SHOW_CLASS}; use crate::wired::Indexable; @@ -60,22 +59,6 @@ impl Tc<'_> { self.operation_uses.insert(effect, operation); } - // Debug-only invariant for the operation-uses swap discipline. Every delimited - // scope opens with a `mem::take` (installing a fresh accumulator), lets its - // body accumulate, drains that accumulation with a second `mem::take`, then - // reinstalls the saved outer scope. Asserting the accumulator is drained - // immediately before each restore pins the balance: the outer scope is always - // reinstalled into an empty slot, so a restore can never silently drop the - // effect uses gathered inside (an under-approximated row). A future reorder - // that accumulated between the drain and the restore, or an orphaned take, - // would trip this. Compiled out of release builds. - fn assert_uses_drained(&self) { - debug_assert!( - self.operation_uses == OperationUses::default(), - "operation-uses accumulator must be drained before restoring the outer scope" - ); - } - // A public effect row carries labels, not operation subsets. Treat each // concrete label as every operation declared by that effect, and preserve // any open tail as an explicit precision barrier. @@ -260,7 +243,6 @@ impl Tc<'_> { |tc| tc.check(&env2, body, ret), ); let latent_uses = mem::take(&mut self.operation_uses); - self.assert_uses_drained(); self.operation_uses = outer_uses; if propagate_latent_uses { self.operation_uses.merge(latent_uses); @@ -512,7 +494,7 @@ impl Tc<'_> { let elem = self.apply(elem); let found = if matches!(elem, Type::Exist(_) | Type::Var(_)) { "an un-inferred element type (add an `OrNull(T)` annotation)".to_string() - } else if is_or_null_element(&elem) { + } else if self.or_null_element_ok(&elem) { continue; } else { format!("`{}`", elem.show()) @@ -660,7 +642,6 @@ impl Tc<'_> { |tc| tc.check(&env2, body, &Type::Exist(ret)), ); let _latent_uses = mem::take(&mut self.operation_uses); - self.assert_uses_drained(); self.operation_uses = outer_uses; checked?; Ok(self.apply(&Type::fun_eff(doms, EffRow::Exist(row), Type::Exist(ret)))) @@ -850,12 +831,12 @@ impl Tc<'_> { return Err(ErrKind::FieldAccessNonRecord { ty: other.show() }.at(span)) } }; - let (field_ty, fi) = self.find_field(span, ctor_name.as_str(), field, &te)?; - if let Some((cname, info)) = self.ctors.iter().find(|(_, c)| { - c.type_name == ctor_name && c.fields.iter().any(|f| f.as_str() == field) - }) { - self.field_res - .insert(id, (cname.clone(), fi, info.args.len())); + let (cname, field_ty, fi, arity) = + self.find_field_projection(span, ctor_name.as_str(), field, &te)?; + if self.field_res.insert(id, (cname, fi, arity)).is_some() { + return Err(TypeError::InternalInvariant { + msg: format!("field node {} produced duplicate resolution facts", id.0), + }); } Ok(field_ty) } @@ -971,6 +952,25 @@ impl Tc<'_> { let (body_ty, body_residual) = self.synth_handle_body(env, body, &scope, arms, mode, span)?; let ret_ex = self.push_ex(); + // With no return clause the implicit arm is the identity, so the + // handler's answer type is the handled body's type. Elaboration and + // both runtimes already pass the body's value through unchanged + // (`Comp::Handle` with no return body), so leaving the answer + // existential unconstrained here would report a polymorphic scheme for + // a value that is always the body's, and any concrete use of that + // scheme fails at the elaboration boundary instead of here. Applied + // before the clauses so each one checks against the settled answer. + if !arms.iter().any(|arm| matches!(arm, HandlerArm::Return(..))) { + let a = self.apply(&body_ty); + let b = self.apply(&Type::Exist(ret_ex)); + self.subtype(&a, &b).map_err(|e| { + e.or(TypeError::TypeMismatch { + span, + expected: b.show(), + found: a.show(), + }) + })?; + } for arm in arms { match arm { HandlerArm::Return(x, arm_body) => { @@ -1064,7 +1064,6 @@ impl Tc<'_> { msg: format!("handler node {} produced duplicate residual facts", id.0), }); } - self.assert_uses_drained(); self.operation_uses = outer_uses; self.operation_uses.merge(residual); Ok(self.apply(&Type::Exist(ret_ex))) @@ -1560,7 +1559,6 @@ impl Tc<'_> { let outer_uses = mem::take(&mut self.operation_uses); let body_ty = self.synth(env, body); let mut body_uses = mem::take(&mut self.operation_uses); - self.assert_uses_drained(); self.operation_uses = outer_uses; let t = body_ty?; let args = (0..self.eff_arity(eff_sym)) @@ -1634,7 +1632,6 @@ impl Tc<'_> { let frame = self.handler_stack.pop().expect("handler frame"); self.cur_row = saved_row; let mut body_uses = mem::take(&mut self.operation_uses); - self.assert_uses_drained(); self.operation_uses = handler_uses; let body_ty = self.apply(&body_ty?); let handled_operations = self.handled_operations(arms); diff --git a/src/tc/infer/decl.rs b/src/tc/infer/decl.rs index ea86908c..e17f4b72 100644 --- a/src/tc/infer/decl.rs +++ b/src/tc/infer/decl.rs @@ -85,9 +85,8 @@ impl Tc<'_> { // // Both halves belong here rather than at the end of the body. The zonk, because // a group's solutions are only complete once every member's body has run. The - // naming, because `renames` is the very renaming the exported scheme carries, so - // a body reads the letters its signature does by construction — rather than by - // rebuilding the naming early and hoping the two agree. + // naming. `renames` is also carried by the exported scheme, so body and + // signature render with the same variable names. fn flush_deferred(&mut self, renames: &Renames) { let Some((spans, rows)) = self.deferred_spans.pop_front() else { return; @@ -495,18 +494,23 @@ impl Tc<'_> { // refinement) before checking a new body. Handler residual facts accumulate // program-wide, but no in-progress use or continuation summary may cross a // declaration/SCC-member boundary. - fn clear_obligations(&mut self) { + fn clear_obligations(&mut self) -> Result<(), TypeError> { + if !self.handler_stack.is_empty() { + return Err(TypeError::InternalInvariant { + msg: "handler scope escaped its declaration boundary".into(), + }); + } self.wanted.clear(); self.num_default.clear(); self.neg_default.clear(); self.index_ops.clear(); self.operation_uses = super::super::OperationUses::default(); self.precise_calls.clear(); - debug_assert!(self.handler_stack.is_empty()); + Ok(()) } fn infer_body(&mut self, env: &Env, d: &Decl, seed: &DeclSeed) -> Result<(), TypeError> { - self.clear_obligations(); + self.clear_obligations()?; let mut env2 = env.clone(); for (p, t) in d.params.iter().zip(&seed.doms) { env2.insert(Sym::from(&p.name), t.clone()); @@ -537,9 +541,8 @@ impl Tc<'_> { self.cur_row = saved_row; checked?; // Held, not read. This declaration's own constraints are settled here, but a - // mutually recursive sibling's are not: `Cli@lex_go` passes `specs` along - // without using it, and `lex_long`'s `find_long(specs, …)` is what pins it — - // a body inferred after this one. Zonking now would report `specs` as the + // mutually recursive sibling's are not. In `Cli@lex_go`, the later + // `lex_long` body pins `specs`. Zonking now would report `specs` as the // unsolved variable it still is at this instant. `finish_decl` reads them // once the whole group is inferred, under the scheme it builds there. // @@ -692,7 +695,7 @@ impl Tc<'_> { d: &Decl, ) -> Result<(Type, Effects), TypeError> { self.reset_ctx(); - self.clear_obligations(); + self.clear_obligations()?; let (ty, effs) = self.scoped_effects_expected(EffRow::Empty, |tc| { let ty = if let Some(ann) = &d.ret { tc.check_annot_rows(ann, d.span)?; @@ -730,7 +733,7 @@ impl Tc<'_> { ) -> Result<(), TypeError> { for m in &inst.methods { self.reset_ctx(); - self.clear_obligations(); + self.clear_obligations()?; let (_, sig) = class .methods .iter() @@ -738,9 +741,9 @@ impl Tc<'_> { .ok_or_else(|| TypeError::InternalInvariant { msg: format!("instance method `{}` missing from class", m.name), })?; - // The instance method is checked against the class method's entire - // instantiated scheme, INCLUDING its effect row, not just its result - // type. The declared row bounds which concrete effects the body may + // Check the instance method's entire instantiated scheme, including + // its effect row. The declared row bounds which concrete effects the + // body may // perform: an effect-polymorphic method (`fmap : ... ! {| e}`) may // only forward effects that flow through the row variable, which stay // as the variable and never appear as concrete labels, so its @@ -774,10 +777,9 @@ impl Tc<'_> { })?; // Deliberately unseeded, unlike a function's spans. Seeding from the // class signature instantiated at this head was measured and made things - // worse: a method body refers to its class's own methods at several - // instantiations at once — `eqPair` uses `eq` at both components — and - // pinning those to one naming reports two genuinely different types as - // though they were the same one named twice. + // worse. A method body can use class methods at several instantiations. + // `eqPair`, for example, uses `eq` at both components. Pinning both to + // one naming conflates different types. self.flush_spans(); self.flush_holes(); // Recorded before the check below consumes it: this row is the only diff --git a/src/tc/infer/numeric.rs b/src/tc/infer/numeric.rs index 7572b393..6d0eec21 100644 --- a/src/tc/infer/numeric.rs +++ b/src/tc/infer/numeric.rs @@ -42,7 +42,7 @@ impl Tc<'_> { // The defer-or-fix ladder shared by every numeric/comparison operator, over // the already-applied left-operand type `t`. `Int` is the default lane and is - // accepted as-is; a fixed-width lane pins `id` so later width inference agrees; + // accepted as-is. A fixed-width lane pins `id` so later width inference agrees. // an unsolved existential defers to the `resolve_all` pass, where a still-later // use can pin its width before the `Int` default fires. Only the leftover case // differs per operator family (`NumClass`), and `blame` is the span the @@ -155,7 +155,7 @@ impl Tc<'_> { // Classify a unary-minus whose operand type is already applied. The lane is // recorded on the node for the elaborator (I64 wrap, Float sign flip); `Int` - // is the default and needs no record; `U64` is rejected; an unsolved operand + // is the default and needs no record. `U64` is rejected. An unsolved operand // defers to `resolve_all`. fn neg_lane(&mut self, t: &Type, id: NodeId, span: Span) -> Result { match t { diff --git a/src/tc/infer/records.rs b/src/tc/infer/records.rs index efa4060d..6a9b1bfb 100644 --- a/src/tc/infer/records.rs +++ b/src/tc/infer/records.rs @@ -93,6 +93,19 @@ impl Tc<'_> { self.ctors.keys().map(|k| names::bare_name(k)), )) })?; + let constructor_count = self + .ctors + .values() + .filter(|candidate| candidate.type_name == info.type_name) + .count(); + if constructor_count != 1 { + return Err(ErrKind::RecordSpreadMultiCtor { + ctor: ctor_name.to_string(), + ty: info.type_name.to_string(), + constructors: constructor_count, + } + .at(span)); + } let (result_ty, tsubs, rsubs) = self.open_ctor(&info); self.check(env, base_expr, &result_ty)?; for (field_name, field_expr) in field_exprs { @@ -163,20 +176,8 @@ impl Tc<'_> { } .at(span)); }; - let mut named: Vec<_> = self - .ctors - .iter() - .filter(|(_, c)| c.type_name == tname) - .map(|(n, c)| (n.clone(), c.args.len())) - .collect(); - let Some((cname, arity)) = named.pop().filter(|_| named.is_empty()) else { - return Err(ErrKind::UpdatePathMultiCtor { - ty: tname.to_string(), - n: named.len() + 1, - } - .at(span)); - }; - let (ft, fi) = self.find_field(span, tname.as_str(), seg, &cur)?; + let (cname, ft, fi, arity) = + self.find_update_field(span, tname.as_str(), seg, &cur)?; chain.push((cname, fi, arity)); cur = ft; } diff --git a/src/tc/mod.rs b/src/tc/mod.rs index eb0fbe03..951b8355 100644 --- a/src/tc/mod.rs +++ b/src/tc/mod.rs @@ -631,6 +631,67 @@ fn clean() : Int ! {} = } } +#[cfg(test)] +mod handler_return_tests { + use super::check; + use crate::parse::parse; + use crate::resolve::resolve; + use crate::syntax::ast::{Core, Program}; + use crate::syntax::desugar::desugar; + + fn core(src: &str) -> Program { + let surface = parse(src).expect("parse handler fixture").program; + let resolved = resolve(surface).expect("resolve handler fixture"); + desugar(resolved).expect("desugar handler fixture") + } + + const NO_RETURN_ARM: &str = " +effect Box + take() : Int + put(Int) : Unit + +fn passes_through() = + handle put(1) with + take() resume k => k(0) + put(v) resume w => w(()) +"; + + #[test] + fn handler_without_return_arm_answers_at_the_body_type() { + let program = core(NO_RETURN_ARM); + let checked = check(&program).expect("check handler fixture"); + let decl = checked + .decls + .iter() + .find(|decl| decl.name == "passes_through") + .expect("fixture declaration"); + assert_eq!(decl.ty.show(), "() -> Unit"); + } + + #[test] + fn concrete_use_of_the_implicit_answer_fails_at_the_use_site() { + let src = format!("{NO_RETURN_ARM}\nfn uses_it() : Int = passes_through()\n"); + let program = core(&src); + let error = check(&program).expect_err("Unit answer used at Int must be a type error"); + assert_eq!(error.code(), Some("E1022"), "{error}"); + } +} + +/// Declaration-level runtime representation of a nominal type. +/// +/// Constructor shape is not enough: an ordinary one-field datatype allocates +/// a cell, while a source `newtype` with the same shape is erased. Vector +/// builtins are multiword values and belong to neither class. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NominalRepr { + /// An allocated, non-zero runtime cell. + BoxedCell, + /// A source `newtype` whose wrapper is removed by mandatory lowering. + Transparent, + /// A two-word vector value. + Vec128, +} + #[derive(Clone, Debug)] pub struct DataInfo { pub params: Vec, @@ -642,6 +703,8 @@ pub struct DataInfo { // against its arguments at each annotation (see `env::check_annot_rows`). pub param_kinds: Vec, pub ctors: Vec, + /// Checked declaration evidence used by representation-sensitive queries. + pub repr: NominalRepr, } pub(crate) use crate::types::CtorInfo; @@ -818,9 +881,8 @@ pub struct Checked { /// /// An instance method is not in `decls`: it is checked from inside its /// instance rather than as a top-level function, so its row was computed, - /// held to the class signature's declared labels, and dropped. A consumer that - /// reports what a definition performs has no other source for it — an instance - /// has no `DeclInfo` and Core carries no rows. + /// held to the class signature's declared labels, and dropped. Instances have + /// no `DeclInfo`, and Core carries no rows, so consumers read this table. pub method_effects: BTreeMap, pub constrained: BTreeMap)>, pub seeds: u32, @@ -988,11 +1050,8 @@ struct Tc<'a> { // span inside it renders under one scheme instead of canonicalizing afresh // per node and calling the same variable `a` in one place and `c` in another. decl_renames: Option, - // One member's spans, held from the moment its body is inferred until its - // scheme exists. A recursion group is solved as a whole — a sibling's body is - // what pins an earlier member's parameter — so reading a member's types when - // its own body finishes reads them too early. One entry per member, pushed and - // taken in the order the group infers and generalizes them. + // Hold each member's spans until the whole recursion group is solved. A later + // sibling may still constrain an earlier member's parameters. deferred_spans: std::collections::VecDeque, hole_sites: Vec, holes: Vec, @@ -1273,7 +1332,7 @@ fn finalize_fn( ) -> Result { // The labels of the inferred row. Effect-row inference is principal: it // discovers every effect on its own (direct performs, applied effect-carrying - // callees, builtin rows, `mask`), so the row is the single source of truth. + // callees, builtin rows, `mask`), so the row alone determines inferred effects. // Real under-coverage is caught downstream by `reconcile_effects` (lowered // ops vs the row) and the parity oracle. let inferred = concrete_effects(&ty); @@ -1336,6 +1395,7 @@ fn finalize_fn( params: d.params.iter().map(|p| p.name.clone()).collect(), ty, effects: inferred, + pure: witness.effects.is_empty() && witness.closed, }) } @@ -1573,6 +1633,7 @@ fn check_seeded_mode( params: Vec::new(), ty, effects: Effects::new(), + pure: true, }); continue; } @@ -1602,6 +1663,7 @@ fn check_seeded_mode( params: Vec::new(), ty, effects: Effects::new(), + pure: true, }); } else { let witness = tc.body_witness.get(&d.name).ok_or_else(|| { @@ -1709,6 +1771,37 @@ pub fn infer_expr(checked: &Checked, e: &S>) -> Result<(Type, Effects infer_expr_env(checked, &Env::new(), e) } +/// A standalone expression plus every node fact established by its inference. +/// The REPL hands this artifact to elaboration as one unit so its fresh numeric +/// node identities can never read facts from the resident program. +pub(crate) struct CheckedExpr { + pub(crate) ty: Type, + pub(crate) effects: Effects, + #[cfg(feature = "native")] + pub(crate) facts: NodeFacts, + pub(crate) holes: Vec, + dicts: DictTable, +} + +/// Infer the complete artifact elaboration needs for a standalone expression. +/// +/// # Errors +/// Fails for ordinary type errors, and for typed holes unless `allow_holes` is +/// true. +#[cfg(feature = "native")] +pub(crate) fn infer_checked_expr( + checked: &Checked, + e: &S>, + allow_holes: bool, +) -> Result { + let inferred = infer_expr_full(checked, &Env::new(), e)?; + if allow_holes || inferred.holes.is_empty() { + Ok(inferred) + } else { + Err(hole_error(&inferred.holes)) + } +} + /// # Errors /// Fails when the expression does not type check. pub fn infer_expr_env( @@ -1716,11 +1809,11 @@ pub fn infer_expr_env( extra: &Env, e: &S>, ) -> Result<(Type, Effects), TypeError> { - let (t, eff, _, holes) = infer_expr_full(checked, extra, e)?; - if holes.is_empty() { - Ok((t, eff)) + let inferred = infer_expr_full(checked, extra, e)?; + if inferred.holes.is_empty() { + Ok((inferred.ty, inferred.effects)) } else { - Err(hole_error(&holes)) + Err(hole_error(&inferred.holes)) } } @@ -1733,8 +1826,8 @@ pub fn infer_expr_allow_holes( extra: &Env, e: &S>, ) -> Result<(Type, Effects, Vec), TypeError> { - let (ty, effects, _, holes) = infer_expr_full(checked, extra, e)?; - Ok((ty, effects, holes)) + let inferred = infer_expr_full(checked, extra, e)?; + Ok((inferred.ty, inferred.effects, inferred.holes)) } // Parse the canonical signature carried by a checked module interface. @@ -1750,11 +1843,11 @@ pub fn infer_expr_dicts( checked: &Checked, e: &S>, ) -> Result<(Type, Effects, DictTable), TypeError> { - let (ty, effects, dicts, holes) = infer_expr_full(checked, &Env::new(), e)?; - if holes.is_empty() { - Ok((ty, effects, dicts)) + let inferred = infer_expr_full(checked, &Env::new(), e)?; + if inferred.holes.is_empty() { + Ok((inferred.ty, inferred.effects, inferred.dicts)) } else { - Err(hole_error(&holes)) + Err(hole_error(&inferred.holes)) } } @@ -1766,14 +1859,20 @@ pub fn infer_expr_dicts_allow_holes( checked: &Checked, e: &S>, ) -> Result<(Type, Effects, DictTable, Vec), TypeError> { - infer_expr_full(checked, &Env::new(), e) + let inferred = infer_expr_full(checked, &Env::new(), e)?; + Ok(( + inferred.ty, + inferred.effects, + inferred.dicts, + inferred.holes, + )) } fn infer_expr_full( checked: &Checked, extra: &Env, e: &S>, -) -> Result<(Type, Effects, DictTable, Vec), TypeError> { +) -> Result { let mut env = checked.env.clone(); env.extend(extra.iter().map(|(k, v)| (*k, v.clone()))); // Re-inference shares `eff_ops`, whose var-state markers lowered to the @@ -1828,11 +1927,32 @@ fn infer_expr_full( tc.resolve_all()?; Ok(t) })?; + tc.check_or_null_sites()?; tc.flush_holes(); let t = tc.apply(&t); let g = tc.generalize(&env, &t); tc.holes.sort_by_key(|h| (h.start, h.end, h.name.clone())); - Ok((g, effs, tc.dicts, tc.holes)) + let dicts = tc.dicts; + #[cfg(feature = "native")] + let facts = NodeFacts::from_tables( + tc.field_res, + tc.unboxed_field, + tc.path_res, + tc.fixed, + tc.span_types, + dicts.clone(), + tc.tooltip_rows, + tc.handler_nodes, + tc.handler_residuals, + ); + Ok(CheckedExpr { + ty: g, + effects: effs, + #[cfg(feature = "native")] + facts, + holes: tc.holes, + dicts, + }) } // A checker context for read-only type queries. Search and synthesis use the diff --git a/src/tc/pat.rs b/src/tc/pat.rs index 67c17206..dd82e92e 100644 --- a/src/tc/pat.rs +++ b/src/tc/pat.rs @@ -231,25 +231,7 @@ impl Tc<'_> { } } - pub(super) fn find_field( - &self, - span: Span, - ctor_name: &str, - field: &str, - ty: &Type, - ) -> Result<(Type, usize), TypeError> { - let (info, fi) = self - .ctors - .values() - .filter(|c| c.type_name.as_str() == ctor_name) - .find_map(|c| Some((c, c.fields.iter().position(|f| f.as_str() == field)?))) - .ok_or_else(|| { - ErrKind::NoFieldOnType { - field: field.to_string(), - ctor_name: ctor_name.to_string(), - } - .at(span) - })?; + fn field_type(&self, info: &super::CtorInfo, fi: usize, ty: &Type) -> Type { let params = match ty { Type::Con(_, ps) => ps.clone(), _ => vec![], @@ -263,6 +245,86 @@ impl Tc<'_> { _ => ft = ft.subst_var(*pn, t), } } - Ok((self.apply(&ft), fi)) + self.apply(&ft) + } + + pub(super) fn find_field_projection( + &self, + span: Span, + type_name: &str, + field: &str, + ty: &Type, + ) -> Result<(String, Type, usize, usize), TypeError> { + let constructors: Vec<_> = self + .ctors + .iter() + .filter(|(_, info)| info.type_name.as_str() == type_name) + .collect(); + let found = constructors.iter().find_map(|(name, info)| { + info.fields + .iter() + .position(|candidate| candidate.as_str() == field) + .map(|fi| (*name, *info, fi)) + }); + let Some((ctor_name, info, fi)) = found else { + return Err(ErrKind::NoFieldOnType { + field: field.to_string(), + ctor_name: type_name.to_string(), + } + .at(span)); + }; + if constructors.len() != 1 { + return Err(ErrKind::PartialFieldProjection { + field: field.to_string(), + ty: type_name.to_string(), + constructors: constructors.len(), + } + .at(span)); + } + Ok(( + ctor_name.clone(), + self.field_type(info, fi, ty), + fi, + info.args.len(), + )) + } + + pub(super) fn find_update_field( + &self, + span: Span, + type_name: &str, + field: &str, + ty: &Type, + ) -> Result<(String, Type, usize, usize), TypeError> { + let mut constructors: Vec<_> = self + .ctors + .iter() + .filter(|(_, info)| info.type_name.as_str() == type_name) + .collect(); + let constructor_count = constructors.len(); + let Some((ctor_name, info)) = constructors.pop().filter(|_| constructors.is_empty()) else { + return Err(ErrKind::UpdatePathMultiCtor { + ty: type_name.to_string(), + n: constructor_count, + } + .at(span)); + }; + let fi = info + .fields + .iter() + .position(|candidate| candidate.as_str() == field) + .ok_or_else(|| { + ErrKind::NoFieldOnType { + field: field.to_string(), + ctor_name: type_name.to_string(), + } + .at(span) + })?; + Ok(( + ctor_name.clone(), + self.field_type(info, fi, ty), + fi, + info.args.len(), + )) } } diff --git a/src/tc/subsume.rs b/src/tc/subsume.rs index d05b5ab4..9b40a80d 100644 --- a/src/tc/subsume.rs +++ b/src/tc/subsume.rs @@ -165,7 +165,7 @@ impl Tc<'_> { let ex = self.push_ex(); let a1 = a0.subst_var(*n, &Type::Exist(ex)); self.subtype(&a1, b)?; - self.drop_marker(m); + self.drop_marker(m)?; Ok(()) } (_, Type::Forall(n, b0)) => { @@ -233,9 +233,22 @@ impl Tc<'_> { // side. The rules mirror except under a binder (left keeps foralls rigid, // right opens them) and function arguments flip side. fn inst(&mut self, ex: u32, t: &Type, left: bool) -> Result<(), TcErr> { + // Callers may still hold an existential that an earlier sibling + // constraint solved. Re-check that constraint against the installed + // solution; never send the stale id back through `solve`, where it + // would either overwrite evidence or (now) correctly fail closed. + if let Some(solved) = self.solved(ex) { + let solved = self.apply(&solved); + let t = self.apply(t); + return if left { + self.subtype(&solved, &t) + } else { + self.subtype(&t, &solved) + }; + } let t = self.apply(t); if t.is_mono() && self.well_formed_before(ex, &t) { - self.solve(ex, t); + self.solve(ex, t)?; return Ok(()); } match t { @@ -253,9 +266,9 @@ impl Tc<'_> { .index_ex(ex) .ok_or_else(|| TcErr::Ice(format!("inst: ^{ex} escaped scope")))?; if oi > ei { - self.solve(other, Type::Exist(ex)); + self.solve(other, Type::Exist(ex))?; } else { - self.solve(ex, Type::Exist(other)); + self.solve(ex, Type::Exist(other))?; } Ok(()) } @@ -345,6 +358,19 @@ impl Tc<'_> { let elem = self.apply(&elem); self.inst(elem_ex, &elem, left) } + Type::Coeffect(inner, row) => { + let inner_ex = self.fresh_id(); + let coeffect = Type::Coeffect(Box::new(Type::Exist(inner_ex)), row); + self.splice_solved(ex, &[inner_ex], coeffect)?; + let inner = self.apply(&inner); + self.inst(inner_ex, &inner, left) + } + Type::Row(row) => { + let row_ex = self.fresh_id(); + self.articulate_row_type(ex, row_ex)?; + let row = self.apply_row(&row); + self.unify_row(&EffRow::Exist(row_ex), &row) + } Type::Forall(n, body) if left => { let sk = Sym::fresh_named(n); let body = body.subst_var(n, &Type::Var(sk)); @@ -360,7 +386,7 @@ impl Tc<'_> { let e = self.push_ex(); let body = body.subst_var(n, &Type::Exist(e)); self.inst(ex, &body, false)?; - self.drop_marker(m); + self.drop_marker(m)?; Ok(()) } Type::RowForall(n, body) if left => { @@ -378,7 +404,7 @@ impl Tc<'_> { let r = self.push_ex_row(); let body = body.subst_row_var(n, &EffRow::Exist(r)); self.inst(ex, &body, false)?; - self.drop_marker(m); + self.drop_marker(m)?; Ok(()) } other => Err(TcErr::Fail(format!( @@ -402,12 +428,9 @@ impl Tc<'_> { // to the older, so a solution only references entries to its left and // survives later truncation at a marker. Mirrors `inst`'s `Exist` arm. (EffRow::Exist(x), EffRow::Exist(y)) => { - // Unlike the type context, the row context does not keep every - // solution strictly left-referencing, so absence here is not an - // internal fault: a later truncation can strand a row variable a - // unification still references. Surface it as a row-scope-escape - // user diagnostic rather than a compiler ICE. `Keep` so the precise - // reason survives a caller's coarse expected/got rewrite. + // A caller may still hold a raw row that an enclosing scope has + // closed. Surface that as a precise row-scope diagnostic rather + // than replacing it with a coarse expected/got mismatch. let xi = self .index_ex_row(*x) .ok_or_else(|| TcErr::Keep(ROW_ESCAPES_SCOPE.into()))?; @@ -415,9 +438,9 @@ impl Tc<'_> { .index_ex_row(*y) .ok_or_else(|| TcErr::Keep(ROW_ESCAPES_SCOPE.into()))?; if xi > yi { - self.solve_row(*x, EffRow::Exist(*y)) + self.solve_row(*x, &EffRow::Exist(*y)) } else { - self.solve_row(*y, EffRow::Exist(*x)) + self.solve_row(*y, &EffRow::Exist(*x)) } } (EffRow::Exist(x), other) | (other, EffRow::Exist(x)) => { @@ -426,7 +449,7 @@ impl Tc<'_> { if fv.contains(x) { return Err(TcErr::Fail("recursive effect row".into())); } - self.solve_row(*x, other.clone()) + self.solve_row(*x, other) } (EffRow::Extend(l, rest1), _) => { let rest2 = self.rewrite_row(&b, l)?; @@ -485,7 +508,7 @@ impl Tc<'_> { self.splice_solved_row( *alpha, &[beta], - EffRow::Extend(label.clone(), Box::new(EffRow::Exist(beta))), + &EffRow::Extend(label.clone(), Box::new(EffRow::Exist(beta))), )?; Ok(EffRow::Exist(beta)) } @@ -748,6 +771,335 @@ mod tests { } } + #[test] + fn type_solutions_fail_closed_on_missing_or_forward_variables() { + let ctors = BTreeMap::new(); + let data = BTreeMap::new(); + let eff_ops = BTreeMap::new(); + let classes = BTreeMap::new(); + let instances = BTreeMap::new(); + let inst_keys = BTreeMap::new(); + let canonical = BTreeMap::new(); + let mut t = tc( + &ctors, &data, &eff_ops, &classes, &instances, &inst_keys, &canonical, + ); + + assert!(matches!(t.solve(41, Type::Int), Err(TcErr::Ice(_)))); + + t.ctx.push(Entry::Ex(1)); + t.ctx.push(Entry::Ex(2)); + assert!(matches!(t.solve(1, Type::Exist(2)), Err(TcErr::Ice(_)))); + assert!(matches!(t.ctx.first(), Some(Entry::Ex(1)))); + + assert!(t.solve(1, Type::Int).is_ok(), "first solution is installed"); + assert!(matches!(t.solve(1, Type::Bool), Err(TcErr::Ice(_)))); + assert!(matches!(t.ctx.first(), Some(Entry::Solved(1, Type::Int)))); + assert!( + t.inst_l(1, &Type::Int).is_ok(), + "a stale constraint is checked against the installed solution" + ); + assert!(matches!(t.inst_l(1, &Type::Bool), Err(TcErr::Fail(_)))); + + t.ctx.push(Entry::ExRow(3)); + assert!( + t.solve_row(3, &EffRow::Empty).is_ok(), + "first row solution is installed" + ); + assert!(matches!( + t.solve_row(3, &EffRow::Var(Sym::from("e"))), + Err(TcErr::Ice(_)) + )); + assert!(matches!( + t.ctx.last(), + Some(Entry::SolvedRow(3, EffRow::Empty)) + )); + } + + #[test] + fn row_solutions_rebase_younger_variables_before_markers() { + let ctors = BTreeMap::new(); + let data = BTreeMap::new(); + let eff_ops = BTreeMap::new(); + let classes = BTreeMap::new(); + let instances = BTreeMap::new(); + let inst_keys = BTreeMap::new(); + let canonical = BTreeMap::new(); + let mut t = tc( + &ctors, &data, &eff_ops, &classes, &instances, &inst_keys, &canonical, + ); + + let owner = t.push_ex_row(); + let marker = t.fresh_id(); + t.ctx.push(Entry::Marker(marker)); + let young = t.push_ex(); + let cell = |arg| { + EffRow::Extend( + Label { + name: "Cell".into(), + args: vec![arg], + }, + Box::new(EffRow::Empty), + ) + }; + + assert!( + t.solve_row(owner, &cell(Type::Exist(young))).is_ok(), + "row solution rebases its young type argument" + ); + let solved = t.apply_row(&EffRow::Exist(owner)); + let mut live = BTreeSet::new(); + Type::Row(solved).free_exist(&mut live); + assert_eq!(live.len(), 1, "one shared representative is retained"); + let representative = *live.first().unwrap(); + assert_ne!(representative, young); + assert!( + t.index_ex(representative).unwrap() < t.index_ex_row(owner).unwrap(), + "the representative lives to the row owner's left" + ); + assert!( + t.drop_marker(marker).is_ok(), + "dropping the instantiation scope leaves no dangling id" + ); + + let concrete = cell(Type::Con("List".into(), vec![Type::Int])); + assert!( + t.unify_row(&EffRow::Exist(owner), &concrete).is_ok(), + "the representative remains available to the outer context" + ); + assert_eq!(t.apply_row(&EffRow::Exist(owner)), concrete); + + // Repeated occurrences preserve sharing: one young id becomes one older + // representative even when two labels mention it. + t.ctx.clear(); + t.next = 0; + let owner = t.push_ex_row(); + let marker = t.fresh_id(); + t.ctx.push(Entry::Marker(marker)); + let young = t.push_ex(); + let repeated = EffRow::Extend( + Label { + name: "First".into(), + args: vec![Type::Exist(young)], + }, + Box::new(EffRow::Extend( + Label { + name: "Second".into(), + args: vec![Type::Exist(young), Type::Exist(young)], + }, + Box::new(EffRow::Empty), + )), + ); + assert!(t.solve_row(owner, &repeated).is_ok()); + let mut live = BTreeSet::new(); + Type::Row(t.apply_row(&EffRow::Exist(owner))).free_exist(&mut live); + assert_eq!(live.len(), 1); + assert!(t.drop_marker(marker).is_ok()); + } + + #[test] + fn type_solutions_cannot_recontaminate_rebased_rows() { + let ctors = BTreeMap::new(); + let data = BTreeMap::new(); + let eff_ops = BTreeMap::new(); + let classes = BTreeMap::new(); + let instances = BTreeMap::new(); + let inst_keys = BTreeMap::new(); + let canonical = BTreeMap::new(); + let mut t = tc( + &ctors, &data, &eff_ops, &classes, &instances, &inst_keys, &canonical, + ); + + // A row may retain an older type variable which is solved only later. + // That later type solution must not smuggle a marker-local row back into + // the surviving row through the older representative. + let proxy = t.push_ex(); + let owner = t.push_ex_row(); + let cell = EffRow::Extend( + Label { + name: "Cell".into(), + args: vec![Type::Exist(proxy)], + }, + Box::new(EffRow::Empty), + ); + assert!(t.solve_row(owner, &cell).is_ok()); + let marker = t.fresh_id(); + t.ctx.push(Entry::Marker(marker)); + let young_row = t.push_ex_row(); + let callback = Type::Fun(Vec::new(), EffRow::Exist(young_row), Box::new(Type::Unit)); + assert!(t.equate(&Type::Exist(proxy), &callback).is_ok()); + assert!(t.drop_marker(marker).is_ok()); + assert!(t.index_ex_row(young_row).is_none()); + let applied = Type::Row(t.apply_row(&EffRow::Exist(owner))); + let mut rows = BTreeSet::new(); + applied.free_exist_row(&mut rows); + assert!(!rows.contains(&young_row)); + assert!(rows.iter().all(|row| t.index_ex_row(*row).is_some())); + + // Direct row types and rows nested under coeffects need their own + // structural articulation paths once the mono fast path rejects a + // forward row reference. + t.ctx.clear(); + t.next = 0; + let value = t.push_ex(); + let marker = t.fresh_id(); + t.ctx.push(Entry::Marker(marker)); + let young_row = t.push_ex_row(); + assert!(t + .equate(&Type::Exist(value), &Type::Row(EffRow::Exist(young_row)),) + .is_ok()); + assert!(t.drop_marker(marker).is_ok()); + let applied = t.apply(&Type::Exist(value)); + let mut rows = BTreeSet::new(); + applied.free_exist_row(&mut rows); + assert!(!rows.contains(&young_row)); + assert!(rows.iter().all(|row| t.index_ex_row(*row).is_some())); + + t.ctx.clear(); + t.next = 0; + let value = t.push_ex(); + let marker = t.fresh_id(); + t.ctx.push(Entry::Marker(marker)); + let young_row = t.push_ex_row(); + let once = CoeffectRow::new(&["once"]).unwrap(); + let coeffect = Type::Coeffect( + Box::new(Type::Fun( + Vec::new(), + EffRow::Exist(young_row), + Box::new(Type::Unit), + )), + once, + ); + assert!(t.equate(&Type::Exist(value), &coeffect).is_ok()); + assert!(t.drop_marker(marker).is_ok()); + let applied = t.apply(&Type::Exist(value)); + let mut rows = BTreeSet::new(); + applied.free_exist_row(&mut rows); + assert!(!rows.contains(&young_row)); + assert!(rows.iter().all(|row| t.index_ex_row(*row).is_some())); + } + + #[test] + fn row_solution_rebasing_is_nested_and_fail_closed() { + let ctors = BTreeMap::new(); + let data = BTreeMap::new(); + let eff_ops = BTreeMap::new(); + let classes = BTreeMap::new(); + let instances = BTreeMap::new(); + let inst_keys = BTreeMap::new(); + let canonical = BTreeMap::new(); + let mut t = tc( + &ctors, &data, &eff_ops, &classes, &instances, &inst_keys, &canonical, + ); + + let owner = t.push_ex_row(); + let marker = t.fresh_id(); + t.ctx.push(Entry::Marker(marker)); + let young_ty = t.push_ex(); + let young_row = t.push_ex_row(); + let once = CoeffectRow::new(&["once"]).unwrap(); + let nested = EffRow::Extend( + Label { + name: "Nested".into(), + args: vec![ + Type::Coeffect(Box::new(Type::Exist(young_ty)), once), + Type::Fun( + Vec::new(), + EffRow::Exist(young_row), + Box::new(Type::Row(EffRow::Exist(young_row))), + ), + ], + }, + Box::new(EffRow::Empty), + ); + assert!( + t.solve_row(owner, &nested).is_ok(), + "rebasing traverses coeffects, functions, and nested rows" + ); + let solved = Type::Row(t.apply_row(&EffRow::Exist(owner))); + let mut types = BTreeSet::new(); + solved.free_exist(&mut types); + let mut rows = BTreeSet::new(); + solved.free_exist_row(&mut rows); + assert_eq!(types.len(), 1); + assert_eq!(rows.len(), 1); + assert!(!types.contains(&young_ty)); + assert!(!rows.contains(&young_row)); + assert!(t.drop_marker(marker).is_ok()); + + // Rigid variables and recursive owner occurrences cannot be lowered to + // flexible proxies, and every refusal happens before the owner mutates. + t.ctx.clear(); + t.next = 0; + let owner = t.push_ex_row(); + let sk = Sym::fresh_named("a".into()); + t.ctx.push(Entry::Uni(sk)); + let captures_type = EffRow::Extend( + Label { + name: "Capture".into(), + args: vec![Type::Var(sk)], + }, + Box::new(EffRow::Empty), + ); + assert!(matches!( + t.solve_row(owner, &captures_type), + Err(TcErr::Keep(_)) + )); + assert!(matches!(t.ctx.first(), Some(Entry::ExRow(v)) if *v == owner)); + + t.ctx.clear(); + t.next = 0; + let owner = t.push_ex_row(); + let sk = Sym::fresh_named("e".into()); + t.ctx.push(Entry::RowUni(sk)); + let captures_row = EffRow::Extend( + Label { + name: "Capture".into(), + args: vec![Type::Row(EffRow::Var(sk))], + }, + Box::new(EffRow::Empty), + ); + assert!(matches!( + t.solve_row(owner, &captures_row), + Err(TcErr::Keep(_)) + )); + assert!(matches!(t.ctx.first(), Some(Entry::ExRow(v)) if *v == owner)); + + t.ctx.clear(); + t.next = 0; + let owner = t.push_ex_row(); + let recursive = EffRow::Extend( + Label { + name: "Recursive".into(), + args: vec![Type::Row(EffRow::Exist(owner))], + }, + Box::new(EffRow::Empty), + ); + assert!(matches!( + t.solve_row(owner, &recursive), + Err(TcErr::Fail(_)) + )); + + // The release-active marker audit catches a corrupt work-doer at the + // boundary, rather than allowing a later lookup to report E9998. + t.ctx.clear(); + t.next = 0; + let owner = t.push_ex_row(); + let marker = t.fresh_id(); + t.ctx.push(Entry::Marker(marker)); + let young = t.push_ex(); + t.ctx[0] = Entry::SolvedRow( + owner, + EffRow::Extend( + Label { + name: "Broken".into(), + args: vec![Type::Exist(young)], + }, + Box::new(EffRow::Empty), + ), + ); + assert!(matches!(t.drop_marker(marker), Err(TcErr::Ice(_)))); + } + // occurs_ex must look through every type former so subsume refuses to solve // an existential against a type that mentions it (an infinite type). #[test] diff --git a/src/types.rs b/src/types.rs index cb8ac7fa..1a02dea0 100644 --- a/src/types.rs +++ b/src/types.rs @@ -11,5 +11,5 @@ pub use crate::tc::{ check, check_allow_holes, check_seeded, check_seeded_allow_holes, hole_error, infer_expr, infer_expr_allow_holes, infer_expr_dicts, infer_expr_dicts_allow_holes, infer_expr_env, Canon, Checked, ClassInfo, DataInfo, Dict, DictTable, Env, HeadKey, HoleBinding, HoleCandidate, - HoleReport, InstInfo, InstKeys, PathRes, TypecheckSeed, Warning, + HoleReport, InstInfo, InstKeys, NominalRepr, PathRes, TypecheckSeed, Warning, }; diff --git a/src/verify/solver.rs b/src/verify/solver.rs index e1426cb0..d129a065 100644 --- a/src/verify/solver.rs +++ b/src/verify/solver.rs @@ -268,7 +268,7 @@ fn is_signaled(status: ExitStatus) -> bool { } #[cfg(not(unix))] -fn is_signaled(_status: ExitStatus) -> bool { +const fn is_signaled(_status: ExitStatus) -> bool { false } diff --git a/src/verify/tests.rs b/src/verify/tests.rs index 5a163d98..a576b658 100644 --- a/src/verify/tests.rs +++ b/src/verify/tests.rs @@ -278,17 +278,49 @@ fn contract_digest_is_stable_and_moves_on_clause_change() { // -- z3 solver-accept gate ----------------------------------------------------- -/// Locate a z3 binary: `PRISM_Z3` if set, else `z3` on `PATH`, confirmed by a -/// successful `--version`. `None` means the gate is skipped (no solver present). -fn z3_exe() -> Option { - let exe = std::env::var("PRISM_Z3").unwrap_or_else(|_| "z3".to_string()); +/// The solvers this module can drive, each with the override that names it and +/// the binary looked up on `PATH` when the override is unset. +const Z3_ENV: &str = "PRISM_Z3"; +const Z3_BIN: &str = "z3"; +const CVC5_ENV: &str = "PRISM_CVC5"; +const CVC5_BIN: &str = "cvc5"; + +/// Comma-separated solver names an environment promises to have installed, which +/// turns a missing one into a failure instead of a skip. A developer without a +/// solver still runs the rest of the suite; an environment that claims to cover +/// the discharge path and silently does not is what this catches, and it is what +/// happened here: nothing installed a solver, so every gate below skipped for as +/// long as the subsystem has existed. +const REQUIRE_SOLVERS_ENV: &str = "PRISM_REQUIRE_SOLVERS"; +const REQUIRE_SEP: char = ','; + +/// Whether the environment promised `name` would be present. +fn solver_required(name: &str) -> bool { + std::env::var(REQUIRE_SOLVERS_ENV) + .is_ok_and(|list| list.split(REQUIRE_SEP).any(|want| want.trim() == name)) +} + +/// Locate a solver binary: `env_var` if set, else `default_bin` on `PATH`, +/// confirmed by a successful `--version`. `None` skips the gate. +fn solver_exe(env_var: &str, default_bin: &str) -> Option { + let exe = std::env::var(env_var).unwrap_or_else(|_| default_bin.to_string()); let ok = Command::new(&exe) .arg("--version") .output() .is_ok_and(|o| o.status.success()); + assert!( + ok || !solver_required(default_bin), + "{REQUIRE_SOLVERS_ENV} names {default_bin} but {exe} did not answer \ + `--version`; the solver-accept gate would have skipped silently" + ); ok.then_some(exe) } +/// Locate a z3 binary. `None` means the gate is skipped (no solver present). +fn z3_exe() -> Option { + solver_exe(Z3_ENV, Z3_BIN) +} + /// Run one canonical SMT-LIB script through z3 over stdin and parse its status /// through the same response parser the out-of-process adapter uses. fn z3_status(exe: &str, script: &str) -> SolverStatus { @@ -1182,15 +1214,9 @@ fn inc(x: Int): Int // -- cvc5 adapter and cross-solver agreement (gated on a present cvc5) ---------- -/// Locate a cvc5 binary: `PRISM_CVC5` if set, else `cvc5` on `PATH`, confirmed by a -/// successful `--version`. `None` means the gate is skipped (cvc5 not installed). +/// Locate a cvc5 binary. `None` means the gate is skipped (cvc5 not installed). fn cvc5_exe() -> Option { - let exe = std::env::var("PRISM_CVC5").unwrap_or_else(|_| "cvc5".to_string()); - let ok = Command::new(&exe) - .arg("--version") - .output() - .is_ok_and(|o| o.status.success()); - ok.then_some(exe) + solver_exe(CVC5_ENV, CVC5_BIN) } const CONTRACT_PROG: &str = "\ diff --git a/src/wasm/mod.rs b/src/wasm/mod.rs index e3bf4e23..ce80604b 100644 --- a/src/wasm/mod.rs +++ b/src/wasm/mod.rs @@ -163,8 +163,7 @@ pub fn boids_run(steps: u32) -> String { } /// Run the boids swarm for `steps` steps and return the whole trajectory in -/// FULL state: like [`boids_run`], but each boid is `x,y,vx,vy` (position and -/// velocity), not just `x,y`. +/// FULL state: like [`boids_run`], with each boid represented as `x,y,vx,vy`. /// /// The velocity is what a branching timeline needs: to fork at frame N and /// continue the run, the frontend perturbs that frame's full state and hands it @@ -414,8 +413,8 @@ fn teleport_roots() -> Vec { /// The code-identity digest (namespace root) of the baked teleport program. /// /// Both tabs compute this from the same embedded source, so it is the hash the -/// receiver checks an incoming envelope against; the demo shows it as the proof -/// that teleport verifies code identity, not just moves bytes. +/// receiver checks an incoming envelope against. This proves code identity during +/// teleport. #[wasm_bindgen] #[must_use] pub fn teleport_bundle() -> String { @@ -616,11 +615,12 @@ pub fn core_ir(src: &str) -> String { } } -/// The checked-HIR fixture of the snippet: the versioned deterministic JSON the -/// `dump hir` phase emits (schema `prism-hir-fixture-v2`), carrying the -/// per-declaration schemes and effect rows plus the per-node checker facts -/// (resolution, dictionary evidence, numeric lane, zonked type, and handler -/// residual operations). +/// The versioned checked-HIR fixture for the snippet. +/// +/// This is the deterministic JSON emitted by `dump hir` (schema +/// `prism-hir-fixture-v2`). It carries per-declaration schemes and effect rows, +/// plus per-node resolution, dictionary, numeric-lane, zonked-type, and handler +/// residual-operation facts. /// /// The prelude is prepended so snippets that reference it type-check; the /// browser strips the prelude declarations for display the same way the Core IR diff --git a/tests/README.md b/tests/README.md index 3412f55f..66b2ea46 100644 --- a/tests/README.md +++ b/tests/README.md @@ -2,38 +2,41 @@ Prism's test suite (or what I lovingly call "The Gauntlet") is intentionally quite extreme. It enforces byte-for-byte compiler and runtime agreement across many layers. -- **[Native parity](native_parity.rs): Matches interpreter and native behavior byte for byte across the corpus.** -- **[Native tiers](native_tier.rs): Makes every effect-lowering tier agree exactly.** -- **[Typed Core spine](typed_spine.rs): Demands exact Core identity across typed erasure boundaries.** -- **[Compiler](compiler.rs): Checks compiler internals and byte-identical cold, warm, and incremental builds.** -- **[Language](language.rs): Probes the type, effect, module, and soundness rules.** -- **[Lineage](lineage_suite.rs): Keeps provenance verifiable and byte-identical across repeated runs.** -- **[Native cache](native_cache.rs): Demands byte-identical cold and cached native artifacts.** -- **[Runtime](runtime.rs): Checks byte-for-byte replay, suspension, scheduling, and recovery.** +The suite compiles as six binaries ([`compiler`](compiler.rs), [`differential`](differential.rs), [`frontend`](frontend.rs), [`native`](native.rs), [`snapshots`](snapshots.rs), [`tooling`](tooling.rs)), each pulling in modules from the directories below; the entries here link to the module that owns the gate, not to the binary that runs it. What each of those binaries costs to run, and on which arm, is declared in [the cost ledger](lane_ledger.txt). + +- **[Native parity](native/parity.rs): Matches interpreter and native behavior byte for byte across the corpus.** +- **[Native tiers](native/tier_parity.rs): Makes every effect-lowering tier agree exactly.** +- **[Typed Core spine](differential/typed_spine.rs): Demands exact Core identity across typed erasure boundaries.** +- **[Compiler](compiler/): Checks compiler internals and byte-identical cold, warm, and incremental builds.** +- **[Language](language/): Probes the type, effect, module, and soundness rules.** +- **[Lineage](lineage_suite/): Keeps provenance verifiable and byte-identical across repeated runs.** +- **[Native cache](native/compiler_cache.rs): Demands byte-identical cold and cached native artifacts.** +- **[Runtime](runtime/): Checks byte-for-byte replay, suspension, scheduling, and recovery.** - **[Snapshots](snapshots.rs): Byte-for-byte golden gates for compiler phases and program output.** -- **[Standard-library hash](stdlib_hash.rs): Pins the standard library to one reproducible semantic root.** -- [Bootstrap](bootstrap.rs): Checks the Prism-written checker against authoritative Rust facts and reports honest coverage. -- [CLI and docs](cli_docs.rs): Keeps examples, projects, docs, and CLI output honest. -- [Contracts](contracts.rs): Keeps logical contracts checked, deterministic, and erased from executable Core. -- [Determinism](determinism.rs): Makes canonical hashes independent of compilation history and scheduling. -- [Durable driver](durable_driver.rs): Crashes and resumes persisted runs without changing their observation trace. -- [Environment knobs](env_knobs.rs): Keeps every `PRISM_*` read in its documented ownership boundary. -- [Error codes](error_codes.rs) and [explain coverage](explain_coverage.rs): Keep diagnostic identities unique and every public code explained. -- [Formatter](formatter.rs): Preserves syntax and comments through formatting. -- [Typed holes](holes.rs) and [type queries](type_query.rs): Exercise reporting, filling, search, and bounded rechecked synthesis through the real CLI. -- [ISA fixture](isa_fixture.rs): Compiles a tiny out-of-tree backend against the public shared-emitter API. -- [Lean fuzz](lean_fuzz.rs): Feeds deterministic generated Core through both the Rust interpreter and Lean CEK oracle. -- [Native conformance](native_conformance.rs): Matches native float behavior to the interpreter. -- [Native fusion](native_fusion.rs): Checks deterministic fusion without semantic drift. -- [Native performance](native_perf.rs): Guards allocation, stack, fusion, and complexity budgets. -- [Native sorting](native_sort.rs): Matches native sorting to the interpreter. -- [Optimizer equivalence](opt_equiv.rs): Forces optimizer configurations over the corpus and requires identical observation traces. -- [Packages and certificates](package.rs): Covers package trust, transport, locking, and certificates. -- [`prism test`](prism_test.rs): Covers discovery, filtering, isolation, capture, manifests, and production neutrality. -- [Semantic patches](semantic_patch.rs): Keeps patches atomic, reproducible, and behavior-checked. -- [Stable locks](stable_lock.rs): Pins migration edges and routes to their content-addressed behavior. -- [Store and package coherence](store_pkg.rs): Tests store immutability, concurrency, hashes, and coherence. -- [Totality](totality.rs): Checks structural termination evidence, assumptions, ranking obligations, and Core erasure. -- [Duplicate warnings](warn_dupes.rs): Checks clone warnings and their severity modes. +- **[Standard-library hash](compiler/stdlib_hash.rs): Pins the standard library to one reproducible semantic root.** +- [Bootstrap](tooling/bootstrap.rs): Checks the Prism-written checker against authoritative Rust facts and reports honest coverage. +- [CLI and docs](cli_docs/): Keeps examples, projects, docs, and CLI output honest. +- [Contracts](frontend/contracts.rs): Keeps logical contracts checked, deterministic, and erased from executable Core. +- [Cost ledger](tooling/lane_ledger.rs): Joins the declared cost of every gauntlet lane against the workflows that run it. +- [Determinism](differential/determinism.rs): Makes canonical hashes independent of compilation history and scheduling. +- [Durable driver](tooling/durable_driver.rs): Crashes and resumes persisted runs without changing their observation trace. +- [Environment knobs](frontend/env_knobs.rs): Keeps every `PRISM_*` read in its documented ownership boundary. +- [Error codes](frontend/error_codes.rs): Keeps diagnostic identities unique and every public code explained exactly once. +- [Formatter](formatter/): Preserves syntax and comments through formatting. +- [Typed holes](frontend/holes.rs) and [type queries](frontend/type_query.rs): Exercise reporting, filling, search, and bounded rechecked synthesis through the real CLI. +- [ISA fixture](tooling/isa_fixture.rs): Compiles a tiny out-of-tree backend against the public shared-emitter API. +- [Lean fuzz](differential/lean_fuzz.rs): Feeds deterministic generated Core through both the Rust interpreter and Lean CEK oracle. +- [Native conformance](native/float_math_conformance.rs): Matches native float behavior to the interpreter. +- [Native fusion](native/fuse_parity.rs): Checks deterministic fusion without semantic drift. +- [Native performance](native/perf_gate.rs): Guards allocation, stack, fusion, and complexity budgets. +- [Native sorting](native/sort_kind.rs): Matches native sorting to the interpreter. +- [Optimizer equivalence](opt_equiv/gate.rs): Forces optimizer configurations over the corpus and requires identical observation traces. +- [Packages and certificates](package/): Covers package trust, transport, locking, and certificates. +- [`prism test`](tooling/prism_test.rs): Covers discovery, filtering, isolation, capture, manifests, and production neutrality. +- [Semantic patches](frontend/semantic_patch.rs): Keeps patches atomic, reproducible, and behavior-checked. +- [Stable locks](tooling/stable_lock.rs): Pins migration edges and routes to their content-addressed behavior. +- [Store and package coherence](store_pkg/): Tests store immutability, concurrency, hashes, and coherence. +- [Totality](frontend/totality.rs): Checks structural termination evidence, assumptions, ranking obligations, and Core erasure. +- [Duplicate warnings](frontend/warn_dupes.rs): Checks clone warnings and their severity modes. One more core gate lives outside this directory: the [Lean 4 differential-oracle runner](../models/diff_against_rust.sh) has the Rust compiler dump its live Core as JSON, feeds that same dump to the verified Lean CEK machine, and requires the Rust and Lean results to agree exactly. The [formal model](../models/README.md) also proves properties of that CEK machine, including determinism, replay faithfulness, and correspondence with the big-step semantics. The companion replayable fuzz gate generates deterministic random source programs, feeds their compiled Core to both implementations, and shrinks any disagreement to a minimal oracle-tested reproducer. diff --git a/tests/cases/field_projection_common.pr b/tests/cases/field_projection_common.pr new file mode 100644 index 00000000..744e4401 --- /dev/null +++ b/tests/cases/field_projection_common.pr @@ -0,0 +1,13 @@ +-- A common field is type-consistent, but plain projection currently carries +-- only one constructor arm. It remains rejected until Core carries multi-arm +-- projection evidence. +type Tagged = A { id: Int } | B { id: Int } + +fn tag_id_by_match(tagged : Tagged) : Int = + match tagged of + A { id = id } => id + B { id = id } => id + +fn tag_id(tagged : Tagged) : Int = tagged.id + +fn main() = println(0) diff --git a/tests/cases/field_projection_nested.pr b/tests/cases/field_projection_nested.pr new file mode 100644 index 00000000..5fcda119 --- /dev/null +++ b/tests/cases/field_projection_nested.pr @@ -0,0 +1,8 @@ +-- A valid outer projection must not hide a partial inner projection. +type Tagged = A { id: Int } | B { id: Int } + +type Outer = Outer { inner: Tagged } + +fn tag_id(outer : Outer) : Int = outer.inner.id + +fn main() = println(0) diff --git a/tests/cases/field_projection_partial.pr b/tests/cases/field_projection_partial.pr new file mode 100644 index 00000000..449de20e --- /dev/null +++ b/tests/cases/field_projection_partial.pr @@ -0,0 +1,11 @@ +-- `radius` exists, but only on one arm of the unrefined `Shape` value. +type Shape = Circle { radius: Int } | Square { side: Int } + +fn radius_by_match(shape : Shape) : Int = + match shape of + Circle { radius = radius } => radius + Square { .. } => 0 + +fn radius(shape : Shape) : Int = shape.radius + +fn main() = println(0) diff --git a/tests/cases/field_projection_shadow.pr b/tests/cases/field_projection_shadow.pr new file mode 100644 index 00000000..0117b08a --- /dev/null +++ b/tests/cases/field_projection_shadow.pr @@ -0,0 +1,9 @@ +-- A bare `.radius` is always field projection. A same-named top-level function +-- must not turn a refused partial projection into silent UFCS fallback. +type Shape = Circle { radius: Int } | Square { side: Int } + +fn radius(_shape : Shape) : Int = 99 + +fn read(shape : Shape) : Int = shape.radius + +fn main() = println(0) diff --git a/tests/cases/perf/arena_promote_linear.pr b/tests/cases/perf/arena_promote_linear.pr new file mode 100644 index 00000000..92b32bfb --- /dev/null +++ b/tests/cases/perf/arena_promote_linear.pr @@ -0,0 +1,22 @@ +-- The bounded case for the promotion oracle: an unshared list of N cells built +-- under `with_arena` and returned out of it. Every cell is reachable by exactly +-- one path, so copies, nodes, and edges are each one per element and no edge +-- ever finds an existing copy to reuse. This is the row that pins what promotion +-- costs when there is no sharing to preserve, so the shared case has something +-- to be compared against. +import Arena (..) + +fn build(n : Int, acc : List(Int)) : List(Int) = + if n == 0 then + acc + else + build(n - 1, Cons(n, acc)) + +fn escape() : List(Int) = build(__N__, Nil) + +fn total(xs : List(Int)) : Int = + match xs of + Nil => 0 + Cons(h, t) => h + total(t) + +fn main() = println(total(with_arena(escape))) diff --git a/tests/cases/perf/arena_promote_none.pr b/tests/cases/perf/arena_promote_none.pr new file mode 100644 index 00000000..1566283b --- /dev/null +++ b/tests/cases/perf/arena_promote_none.pr @@ -0,0 +1,19 @@ +-- The zero case for the promotion oracle: a region whose result is a scalar. +-- Everything the scope builds dies with the region, so the promotion walk has +-- nothing to enter and nothing to copy, whatever N is. +import Arena (..) + +fn build(n : Int, acc : List(Int)) : List(Int) = + if n == 0 then + acc + else + build(n - 1, Cons(n, acc)) + +fn total(xs : List(Int)) : Int = + match xs of + Nil => 0 + Cons(h, t) => h + total(t) + +fn scratch() : Int = total(build(__N__, Nil)) + +fn main() = println(with_arena(scratch)) diff --git a/tests/cases/perf/arena_promote_shared.pr b/tests/cases/perf/arena_promote_shared.pr new file mode 100644 index 00000000..0d86f805 --- /dev/null +++ b/tests/cases/perf/arena_promote_shared.pr @@ -0,0 +1,26 @@ +-- A shared DAG built under `with_arena` and returned out of it. Each level's +-- two fields point at the same child, so N region cells span 2^N root-to-leaf +-- paths. Promotion must copy each reachable cell once and share the copy, which +-- costs N allocations; a walk that recurses per path copies the whole expansion +-- instead and costs 2^N. Both produce the identical value, so only the +-- allocation counter can tell them apart. +import Arena (..) + +type Shared = Leaf(Int) | Fork(Shared, Shared) + +fn share(n : Int, x : Shared) : Shared = + if n == 0 then + x + else + share(n - 1, Fork(x, x)) + +fn build() : Shared = share(__N__, Leaf(1)) + +-- Walks one spine, so reading the result stays linear in N whatever the +-- promotion did to its sharing. +fn depth(t : Shared) : Int = + match t of + Leaf(_) => 0 + Fork(a, _) => 1 + depth(a) + +fn main() = println(depth(with_arena(build))) diff --git a/tests/cases/perf/borrowed_walk.pr b/tests/cases/perf/borrowed_walk.pr new file mode 100644 index 00000000..b2c78f4a --- /dev/null +++ b/tests/cases/perf/borrowed_walk.pr @@ -0,0 +1,21 @@ +-- A read-only walk repeated over one shared list. The walk never consumes the +-- spine, so once its parameters are inferred borrowed, every pass threads no +-- reference-count traffic on cells: the only counted release is the list's own +-- teardown. With inference disabled the same program pays a retain/release +-- pair per level per pass, which is the floor the gate uses to prove the +-- probe still exercises real pressure. + +fn sum_walk(xs, acc) = + match xs of + Nil => acc + Cons(h, t) => sum_walk(t, acc + h) + +fn walk_rounds(k, xs, acc) = + if k == 0 then + acc + else + walk_rounds(k - 1, xs, acc + sum_walk(xs, 0)) + +fn main() = + let xs = range(1, 1000) + println(walk_rounds(20, xs, 0)) diff --git a/tests/cases/perf/bytes_body_decode.pr b/tests/cases/perf/bytes_body_decode.pr new file mode 100644 index 00000000..984ad99d --- /dev/null +++ b/tests/cases/perf/bytes_body_decode.pr @@ -0,0 +1,34 @@ +import Wire (..) + +-- Decode a `Bytes` payload of N bytes, whose wire form is one signed varint per +-- byte. The reader accumulates the decoded bytes into a growable buffer, and that +-- buffer extends in place only while it is uniquely owned, so the whole loop is +-- kept out of the failure row: a total scan reports a short or malformed run +-- through its cursor and the caller raises once at the boundary. Threading the +-- accumulator through the failure row instead shares it at every step, and each +-- push then copies the whole accumulation, which costs the square of N. +type Blob = Blob(Bytes) deriving (Serialize) + +fn mk(i : Int, n : Int, acc : Buf) : Buf = + if i >= n then + acc + else + mk(i + 1, n, buf_push(acc, 97)) + +fn dec(bs : Bytes) : (Blob, Bytes) = decode(bs) + +fn body_len(bs : Bytes) : Int = + match dec(bs) of + (Blob(out), _r) => wire_len(out) + +-- The row is inferred (decode performs `Fail`); `caught` discharges it to a +-- default so `main` stays pure, exactly as `wire_decode.pr` does. +fn caught(action : () -> Int ! {Fail | e}) : Int = + handle action() with + never fail() => -1 + return r => r + +fn run(bs : Bytes) : Int = caught(\() -> body_len(bs)) + +fn main() : Unit ! {IO} = + println(show_int(run(encode(Blob(bytes_of_buf(mk(0, __N__, buf_empty()))))))) diff --git a/tests/cases/perf/json_escape_runs.pr b/tests/cases/perf/json_escape_runs.pr new file mode 100644 index 00000000..4ff7c0a1 --- /dev/null +++ b/tests/cases/perf/json_escape_runs.pr @@ -0,0 +1,18 @@ +import Json (encode, JStr) + +-- Encode a string whose every other byte needs escaping, so the escape count +-- and the output length both scale with N. Appending each clean run and each +-- escape into one growable buffer costs one pass over the input; rebuilding the +-- accumulated output at every escape costs the escape count times the length +-- escaped so far, which is quadratic in N and shows up as a byte total that +-- grows with the square of the input. +fn quoted(i : Int, acc : Buf) : Buf = + if i >= __N__ then + acc + else + quoted(i + 1, buf_push(buf_push(acc, 97), 34)) + +fn main() : Unit ! {IO} = + println( + show_int(byte_len(encode(JStr(string_of_buf(quoted(0, buf_empty())))))), + ) diff --git a/tests/cases/perf/str_slice_window.pr b/tests/cases/perf/str_slice_window.pr new file mode 100644 index 00000000..c89af39c --- /dev/null +++ b/tests/cases/perf/str_slice_window.pr @@ -0,0 +1,16 @@ +import Data.String (str_slice) + +-- Take a fixed number of long windows onto one N-byte string. A window shares +-- the parent's bytes and holds it alive, so the loop allocates one small cell +-- per window and its byte total is independent of N; a slice that copied would +-- materialize most of the parent on every step, making the byte total grow with +-- N times the loop count. +fn windows(s : String, i : Int, acc : Int) : Int = + if i >= 200 then + acc + else + windows(s, i + 1, acc + byte_len(str_slice(s, i, byte_len(s)))) + +fn main() : Unit ! {IO} = + let s = string_of_buf(buf_new(__N__, 65)) + println(show_int(windows(s, 0, 0))) diff --git a/tests/cases/record_spread_sum.pr b/tests/cases/record_spread_sum.pr new file mode 100644 index 00000000..7df429ca --- /dev/null +++ b/tests/cases/record_spread_sum.pr @@ -0,0 +1,7 @@ +-- A constructor spread extracts unchanged fields from that constructor. An +-- unrefined sum base may hold another layout, so it must be rejected before Core. +type Shape = Circle { radius: Int } | Square { side: Int } + +fn resize(shape : Shape) : Shape = Circle { ..shape, radius = 2 } + +fn main() = println(0) diff --git a/tests/cases/run/bytes_view.pr b/tests/cases/run/bytes_view.pr new file mode 100644 index 00000000..c0ea89c0 --- /dev/null +++ b/tests/cases/run/bytes_view.pr @@ -0,0 +1,66 @@ +-- The observable contract of a byte-string window, which is the same on both +-- tiers whether the window shares the parent's bytes or copies them. What is +-- pinned here: both endpoints are clamped to what the value actually holds, so a +-- negative start, a start past the end, a negative length, and a length past the +-- remainder all yield the in-range part rather than trapping; a window onto a +-- window addresses the original, so re-slicing a remainder never deepens a chain; +-- a window is an ordinary `Bytes` everywhere downstream, including equality, +-- comparison, hashing, concatenation, and the codecs; and a window over raw +-- non-UTF-8 bytes stays raw, so it round-trips through hex verbatim while +-- decoding to a `String` reports the failure instead of repairing it. + +import Data.Bytes (..) + +fn show_case(label : String, bs : Bytes) : Unit ! {IO} = + println("{label}: [{hex_encode(bs)}] len={bytes_length(bs)}") + +fn show_text(label : String, bs : Bytes) : Unit ! {IO} = + match bytes_to_string(bs) of + Some(s) => println("{label}: [{s}]") + None => println("{label}: ") + +-- A window onto a window onto a window: the composition addresses the original, +-- so the result is the same span a single slice of the original would give. +fn nest(bs : Bytes) : Bytes = + bytes_slice(bytes_slice(bytes_slice(bs, 2, 20), 1, 9), 2, 6) + +-- Peel one byte at a time off the front, reassembling what falls off. Every step +-- takes a window onto the whole remainder, which is where a chain would grow. +fn peel(bs : Bytes, acc : Bytes) : Bytes = + if bytes_length(bs) == 0 then + acc + else + peel( + bytes_slice(bs, 1, bytes_length(bs) - 1), + bytes_push(acc, bytes_index(bs, 0)), + ) + +fn main() : Unit ! {IO} = + let bs = string_to_bytes("hello wide world") + show_case("whole", bs) + show_case("middle", bytes_slice(bs, 6, 4)) + -- Every out-of-range endpoint clamps to the value rather than trapping. + show_case("start negative", bytes_slice(bs, -5, 4)) + show_case("start past end", bytes_slice(bs, 99, 4)) + show_case("len negative", bytes_slice(bs, 3, -1)) + show_case("len past end", bytes_slice(bs, 11, 99)) + show_case("empty span", bytes_slice(bs, 4, 0)) + show_case("nested", nest(bs)) + show_case("peeled", peel(bs, bytes_empty)) + show_text("peeled text", peel(bs, bytes_empty)) + -- A window is an ordinary value downstream: it compares, hashes, concatenates, + -- and re-encodes exactly like a value built from its own bytes. + let win = bytes_slice(bs, 6, 4) + let own = string_to_bytes("wide") + println("eq={bytes_eq(win, own)} cmp={show_int(bytes_compare(win, own))}") + println("hash={bytes_hash(win) == bytes_hash(own)}") + show_case("concat", bytes_concat(win, bytes_slice(bs, 11, 5))) + show_text( + "base64 roundtrip", + unwrap_or(bytes_empty, base64_decode(base64_encode(win))), + ) + -- Raw bytes with no UTF-8 reading: the window keeps them verbatim, so hex + -- round-trips and the string decode reports the failure rather than repairing it. + let raw = unwrap_or(bytes_empty, hex_decode("00ff10fe80c3")) + show_case("raw window", bytes_slice(raw, 1, 4)) + show_text("raw window text", bytes_slice(raw, 1, 4)) diff --git a/tests/cases/run/eff_poly_fn_arg.pr b/tests/cases/run/eff_poly_fn_arg.pr index fc8de1f2..481cc0f8 100644 --- a/tests/cases/run/eff_poly_fn_arg.pr +++ b/tests/cases/run/eff_poly_fn_arg.pr @@ -3,11 +3,17 @@ -- effect row) used to ICE in row unification: `rewrite_row` opened an existential -- row tail by appending the fresh tail to the right, a forward reference that a -- later truncation stranded ("solve_row: ^N not in context"). It must now type --- check and run. +-- check and run. The second half pins the type argument inside a parameterized +-- effect label: the generic callback opens it under an instantiation marker, +-- while `map`'s callback row was created outside that marker. The row solution +-- must retain a live representative rather than a dangling inner existential. effect Tag fresh() : Int +effect Cell(a) + read() : a + -- Polymorphic in `a`, effectful in `Tag`: this is the shape that triggered it. fn tag(x) : (Int, a) ! {Tag} = (fresh(), x) @@ -23,7 +29,23 @@ fn run_tags(action) = return r => \(_n) -> r go(0) +-- `read` must actually run. A wider annotation on an otherwise pure callback +-- does not exercise the row-solution scope boundary. +fn peek(x : Int) : Int ! {Cell(d)} = + match read() of + _value => x + +fn roots(xs : List(Int)) : List(Int) ! {Cell(List(Int))} = map(peek, xs) + +fn run_roots() = + handle roots([1, 2, 3]) with + read() resume k => + println("cell read") + k([99]) + return r => r + fn main() = println( show(run_tags(\() -> map_eff(tag, Cons(10, Cons(20, Cons(30, Nil)))))), ) + println(show(run_roots())) diff --git a/tests/cases/run/eff_poly_handler_install.pr b/tests/cases/run/eff_poly_handler_install.pr new file mode 100644 index 00000000..42d1161f --- /dev/null +++ b/tests/cases/run/eff_poly_handler_install.pr @@ -0,0 +1,24 @@ +-- A function that both installs a handler and applies an effect-polymorphic +-- callee (`! {| e}`) inside an operation clause, instantiated at the empty +-- row while the function's own row stays open. The fused lowering must carry +-- its ambient row through the call's instantiation as well as the signature, +-- so the program stays on the fused path and prints the same pair everywhere. +effect Tick + bump(Int) : Unit + +fn apply_poly(f : (Int, Int) -> Int ! {| e}, a : Int, b : Int) : Int ! { | e} = + f(a, b) + +fn add(a : Int, b : Int) : Int = a + b + +fn run(action : () -> a ! {Tick | e}) : (a, Int) = + var total := 0 + let r = + handle action() with + bump(n) resume k => + total := apply_poly(add, total, n) + k(()) + return r => r + (r, total) + +fn main() = println(show(run(\() -> bump(3)))) diff --git a/tests/cases/run/eff_row_unwitnessed.pr b/tests/cases/run/eff_row_unwitnessed.pr new file mode 100644 index 00000000..4d3c1a52 --- /dev/null +++ b/tests/cases/run/eff_row_unwitnessed.pr @@ -0,0 +1,25 @@ +-- A container whose element type declares an effect row no element ever +-- performs: every element satisfies the annotation by subsumption, so the +-- program compiles (with a proportionate unused-effect warning) and the +-- unwitnessed row costs at most a slower tier, never an error. The pure +-- elements must widen to the declared row at the container's construction. +effect Log + emit(Int) : Unit + +fn pure_a(x : Int) : Int ! {Log} = x + 1 + +fn pure_b(x : Int) : Int ! {Log} = x * 2 + +fn apply_all(fs : List((Int) -> Int ! {Log}), x : Int) : Int ! {Log} = + match fs of + Nil => x + Cons(f, rest) => apply_all(rest, f(x)) + +fn table() : List((Int) -> Int ! {Log}) = [pure_a, pure_b] + +fn main() = + let r = + handle apply_all(table(), 1) with + emit(_n) resume k => k(()) + return r => r + println(show(r)) diff --git a/tests/cases/run/evidence_residual_row_after_handle.pr b/tests/cases/run/evidence_residual_row_after_handle.pr new file mode 100644 index 00000000..1ef558c4 --- /dev/null +++ b/tests/cases/run/evidence_residual_row_after_handle.pr @@ -0,0 +1,24 @@ +-- The residual-row rewrite belongs to the whole planned function, not only a +-- handler arm: the same effect-polymorphic call after the handle must carry the +-- ambient witness too. + +effect Tick + bump(Int) : Unit + +fn apply_poly(f : (Int, Int) -> Int ! {| e}, a : Int, b : Int) : Int ! { | e} = + f(a, b) + +fn add(a : Int, b : Int) : Int = a + b + +fn run(action : () -> a ! {Tick | e}) : (a, Int) = + var total := 0 + let r = + handle action() with + bump(n) resume k => + total := total + n + k(()) + return r => r + let final_total = apply_poly(add, total, 0) + (r, final_total) + +fn main() = print(show(run(\() -> bump(3)))) diff --git a/tests/cases/run/evidence_residual_row_clause.pr b/tests/cases/run/evidence_residual_row_clause.pr new file mode 100644 index 00000000..c439b6e5 --- /dev/null +++ b/tests/cases/run/evidence_residual_row_clause.pr @@ -0,0 +1,23 @@ +-- An unchanged effect-polymorphic call inside a handler clause carries the +-- enclosing function's residual-row witness. Evidence lowering replaces that +-- source row with its ambient row through the whole body before threading. + +effect Tick + bump(Int) : Unit + +fn apply_poly(f : (Int, Int) -> Int ! {| e}, a : Int, b : Int) : Int ! { | e} = + f(a, b) + +fn add(a : Int, b : Int) : Int = a + b + +fn run(action : () -> a ! {Tick | e}) : (a, Int) = + var total := 0 + let r = + handle action() with + bump(n) resume k => + total := apply_poly(add, total, n) + k(()) + return r => r + (r, total) + +fn main() = print(show(run(\() -> bump(3)))) diff --git a/tests/cases/run/field_projection_single.pr b/tests/cases/run/field_projection_single.pr new file mode 100644 index 00000000..a6c4a538 --- /dev/null +++ b/tests/cases/run/field_projection_single.pr @@ -0,0 +1,8 @@ +-- Plain field projection is total on a single-constructor record. Keep this in +-- the runnable corpus so interpreter/native parity exercises the recorded +-- constructor, field index, and arity together. +type Box = Box { value: Int } + +fn value(box : Box) : Int = box.value + +fn main() = println(value(Box { value = 22 })) diff --git a/tests/cases/run/handler_implicit_return.pr b/tests/cases/run/handler_implicit_return.pr new file mode 100644 index 00000000..17a9fcf6 --- /dev/null +++ b/tests/cases/run/handler_implicit_return.pr @@ -0,0 +1,22 @@ +effect Ask + ask() : Int + +effect Put + put(Int) : Unit + +fn passes_through() = + handle put(1) with + put(v) resume w => w(()) + +fn body_value(n) = + handle n + ask() with + once ask() => 21 + +fn abandoned() = + handle ask() with + never ask() => 9 + +fn main() = + passes_through() + println(body_value(21)) + println(abandoned()) diff --git a/tests/cases/run/local_mono_effectful_helper.pr b/tests/cases/run/local_mono_effectful_helper.pr new file mode 100644 index 00000000..31035762 --- /dev/null +++ b/tests/cases/run/local_mono_effectful_helper.pr @@ -0,0 +1,26 @@ +-- The confined region must grow past the function holding the escaping +-- closures: `sample` performs the two-argument `measure` op but never appears +-- in a handler or a closure list itself, so it is pulled into the region by +-- its latent footprint, while the pure `shared` helper (called from the region +-- and from `main`) stays outside on the fused side with the stream pipeline. +effect Gauge + measure(Int, Int) : Int + +fn run_thunks(fs, acc) = + match fs of + Nil => acc + Cons(f, rest) => run_thunks(rest, acc + f()) + +fn sample(x) = measure(x, x + 1) + +fn shared(x) = x * 7 + +fn gauged() = + let fs = [\() -> sample(shared(1)), \() -> sample(2), \() -> sample(3)] + handle run_thunks(fs, 0) with + measure(lo, hi) resume k => k(lo + hi) + return r => r + +fn main() = + println(srange(1, 30).smap(shared).ssum()) + println(gauged() + shared(1)) diff --git a/tests/cases/run/local_mono_nontail_resume.pr b/tests/cases/run/local_mono_nontail_resume.pr new file mode 100644 index 00000000..deaa8430 --- /dev/null +++ b/tests/cases/run/local_mono_nontail_resume.pr @@ -0,0 +1,25 @@ +-- A confined region whose handler does real work after the continuation +-- returns: `k(n) + n` makes the clause non-tail, so the region's free monad +-- must sequence the resumption before the addition, while the unrelated +-- stream pipeline beside it stays fused. Any tier that reordered or flattened +-- the post-resume work would print a different sum, which is exactly the +-- divergence the confinement split has to keep unobservable. +effect Peek + peek(Int) : Int + +fn run_thunks(fs, acc) = + match fs of + Nil => acc + Cons(f, rest) => run_thunks(rest, acc + f()) + +fn peeked() = + let fs = [\() -> peek(4), \() -> peek(7), \() -> peek(9)] + handle run_thunks(fs, 0) with + peek(n) resume k => k(n) + n + return r => r + +fn double(n) = n * 2 + +fn main() = + println(srange(1, 50).smap(double).ssum()) + println(peeked()) diff --git a/tests/cases/run/local_mono_state_rest.pr b/tests/cases/run/local_mono_state_rest.pr new file mode 100644 index 00000000..f1d4cca9 --- /dev/null +++ b/tests/cases/run/local_mono_state_rest.pr @@ -0,0 +1,46 @@ +-- An escaping Trace component confined to its region while the rest of the +-- program fuses at the state rung rather than the evidence rung: the State +-- handler interprets get/put by parameter passing (each clause returns a +-- function of the running state), which the evidence rung declines, so the +-- fused rest comes out of state threading. This pins the confinement split +-- where the rest's own engine is the second one tried, not the first. +effect Trace + emit(Int) : Int + +effect State + get() : Int + put(Int) : Unit + +fn run_thunks(fs, acc) = + match fs of + Nil => acc + Cons(f, rest) => run_thunks(rest, acc + f()) + +fn traced() = + let fs = [\() -> emit(1), \() -> emit(2), \() -> emit(3)] + handle run_thunks(fs, 0) with + emit(n) resume k => k(n * 2) + return r => r + +fn tick() : Int ! {State} = + let n = get() + put(n + 1) + n + +fn counter() : Int ! {State} = + tick() + tick() + tick() + get() + +fn run_counter(init) = + let f = + handle counter() with + get() resume k => \(s) -> k(s)(s) + put(s2) resume k => \(_s) -> k(())(s2) + return r => \(_s) -> r + f(init) + +fn main() = + println(run_counter(0)) + println(traced()) diff --git a/tests/cases/run/local_mono_two_entries.pr b/tests/cases/run/local_mono_two_entries.pr new file mode 100644 index 00000000..a201b08f --- /dev/null +++ b/tests/cases/run/local_mono_two_entries.pr @@ -0,0 +1,35 @@ +-- Two escaping components with disjoint effects share one confined region with +-- two boundary entries. Each part stores effect-performing closures in a list +-- and drains it through the shared `run_thunks` driver, so the region is the +-- union of both parts plus the driver, and `main` crosses the boundary three +-- times while the stream pipeline beside the crossings stays fused. +effect SigA + siga(Int) : Int + +effect SigB + sigb(Int) : Int + +fn run_thunks(fs, acc) = + match fs of + Nil => acc + Cons(f, rest) => run_thunks(rest, acc + f()) + +fn part_a() = + let fs = [\() -> siga(10), \() -> siga(20)] + handle run_thunks(fs, 0) with + siga(n) resume k => k(n + 1) + return r => r + +fn part_b() = + let fs = [\() -> sigb(5), \() -> sigb(6)] + handle run_thunks(fs, 1) with + sigb(n) resume k => k(n * 3) + return r => r + +fn triple(n) = n * 3 + +fn main() = + println(srange(1, 40).smap(triple).ssum()) + println(part_a()) + println(part_b()) + println(part_a() + part_b()) diff --git a/tests/cases/run/record_pattern_rest.pr b/tests/cases/run/record_pattern_rest.pr new file mode 100644 index 00000000..52df7a71 --- /dev/null +++ b/tests/cases/run/record_pattern_rest.pr @@ -0,0 +1,28 @@ +-- A spread-only record pattern matches the constructor and ignores every field. +-- The same field name may have a different type in each variant because a match +-- refines the constructor before binding it. +type Shape = Circle { radius: Int } | Square { radius: Float } + +fn kind(shape : Shape) : Int = + match shape of + Circle { .. } => 1 + Square { .. } => 2 + +fn radius_text(shape : Shape) : String = + match shape of + Circle { radius = radius } => show(radius) + Square { radius = radius } => show(radius) + +-- An or-pattern checks each refined constructor arm independently. The shared +-- binder may therefore have the constructor-local field type in each arm. +fn radius_text_or(shape : Shape) : String = + match shape of + Circle { radius = radius } | Square { radius = radius } => show(radius) + +fn main() = + println(kind(Circle { radius = 7 })) + println(kind(Square { radius = 2.5 })) + println(radius_text(Circle { radius = 7 })) + println(radius_text(Square { radius = 2.5 })) + println(radius_text_or(Circle { radius = 9 })) + println(radius_text_or(Square { radius = 3.5 })) diff --git a/tests/cases/run/row_widen_named_effect_list.pr b/tests/cases/run/row_widen_named_effect_list.pr new file mode 100644 index 00000000..5242ff70 --- /dev/null +++ b/tests/cases/run/row_widen_named_effect_list.pr @@ -0,0 +1,29 @@ +-- A named function may promise a wider latent row than its body performs. Two +-- occurrences in one invariant container used to leave the first producer at +-- its pure body witness while the shared list element was widened to `Log`, so +-- typed-Core verification rejected the bind assembled for the list literal. + +effect Log + emit(Int) : Unit + +fn quiet(x : Int) : Int ! {Log} = x * 2 + +fn apply_all(fs : List((Int) -> Int ! {Log}), x : Int) : Int ! {Log} = + match fs of + Nil => x + Cons(f, rest) => apply_all(rest, f(x)) + +fn one_item() : List((Int) -> Int ! {Log}) = [quiet] + +fn table() : List((Int) -> Int ! {Log}) = [quiet, quiet] + +fn main() = + let once = + handle apply_all(one_item(), 1) with + emit(_n) resume k => k(()) + return value => value + let twice = + handle apply_all(table(), 1) with + emit(_n) resume k => k(()) + return value => value + println(show((once, twice))) diff --git a/tests/cases/run/shadowed_field_binder.pr b/tests/cases/run/shadowed_field_binder.pr new file mode 100644 index 00000000..aa41e9e7 --- /dev/null +++ b/tests/cases/run/shadowed_field_binder.pr @@ -0,0 +1,32 @@ +-- A field binder may reuse the name of the value being matched. The binder +-- hides that value for the rest of the arm, so nothing in the arm can name it, +-- and reference counting still has to release it exactly once. `walk` and +-- `walk_renamed` differ only in whether the second field reuses the name, so +-- they must agree on both the answer and the live-cell count at exit. +type Tree = Leaf(Int) | Node(List(Tree), Tree) + +fn walk(t : Tree) : Int = + match t of + Leaf(n) => n + Node(kids, t) => walk_all(kids) + walk(t) + +fn walk_renamed(t : Tree) : Int = + match t of + Leaf(n) => n + Node(kids, last) => walk_all_renamed(kids) + walk_renamed(last) + +fn walk_all(ts : List(Tree)) : Int = + match ts of + Nil => 0 + Cons(x, rest) => walk(x) + walk_all(rest) + +fn walk_all_renamed(ts : List(Tree)) : Int = + match ts of + Nil => 0 + Cons(x, rest) => walk_renamed(x) + walk_all_renamed(rest) + +fn sample() : Tree = Node([Leaf(1), Node([Leaf(2)], Leaf(3))], Leaf(4)) + +fn main() = + println(show_int(walk(sample()))) + println(show_int(walk_renamed(sample()))) diff --git a/tests/cases/run/str_view.pr b/tests/cases/run/str_view.pr new file mode 100644 index 00000000..4278884c --- /dev/null +++ b/tests/cases/run/str_view.pr @@ -0,0 +1,63 @@ +-- The observable contract of a byte-indexed string window, which is the same on +-- both tiers whether the window shares the parent's bytes or copies them. What is +-- pinned here: endpoints are clamped to the string and an empty or reversed span +-- is the empty string; a window onto a window addresses the original; a span that +-- splits a multi-byte character is repaired rather than rejected, so the result is +-- always well-formed; and a window is an ordinary String everywhere downstream, +-- including the operations that read to the end of a value. + +import Data.String (str_slice, trim, index_of, to_upper) + +fn show_case(label : String, got : String) : Unit ! {IO} = + println("{label}: [{got}] len={byte_len(got)}") + +-- A window onto a window onto a window: the composition addresses the original +-- string, so re-slicing a remainder never deepens the chain. +fn nest(s : String) : String = + str_slice(str_slice(str_slice(s, 2, 20), 1, 9), 2, 6) + +-- Peel one byte at a time off the front, reassembling what falls off. Every step +-- takes a window onto the whole remainder, which is where a chain would grow. +fn eat(s : String, acc : String) : String = + if byte_len(s) == 0 then + acc + else + eat(str_slice(s, 1, byte_len(s)), concat(acc, str_slice(s, 0, 1))) + +fn main() : Unit ! {IO} = + let base = "the quick brown fox jumps over the lazy dog" + show_case("whole", str_slice(base, 0, byte_len(base))) + show_case("mid", str_slice(base, 4, 9)) + show_case("empty", str_slice(base, 5, 5)) + show_case("reversed", str_slice(base, 9, 4)) + show_case("clamped_hi", str_slice(base, 36, 400)) + show_case("clamped_lo", str_slice(base, -7, 3)) + show_case("both_out", str_slice(base, 400, 900)) + show_case("nested", nest(base)) + show_case("eaten", eat(str_slice(base, 4, 15), "")) + -- A span that cuts into a character is repaired, and the repair is the same + -- replacement the lossy decoder writes anywhere else. + let uni = "naive cafe: ünïcode" + show_case("uni_aligned", str_slice(uni, 0, 5)) + show_case("uni_split_hi", str_slice(uni, 0, 13)) + show_case("uni_split_lo", str_slice(uni, 13, 18)) + -- Windows flowing through the ordinary string operations, including the two + -- that parse to the end of their input. + show_case("view_trim", trim(str_slice(base, 3, 10))) + show_case( + "view_concat", + concat(concat(str_slice(base, 0, 3), "/"), str_slice(base, 4, 9)), + ) + show_case("view_eq", show(str_slice(base, 4, 9) == "quick")) + show_case("view_cmp", show(str_cmp(str_slice(base, 4, 9), "quick"))) + show_case("view_upper", to_upper(str_slice(base, 4, 9))) + show_case("view_index", show(index_of("brown", str_slice(base, 4, 20)))) + show_case("view_char_at", show(char_at(str_slice(uni, 12, 18), 1))) + show_case("view_len", show(str_len(str_slice(uni, 12, 18)))) + show_case("view_sub", substring(str_slice(uni, 12, 18), 1, 2)) + show_case( + "view_parse_float", + show(parse_float(str_slice("3.14159xyz", 0, 4))), + ) + show_case("view_parse_cut", show(parse_float(str_slice("3.14159", 0, 3)))) + show_case("view_parse_int", show(parse_int(str_slice("12345abc", 0, 3)))) diff --git a/tests/cases/run/wire_hostile.pr b/tests/cases/run/wire_hostile.pr index 21033e41..01981942 100644 --- a/tests/cases/run/wire_hostile.pr +++ b/tests/cases/run/wire_hostile.pr @@ -1,8 +1,9 @@ -- Hand-crafted hostile frames and bodies: every one must fail through `Fail` and -- be caught, never trap or hang. Covers the frame header checks (a foreign scheme -- tag, a wrong kind, a wrong contract digest), a body truncated mid-value, a --- length prefix larger than the bytes that follow it, and a map body whose keys --- arrive out of canonical order. Each hostile case is paired with a well-formed +-- length prefix larger than the bytes that follow it, a map body whose keys +-- arrive out of canonical order, and an element varint whose continuation run +-- overruns the byte cap. Each hostile case is paired with a well-formed -- control that must be accepted, so the rejection is never vacuous. Runs under the -- parity + leak oracles: native and interpreter must agree byte-for-byte. -- @@ -28,6 +29,8 @@ fn dec_str(bs : Bytes) : (String, Bytes) = decode(bs) fn dec_map_ii(bs : Bytes) : (Map(Int, Int), Bytes) = decode(bs) +fn dec_by(bs : Bytes) : (Bytes, Bytes) = decode(bs) + fn dec_val(bs : Bytes) : Int = wire_decode_value_with_digest(bs, dig) -- Keep the first `k` bytes of a body, so a valid frame can be cut mid-value. @@ -103,6 +106,37 @@ fn map_ordering() : Bool = match dec_map_ii(body) of _ => false +-- A byte body is a count followed by that many varints. Well-formed elements are +-- one or two bytes, so only a hand-crafted body reaches the varint byte cap at +-- all: `varint_body(n)` writes a single element as `n` continuation bytes (high +-- bit set, no payload) followed by a terminator. +fn varint_run(n : Int) : List(Int) = + if n <= 0 then + [0] + else + Cons(128, varint_run(n - 1)) + +fn varint_body(n : Int) : Bytes = bytes_of_list(Cons(1, varint_run(n))) + +-- Nine continuations plus a terminator is ten bytes, exactly the cap, so this is +-- the widest element a body may legally carry. +fn varint_cap_control() : Bool = + caught(false) fn + match dec_by(varint_body(9)) of + (out, _r) => wire_len(out) == 1 + +-- One byte past the cap, and a long run well past it: both must be refused where +-- the cap falls rather than scanned to the end of the input. +fn varint_overlong() : Bool = + caught(true) fn + match dec_by(varint_body(10)) of + _ => false + +fn varint_long_run() : Bool = + caught(true) fn + match dec_by(varint_body(64)) of + _ => false + fn report(name : String, ok : Bool) = println(concat(name, if ok then ": OK" else ": FAIL")) @@ -115,3 +149,6 @@ fn main() = report("truncated body rejected", truncated_body()) report("hostile length rejected", hostile_length()) report("descending map rejected", map_ordering()) + report("varint at cap accepted", varint_cap_control()) + report("overlong varint rejected", varint_overlong()) + report("long continuation run rejected", varint_long_run()) diff --git a/tests/cli_docs/determinism_machine.rs b/tests/cli_docs/determinism_machine.rs index d0f3b269..9545e002 100644 --- a/tests/cli_docs/determinism_machine.rs +++ b/tests/cli_docs/determinism_machine.rs @@ -155,8 +155,8 @@ fn gallery_and_vite_wire_every_resident() { if resident.gallery { assert_contains(&gallery, &gallery_link, resident.slug); } else { - // Unlisted is a decision, not an accident: the card must be absent, - // not merely unnoticed, so relisting is a deliberate flip here. + // Unlisted is explicit: the card must be absent, so relisting changes + // this assertion deliberately. assert!( !gallery.contains(&gallery_link), "{} is marked unlisted but the gallery links it", diff --git a/tests/cli_docs/time_compile.rs b/tests/cli_docs/time_compile.rs index 3855aaa2..c1b265d9 100644 --- a/tests/cli_docs/time_compile.rs +++ b/tests/cli_docs/time_compile.rs @@ -218,7 +218,15 @@ fn native_link_reports_direct_cc_work_and_reuses_runtime_objects() { let first_compile = count_field(first_link, "cc_compile_invocations"); let first_links = count_field(first_link, "cc_link_invocations"); let first_runtime_misses = count_field(first_link, "runtime_object_misses"); - assert_eq!(first_probe, 1, "one toolchain-version probe is cold"); + // Not an exact count: how many components a link resolves is a property of + // the platform, not of this program. Apple platforms take their SDK linker + // without asking, while elsewhere prism resolves the linker itself so the + // ThinLTO link cannot inherit whichever one happens to own `ld`. What every + // platform owes is that the probes happened and are accounted for below. + assert!( + first_probe >= 1, + "a cold process must probe the toolchain before linking" + ); assert!(first_compile > 0, "native build must compile an object"); assert_eq!(first_links, 1, "native build must invoke one final link"); assert!( diff --git a/tests/compiler.rs b/tests/compiler.rs index 1dbc90ce..b1ae2b19 100644 --- a/tests/compiler.rs +++ b/tests/compiler.rs @@ -25,6 +25,8 @@ mod alloc_certificate; mod arena; #[path = "compiler/base_surface.rs"] mod base_surface; +#[path = "compiler/convention_split.rs"] +mod convention_split; #[path = "compiler/core_identity.rs"] mod core_identity; #[path = "compiler/cursor.rs"] @@ -85,5 +87,7 @@ mod syntax_lex; mod syntax_roundtrip; #[path = "compiler/tc_input_codec.rs"] mod tc_input_codec; +#[path = "compiler/tc_rejection.rs"] +mod tc_rejection; #[path = "compiler/typespans.rs"] mod typespans; diff --git a/tests/compiler/arena.rs b/tests/compiler/arena.rs index 5bef974b..e5329df4 100644 --- a/tests/compiler/arena.rs +++ b/tests/compiler/arena.rs @@ -37,6 +37,20 @@ fn shared_function_is_not_reified() { ); } +/// An arena scope that builds through a let-bound closure lowers like one that +/// calls a named function. The rewrite widens the lambda, the thunk suspending +/// it, and the binder holding that thunk; the references reading the binder have +/// to widen with it, or the verifier rejects a reference claiming a purity its +/// binder no longer has and the whole program fails to lower. +#[test] +fn arena_closure_binding_is_reified() { + let out = lowered("examples/fixtures/compiler/arena_closure.pr"); + assert!( + out.contains("init_at"), + "an arena-only constructor behind a let-bound closure was not reified:\n{out}" + ); +} + /// Every reified program carries the region bracket: `arena_enter` before the /// installer's handler activation and `arena_exit` threading its token and /// result. Without the bracket, reified `alloc`s would fall to the delegating diff --git a/tests/compiler/convention_split.rs b/tests/compiler/convention_split.rs new file mode 100644 index 00000000..7d99fc12 --- /dev/null +++ b/tests/compiler/convention_split.rs @@ -0,0 +1,160 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use prism::core::typed::effect_lower::analysis::{self, MonadicScope}; +use prism::core::typed::effect_lower::plan::collect_calls; +use prism::core::typed::effect_lower::{lower_effects, prepare, EffectPlan}; +use prism::core::typed::verify::VerifyEnv; +use prism::core::typed::{Elaborated, TypedCore, TypedCoreFn}; +use prism::core::{EffectStrategy, OpGrades}; +use prism::flags::DynFlags; +use prism::types::CtorInfo; +use prism_common::sym::Sym; + +const PURE: &str = include_str!("../fixtures/tier_cross/convention_split_map_pure.pr"); +const MIXED: &str = include_str!("../fixtures/tier_cross/convention_split_map.pr"); +const UNROLLED: &str = include_str!("../fixtures/tier_cross/convention_split_map_unrolled.pr"); + +fn typed_from_program( + source: &str, +) -> ( + TypedCore, + VerifyEnv, + BTreeMap, + OpGrades, +) { + let source = prism::driver::with_prelude(source); + let parsed = prism_syntax::parse::parse(&source) + .expect("fixture parses") + .program; + let roots = [prism::resolve::Root::Embedded(prism::stdlib::STDLIB)]; + let resolved = prism::resolve::resolve_modules_in(parsed, &roots).expect("fixture resolves"); + let program = prism::syntax::desugar::desugar(resolved).expect("fixture desugars"); + let checked = prism::types::check(&program).expect("fixture typechecks"); + let grades = checked.op_grades(); + let ctors = checked.ctors.clone(); + let elaboration = prism::core::elaborate_typed(&program, &checked).expect("fixture elaborates"); + let (_compat, typed, env) = elaboration.into_parts(); + (typed, env, ctors, grades) +} + +fn calls(function: &TypedCoreFn) -> BTreeSet { + let mut calls = BTreeSet::new(); + collect_calls(function.body(), &mut calls); + calls +} + +#[test] +fn mixed_direct_and_effectful_map_keeps_the_pure_clone_direct() { + let (typed, env, ctors, grades) = typed_from_program(MIXED); + let flags = DynFlags::default(); + let prepared = prepare(typed.clone(), &env, &ctors, &flags, &grades) + .expect("convention preparation succeeds"); + let clone = prepared + .fns + .iter() + .find(|function| function.name().as_str().starts_with("Data.List.map$ec")) + .expect("the mixed demand materializes a map convention clone"); + let clone_name = clone.name(); + let clone_sig = clone.sig().clone(); + let original = Sym::new("Data.List.map"); + let pure_use = prepared + .fns + .iter() + .find(|function| function.name().as_str() == "pure_use") + .expect("pure caller remains reachable"); + let effect_use = prepared + .fns + .iter() + .find(|function| function.name().as_str() == "effect_use") + .expect("effectful caller remains reachable"); + + assert!(calls(pure_use).contains(&clone_name)); + assert!(!calls(pure_use).contains(&original)); + assert!(calls(effect_use).contains(&original)); + assert!(!calls(effect_use).contains(&clone_name)); + assert!(calls(clone).contains(&clone_name)); + assert!(!calls(clone).contains(&original)); + + let effects = EffectPlan::analyze(&prepared.fns); + let region = analysis::plan(&prepared.fns, &effects, false); + assert_eq!(region.scope, MonadicScope::Selective); + assert!(region.members.contains(&original)); + assert!(region.members.contains(&effect_use.name())); + assert!(!region.members.contains(&clone_name)); + assert!(!region.members.contains(&pure_use.name())); + assert_eq!( + region.monadic_params.get(&original), + Some(&BTreeSet::from([0])) + ); + assert!(!region.monadic_params.contains_key(&clone_name)); + + let lowered = lower_effects(typed, &env, &ctors, &flags, &grades) + .expect("the convention-split program lowers"); + assert_ne!(lowered.strategy, EffectStrategy::WholeProgramFreeMonad); + assert_eq!( + lowered + .core + .functions() + .iter() + .find(|function| function.name() == clone_name) + .expect("the direct clone survives lowering") + .sig(), + &clone_sig, + "the pure clone never crosses a monadic convention boundary" + ); + + let run = prism::interpret(&prism::driver::with_prelude(MIXED)) + .expect("the reference interpreter accepts the same fixture"); + assert_eq!(run.term, "[2, 3, 4][8, 10]"); +} + +#[test] +fn pure_and_unrolled_controls_pin_the_expected_tiers_and_outputs() { + for (name, source, strategy, output) in [ + ("pure", PURE, EffectStrategy::Pure, "[2, 3, 4]"), + ( + "unrolled", + UNROLLED, + EffectStrategy::Evidence, + "[2, 3, 4][8, 10]", + ), + ] { + let (typed, env, ctors, grades) = typed_from_program(source); + let lowered = lower_effects(typed, &env, &ctors, &DynFlags::default(), &grades) + .unwrap_or_else(|error| panic!("{name} control lowers: {error}")); + assert_eq!(lowered.strategy, strategy, "{name} control tier"); + + let run = prism::interpret(&prism::driver::with_prelude(source)) + .unwrap_or_else(|error| panic!("{name} control interprets: {error}")); + assert_eq!(run.term, output, "{name} control output"); + } +} + +#[test] +fn a_single_effectful_map_demand_keeps_the_original_symbol() { + let source = r#" +effect Log + emit(Int) : Unit + +fn shout(x : Int) : Int ! {Log} = + let _u = emit(x) + x * 2 + +fn main() = + let _handled = + handle map(shout, [1]) with + emit(_n) resume k => k(()) + return r => r + print("ok") +"#; + let (typed, env, ctors, grades) = typed_from_program(source); + let prepared = prepare(typed, &env, &ctors, &DynFlags::default(), &grades) + .expect("single-convention preparation succeeds"); + assert!( + prepared + .fns + .iter() + .all(|function| !function.name().as_str().contains("$ec")), + "one known convention does not need a clone" + ); +} diff --git a/tests/compiler/effect_lower.rs b/tests/compiler/effect_lower.rs index 264058ed..3686ee9e 100644 --- a/tests/compiler/effect_lower.rs +++ b/tests/compiler/effect_lower.rs @@ -27,7 +27,9 @@ use prism::core::typed::*; use prism::core::EffectStrategy::{ Evidence, LocalPartial, Pure, SelectiveFreeMonad, StateFusion, WholeProgramFreeMonad, }; -use prism::core::{EffectStrategy, OpGrades}; +use prism::core::{ + audit_typed_core, verify_typed_core, EffectStrategy, OpGrades, UncheckedTypedCore, +}; use prism::flags::DynFlags; use prism::types::ty::EffRow; use prism::types::CtorInfo; @@ -214,7 +216,8 @@ fn assert_lowering( env: &VerifyEnv, ctors: &BTreeMap, ) -> TypedLowering { - let input = TypedCore::new(functions); + let input = verify_typed_core(UncheckedTypedCore::new(functions), env) + .unwrap_or_else(|violations| panic!("input fixture is invalid: {violations:#?}")); assert_typed_lowering(input, env, ctors, &DynFlags::default(), &OpGrades::new()) } @@ -225,11 +228,11 @@ fn assert_typed_lowering( flags: &DynFlags, grades: &OpGrades, ) -> TypedLowering { - if let Err(violations) = verify(&input, env) { + if let Err(violations) = audit_typed_core(&input, env) { panic!("input fixture is invalid: {violations:#?}"); } let out = lower_effects(input, env, ctors, flags, grades).expect("typed lowering succeeds"); - if let Err(violations) = verify(&out.core, &out.env) { + if let Err(violations) = audit_typed_core(&out.core, &out.env) { panic!("lowered typed Core is invalid: {violations:#?}"); } prism::core::residual_effects(&out.core.clone().erase()) @@ -375,8 +378,13 @@ fn every_effect_strategy_and_lowering_flag_boundary_is_accounted_for() { ], }, Fixture { + // The bottom rung needs a program every partial lowering must + // decline: a declared-effectful callback hidden in a list is + // opaque to the flow analysis at every knob position. A merely + // effect-polymorphic caller no longer qualifies, because the + // builder records exact stored witnesses and the region confines. name: "whole", - source: include_str!("../../examples/eff_poly.pr"), + source: include_str!("../cases/run/row_widen_named_effect_list.pr"), expected: [ WholeProgramFreeMonad, WholeProgramFreeMonad, @@ -554,12 +562,12 @@ fn assert_compiled_lowering( ), ) -> TypedLowering { let (typed, env, ctors, grades) = compiled; - if let Err(violations) = verify(&typed, &env) { + if let Err(violations) = audit_typed_core(&typed, &env) { panic!("compiled fixture is invalid: {violations:#?}"); } let flags = DynFlags::default(); let out = lower_effects(typed, &env, &ctors, &flags, &grades).expect("typed lowering succeeds"); - if let Err(violations) = verify(&out.core, &out.env) { + if let Err(violations) = audit_typed_core(&out.core, &out.env) { panic!("lowered typed Core is invalid: {violations:#?}"); } prism::core::residual_effects(&out.core.clone().erase()) @@ -648,6 +656,54 @@ fn tail_resumptive_handler_lowers_by_evidence() { assert_eq!(out.strategy, EffectStrategy::Evidence); } +// The evidence signature prepass replaces `run`'s source residual row with a +// fresh ambient row. An unchanged polymorphic call retains that row in its +// explicit Core instantiation, both inside a handler clause and after the +// handle, so the substitution has to cover the entire typed body before the +// threading rewrite. Both programs also compile from a lower ladder start; +// the default evidence result and the forced fallback must independently +// verify. +#[test] +fn evidence_rewrites_residual_rows_through_the_whole_body() { + let fixtures = [ + include_str!("../cases/run/evidence_residual_row_clause.pr"), + include_str!("../cases/run/evidence_residual_row_after_handle.pr"), + ]; + for src in fixtures { + let (typed, env, ctors, grades) = typed_from_program(src); + let evidence = + assert_typed_lowering(typed.clone(), &env, &ctors, &DynFlags::default(), &grades); + assert_eq!(evidence.strategy, EffectStrategy::Evidence); + + let state_flags = DynFlags { + effect_tier: EffectTier::StateFusion, + ..DynFlags::default() + }; + let state = assert_typed_lowering(typed, &env, &ctors, &state_flags, &grades); + assert_eq!(state.strategy, EffectStrategy::SelectiveFreeMonad); + } +} + +// A callback stored in data is recovered through a pattern, so the flow plan +// cannot attach one exact runtime convention to it. Even when every concrete +// callback is pure, the stored effectful witness is authoritative after that +// extraction. The cascade must recognize the opacity before a selective rung +// commits and route both the one- and two-element forms through the uniform +// whole-program convention. +#[test] +fn declared_effectful_callbacks_hidden_in_data_route_whole() { + let src = include_str!("../cases/run/row_widen_named_effect_list.pr"); + for effect_tier in [EffectTier::Auto, EffectTier::StateFusion] { + let (typed, env, ctors, grades) = typed_from_program(src); + let flags = DynFlags { + effect_tier, + ..DynFlags::default() + }; + let lowered = assert_typed_lowering(typed, &env, &ctors, &flags, &grades); + assert_eq!(lowered.strategy, EffectStrategy::WholeProgramFreeMonad); + } +} + // A stream producer returns an effectful thunk rather than performing in // the producer call itself. The signature plan must widen that returned // thunk from `flow.ret`, then carry the new witness through map/filter @@ -710,7 +766,7 @@ fn arena_preparation_rewrites_constructors_and_verifies() { arena::insert_builtin_sigs(&mut env); let before = typed.clone().erase(); let prepared = arena::prepare(typed.functions().to_vec(), &env).expect("preparation"); - assert_eq!(verify(&prepared, &env), Ok(())); + assert_eq!(audit_typed_core(&prepared, &env), Ok(())); assert!( prepared .functions() @@ -753,7 +809,7 @@ fn threading_eff_state_verifies_and_eliminates_effects() { let (threaded, threaded_env) = threaded_state_typed(typed, &env, &ctors, &flags, &grades) .expect("the typed cascade classifies") .expect("and the state engine threads this program"); - assert_eq!(verify(&threaded, &threaded_env), Ok(())); + assert_eq!(audit_typed_core(&threaded, &threaded_env), Ok(())); prism::core::residual_effects(&threaded.erase()).expect("no raw effects survive"); } @@ -839,7 +895,7 @@ fn production_state_corpus_routes_and_eliminates_effects() { .unwrap_or_else(|e| panic!("{path}: the typed production rung fails: {e:?}")); assert_eq!(threaded.strategy, EffectStrategy::StateFusion); assert_eq!( - verify(&threaded.core, &threaded.env), + audit_typed_core(&threaded.core, &threaded.env), Ok(()), "{path}: typed State output must verify" ); @@ -874,7 +930,7 @@ fn threaded_state_corpus_verifies() { let (threaded, env2) = threaded_state_typed(typed, &env, &ctors, &flags, &grades) .unwrap_or_else(|e| panic!("{path}: cascade fails: {e:?}")) .unwrap_or_else(|| panic!("{path}: declines")); - if let Err(violations) = verify(&threaded, &env2) { + if let Err(violations) = audit_typed_core(&threaded, &env2) { failures.push(format!( "{path}: {} violations, first three: {:#?}", violations.len(), @@ -895,7 +951,7 @@ fn threaded_state_bind_rows_cover_transformed_children() { threaded_state_typed(typed, &env, &ctors, &DynFlags::default(), &grades) .unwrap_or_else(|e| panic!("{path}: cascade fails: {e:?}")) .unwrap_or_else(|| panic!("{path}: state engine declines")); - if let Err(violations) = verify(&threaded, &env2) { + if let Err(violations) = audit_typed_core(&threaded, &env2) { panic!("{path}: transformed Bind hides child effects: {violations:#?}"); } } @@ -909,7 +965,7 @@ fn assert_threading_verifies(src: &str) { let (threaded, threaded_env) = threaded_state_typed(typed, &env, &ctors, &flags, &grades) .expect("the typed cascade classifies") .expect("and the state engine threads this program"); - assert_eq!(verify(&threaded, &threaded_env), Ok(())); + assert_eq!(audit_typed_core(&threaded, &threaded_env), Ok(())); prism::core::residual_effects(&threaded.erase()).expect("no raw effects survive"); } @@ -959,10 +1015,13 @@ fn native_function_answer_region_matches_the_typed_production_route() { .expect("native function-answer region lowers"); lowered.push(abi::ebind_fn()); lowered.push(abi::qapply_fn()); - let typed = TypedCore::::new(lowered); let mut lowered_env = prepared.env.clone(); abi::insert(&mut lowered_env); - assert_eq!(verify(&typed, &lowered_env), Ok(())); + let typed = verify_typed_core( + UncheckedTypedCore::::new(lowered), + &lowered_env, + ) + .expect("native function-answer lowering must mint effect-lowered authority"); assert_eq!(typed.erase(), out.core.clone().erase()); let off_flags = DynFlags { @@ -997,8 +1056,11 @@ fn native_function_answer_region_matches_the_typed_production_route() { off_functions.push(abi::qapply_fn()); let mut off_env = prepared.env; abi::insert(&mut off_env); - let off = TypedCore::::new(off_functions); - assert_eq!(verify(&off, &off_env), Ok(())); + let off = verify_typed_core( + UncheckedTypedCore::::new(off_functions), + &off_env, + ) + .expect("non-native fallback must mint effect-lowered authority"); assert_eq!(off.erase(), off_out.core.erase()); } @@ -1068,8 +1130,7 @@ fn whole_program_trampoline_is_deterministic_and_verifies() { .expect("second typed isolated trampoline lowering"); second.push(trampoline::prism_drive_fn()); assert_eq!( - TypedCore::::new(first).erase().fns, - TypedCore::::new(second).erase().fns, + first, second, "the transform must be deterministic for the same fresh-name state" ); @@ -1078,8 +1139,11 @@ fn whole_program_trampoline_is_deterministic_and_verifies() { let mut lowered_env = prepared.env; abi::insert(&mut lowered_env); - let typed = TypedCore::::new(lowered); - assert_eq!(verify(&typed, &lowered_env), Ok(())); + let typed = verify_typed_core( + UncheckedTypedCore::::new(lowered), + &lowered_env, + ) + .expect("trampoline lowering must mint effect-lowered authority"); assert_eq!(typed.erase(), out.core.erase()); // The control asks for the free-monad rung without pinning its scope, so @@ -1271,7 +1335,7 @@ fn main() : Int ! {} = } "; let (typed, env, ctors, grades) = typed_from_source(src); - if let Err(violations) = verify(&typed, &env) { + if let Err(violations) = audit_typed_core(&typed, &env) { panic!("compiled fixture is invalid: {violations:#?}"); } let flags = DynFlags::default(); @@ -1495,7 +1559,7 @@ fn main() = println(logged() + answered()) let (typed, env, ctors, grades) = typed_from_program(src); let out = assert_typed_lowering(typed, &env, &ctors, &DynFlags::default(), &grades); assert_eq!(out.strategy, EffectStrategy::LocalPartial); - assert_eq!(verify(&out.core, &out.env), Ok(())); + assert_eq!(audit_typed_core(&out.core, &out.env), Ok(())); } #[test] @@ -1538,7 +1602,7 @@ fn local_partial_composes_fused_rest_and_monadic_region_exactly() { ))), "the State producer's evidence row keeps the same global hole" ); - assert_eq!(verify(&combined, &lowered_env), Ok(())); + assert_eq!(audit_typed_core(&combined, &lowered_env), Ok(())); prism::core::residual_effects(&combined.erase()).expect("no raw effects survive"); } @@ -1572,7 +1636,7 @@ fn main() = assert!(region.contains(&sym("run_pair"))); assert!(region.contains(&sym("logged"))); assert_eq!(entries, BTreeSet::from([sym("logged")])); - assert_eq!(verify(&combined, &lowered_env), Ok(())); + assert_eq!(audit_typed_core(&combined, &lowered_env), Ok(())); prism::core::residual_effects(&combined.erase()).expect("no raw effects survive"); } @@ -1599,7 +1663,7 @@ fn local_partial_region_retains_its_direct_io_row_exactly() { .effects() .label_names() .contains(&Sym::from(prism_syntax::names::IO_EFFECT))); - assert_eq!(verify(&combined, &lowered_env), Ok(())); + assert_eq!(audit_typed_core(&combined, &lowered_env), Ok(())); prism::core::residual_effects(&combined.erase()).expect("no raw effects survive"); } @@ -1667,7 +1731,11 @@ fn local_partial_composition( ) .expect("LocalPartial assembly is total after planning") .expect("the LocalPartial whole-style boundary is sound"); - let combined = TypedCore::::new(artifacts.fns); + let combined = verify_typed_core( + UncheckedTypedCore::::new(artifacts.fns), + &artifacts.env, + ) + .expect("LocalPartial assembly must mint effect-lowered authority"); (combined, artifacts.env, region, entries) } @@ -1707,7 +1775,7 @@ fn typed_local_decline_digests(point: LocalDeclinePoint) -> (String, String) { assert_eq!(probed.warning, clean.warning); for lowering in [&probed, &clean] { - assert_eq!(verify(&lowering.core, &lowering.env), Ok(())); + assert_eq!(audit_typed_core(&lowering.core, &lowering.env), Ok(())); prism::core::residual_effects(&lowering.core.clone().erase()) .expect("typed fallback leaves no raw effects"); assert!(lowering @@ -1735,9 +1803,8 @@ fn typed_local_decline_digests(point: LocalDeclinePoint) -> (String, String) { (probed, clean) } -// These pin a blake3 of the `pp_core` dump, which prints raw fresh ids, so the -// digests are a function of the process-global `Sym` supply, not just the -// program: adding a builtin or prelude effect shifts the supply and moves them. +// These pin a blake3 of the `pp_core` dump, which prints raw fresh ids. The +// digests depend on both the program and process-global `Sym` supply. // What the tests actually guard (probed != clean) is intact; only the concrete // hex is regenerated when the supply moves. Canonical (`core-hash`) output is // unaffected, which `tests/determinism.rs` proves. diff --git a/tests/compiler/module_interface.rs b/tests/compiler/module_interface.rs index 99a248c8..c476f96c 100644 --- a/tests/compiler/module_interface.rs +++ b/tests/compiler/module_interface.rs @@ -1,6 +1,7 @@ use prism::core::Digest; +use prism::types::NominalRepr; use prism::{ - check_with_seed, module_interface, with_prelude, ModuleInterface, Root, Sym, + check_with_seed, module_interface, with_prelude, Error, ModuleInterface, Root, Sym, MODULE_INTERFACE_FORMAT, }; @@ -104,6 +105,7 @@ fn transparent_data_shape_and_constructor_facts_rehydrate() { let shape = facts.data.get("Shape").expect("exported data metadata"); assert_eq!(shape.ctors, ["Circle", "Square"]); + assert_eq!(shape.repr, NominalRepr::BoxedCell); assert_eq!(facts.ctors["Circle"].tag, FIRST_CTOR_TAG); assert_eq!(facts.ctors["Square"].tag, SECOND_CTOR_TAG); assert!(facts.env.contains_key(&Sym::from("Circle"))); @@ -130,8 +132,36 @@ fn opaque_data_rehydrates_shape_without_constructors() { ); let facts = interface.rehydrate().unwrap(); assert!(facts.data["Counter"].ctors.is_empty()); + assert_eq!(facts.data["Counter"].repr, NominalRepr::BoxedCell); assert!(!facts.ctors.contains_key("Counter")); assert!(!facts.env.contains_key(&Sym::from("Counter"))); + + check_with_seed( + "fn maybe(x : Counter) : OrNull(Counter) = This(x)\n", + &facts.typecheck_seed(), + ) + .expect("opaque ordinary data retains its boxed representation evidence"); +} + +#[test] +fn opaque_newtype_keeps_transparent_representation_evidence() { + let interface = interface( + "opaque newtype Zero = Zero(Unit)\n\ + pub fn zero() : Zero = Zero(())\n", + ); + let facts = interface.rehydrate().unwrap(); + assert!(facts.data["Zero"].ctors.is_empty()); + assert_eq!(facts.data["Zero"].repr, NominalRepr::Transparent); + + let error = check_with_seed( + "fn maybe(x : Zero) : OrNull(Zero) = This(x)\n", + &facts.typecheck_seed(), + ) + .expect_err("opacity cannot turn a transparent newtype into a boxed cell"); + let Error::Type(error) = error else { + panic!("expected a type error, got {error}"); + }; + assert_eq!(error.code(), Some("E1019")); } #[test] diff --git a/tests/compiler/module_queries.rs b/tests/compiler/module_queries.rs index 1fbf14c6..4e3a4dac 100644 --- a/tests/compiler/module_queries.rs +++ b/tests/compiler/module_queries.rs @@ -322,7 +322,14 @@ fn malformed_durable_checked_body_query_is_rejected() { cfg.flags.store_path = Some(store.clone()); check_modules_on(ROOT, &roots(BEFORE_VALUE), &cfg).unwrap(); - let query = fs::read_dir(store.join(CHECKED_BODY_QUERY_DIR)) + // Query bindings sit one shard level below the kind directory. + let shard = fs::read_dir(store.join(CHECKED_BODY_QUERY_DIR)) + .unwrap() + .filter_map(Result::ok) + .find(|entry| entry.path().is_dir()) + .expect("checked body query shard") + .path(); + let query = fs::read_dir(shard) .unwrap() .next() .expect("checked body query") diff --git a/tests/compiler/opt.rs b/tests/compiler/opt.rs index d36ee955..06f30c9b 100644 --- a/tests/compiler/opt.rs +++ b/tests/compiler/opt.rs @@ -113,7 +113,7 @@ fn core_lint_clean_on_corpus() { } // The `--passes` spec parser: a two-stage spec lands each pass in the right -// section, in order; a bare list defaults to the pre stage; and the validation +// section, in order. A bare list defaults to the pre stage. The validation // rules each reject their bad input with a message. #[test] fn pass_spec_parse() { diff --git a/tests/compiler/parser_parity.rs b/tests/compiler/parser_parity.rs index 719ed12f..3dc657af 100644 --- a/tests/compiler/parser_parity.rs +++ b/tests/compiler/parser_parity.rs @@ -8,14 +8,19 @@ // Generating the oracle each run means it tracks the tree instead of a pinned // commit, so the gate cannot certify a parser against a stale grammar. // -// Two corpora: the committed syntax fixtures, which are the same sources the -// artifact round trip already pins, and a focused file of grammar edges that -// are easy to lose in handwritten maintenance. +// Three corpora: the committed syntax fixtures, which are the same sources the +// artifact round trip already pins; a focused file of grammar edges that are +// easy to lose in handwritten maintenance; and four deterministic mutation +// matrices generated live, so a parser shaped only around fixtures cannot pass. +use std::collections::BTreeSet; use std::path::{Path, PathBuf}; -use std::{env, fs}; +use std::{env, fs, process}; -use prism::{default_roots, dump_on, interpret_io_on_with_args, with_prelude, Config, Root}; +use prism::{ + default_roots, dump_on, interpret_io_on_with_args, step_ruler_on, with_prelude, Config, Root, +}; +use serde_json::Value; use super::fixture_stems; @@ -55,31 +60,60 @@ fn roots() -> Vec { default_roots(root()) } -// Run the differential witness over one surface artifact and return its verdict -// line. `arg(1)` names where the witness dumps its own encoding on a mismatch, -// which is the artifact to diff when this fails. -fn parity(artifact: &Path, mismatch: &Path) -> String { +// Run the differential witness under an explicit search path, returning its +// output lines or the run's own failure. The search path is a parameter rather +// than a constant because which one the witness runs under is itself something +// the gate has to pin: the modules it imports are shadowable by any directory +// root ahead of the standard library. +fn run_witness(roots: &[Root], args: Vec) -> Result, String> { let src = fs::read_to_string(fixture(PARSER_FIXTURES).join(WITNESS)) .expect("differential witness source"); let full = with_prelude(&src); let mut sink = Vec::new(); - let args = vec![ - artifact.display().to_string(), - mismatch.display().to_string(), - ]; interpret_io_on_with_args( &full, - &roots(), + roots, &mut sink, &mut &b""[..], &Config::from_env(), args, ) - .unwrap_or_else(|e| panic!("{}: witness run: {e}", artifact.display())); - String::from_utf8(sink) + .map_err(|error| error.to_string())?; + Ok(String::from_utf8(sink) .expect("utf8 witness output") - .trim() - .to_string() + .lines() + .filter(|line| !line.is_empty()) + .map(str::to_string) + .collect()) +} + +// The argument vector the witness reads: one surface artifact and one mismatch +// path per comparison, in order. +fn witness_args(pairs: &[(&Path, &Path)]) -> Vec { + pairs + .iter() + .flat_map(|(artifact, mismatch)| { + [ + artifact.display().to_string(), + mismatch.display().to_string(), + ] + }) + .collect() +} + +// Run the differential witness once over surface-artifact/mismatch-path pairs. +// One verdict line is returned per pair, in argument order. +fn parity_pairs(pairs: &[(&Path, &Path)]) -> Vec { + run_witness(&roots(), witness_args(pairs)) + .unwrap_or_else(|error| panic!("differential witness run: {error}")) +} + +// Run the differential witness over one surface artifact and return its verdict +// line. The second path receives the Prism encoding on a mismatch. +fn parity(artifact: &Path, mismatch: &Path) -> String { + let mut verdicts = parity_pairs(&[(artifact, mismatch)]); + assert_eq!(verdicts.len(), 1, "one verdict for one surface artifact"); + verdicts.pop().expect("one differential verdict") } // Assert that re-parsing a source through the Prism parser reproduces the Rust @@ -143,6 +177,344 @@ fn parity_grammar_edges() { assert_parses_identically("edge_parity", &path); } +// A generated lane carries several independently keyed source mutations in one +// program. One witness invocation per lane keeps this a cheap parser gate while +// item-count and key checks prevent an empty or duplicate generator from going +// green. +struct GeneratedLane { + source: String, + cases: usize, + // How many of this lane's cases the parser is expected to tell apart once + // spans and generated names are folded away. It equals the case count for a + // lane that varies structure on every axis, and is smaller for one that also + // varies layout, because indentation is not allowed to reach the tree. + shapes: usize, +} + +fn aggregate_lane( + label: &str, + base_key: &str, + cases: Vec<(String, String)>, + expected: usize, + shapes: usize, +) -> GeneratedLane { + assert_eq!(cases.len(), expected, "{label}: mutation count drift"); + assert!(expected > 1, "{label}: mutation lane is vacuous"); + + let keys: BTreeSet<&str> = cases.iter().map(|(key, _)| key.as_str()).collect(); + assert_eq!(keys.len(), expected, "{label}: duplicate mutation key"); + assert!( + !keys.contains(base_key), + "{label}: the unmutated base leaked into the lane" + ); + let sources: BTreeSet<&str> = cases.iter().map(|(_, source)| source.as_str()).collect(); + assert_eq!( + sources.len(), + expected, + "{label}: duplicate source mutation" + ); + + assert!( + shapes > 0 && shapes <= expected, + "{label}: {shapes} distinct shapes is not a bound {expected} cases can meet" + ); + + GeneratedLane { + source: cases.into_iter().map(|(_, source)| source).collect(), + cases: expected, + shapes, + } +} + +fn type_mutations() -> GeneratedLane { + let heads = [ + "Int", + "List(Int)", + "(Int, Float)", + "#(I64, U64)", + "#{ w : Int, h : Float }", + ]; + let rows = ["{}", "{Tick}", "{Emit(Int)}", "{Tick, Emit(Int) | e}"]; + let mut cases = Vec::new(); + for head in heads { + for row in rows { + let ty = format!("({head}) -> {head} ! {row}"); + let index = cases.len(); + cases.push((ty.clone(), format!("alias TypeMutation{index} = {ty}\n"))); + } + } + aggregate_lane("type", "Bool", cases, 20, 20) +} + +fn pattern_mutations() -> GeneratedLane { + let atoms = ["x", "_", "0", "'a'"]; + let shells = ["ctor", "tuple", "list", "record", "or"]; + let mut cases = Vec::new(); + for atom in atoms { + for shell in shells { + let pat = match shell { + "ctor" => format!("Some({atom})"), + "tuple" => format!("({atom}, _)"), + "list" => format!("[{atom}, _]"), + "record" => format!("Point {{ x = {atom}, .. }}"), + "or" => format!("Some({atom}) | None"), + _ => unreachable!("closed pattern shell matrix"), + }; + let index = cases.len(); + cases.push(( + format!("{shell}:{atom}"), + format!( + "fn pattern_mutation_{index}(v) =\n match v of\n {pat} => 0\n _ => 1\n" + ), + )); + } + } + aggregate_lane("pattern", "ctor:None", cases, 20, 20) +} + +fn vertical_mutations() -> GeneratedLane { + let mut cases = Vec::new(); + for width in [2, 4, 6] { + let one = " ".repeat(width); + let two = " ".repeat(width * 2); + for shape in ["closed-if", "open-if", "match", "try"] { + let index = cases.len(); + let body = match shape { + "closed-if" => { + format!("{one}if x then\n{two}1\n{one}else\n{two}0\n") + } + "open-if" => { + format!("{one}if x > 0 then\n{two}1\n{one}elif x < 0 then\n{two}2\n{one}0\n") + } + "match" => { + format!("{one}match x of\n{two}0 => 1\n{two}_ => 2\n") + } + "try" => format!("{one}let y = r?\n{one}y\n"), + _ => unreachable!("closed vertical shape matrix"), + }; + cases.push(( + format!("{width}:{shape}"), + format!("fn vertical_mutation_{index}(x, r) =\n{body}"), + )); + } + } + aggregate_lane("vertical", "0:inline", cases, 12, 4) +} + +fn cross_mutations() -> GeneratedLane { + let mut cases = Vec::new(); + for ty in ["Int", "List(Int)"] { + for pat in ["Some(x)", "(x, _)", "Pair(x, _)"] { + for width in [2, 4] { + let indent = " ".repeat(width); + let index = cases.len(); + cases.push(( + format!("{ty}|{pat}|{width}"), + format!( + "fn cross_mutation_{index}(v : {ty}, r) : {ty} =\n\ + {indent}let {pat} = r?\n{indent}v\n" + ), + )); + } + } + } + aggregate_lane("cross", "Int|_|0", cases, 12, 6) +} + +fn json_span(node: &Value, context: &str) -> (usize, usize) { + let span = node["span"] + .as_array() + .unwrap_or_else(|| panic!("{context}: span is not an array")); + assert_eq!(span.len(), 2, "{context}: span width"); + let bound = |index: usize| { + let raw = span[index] + .as_u64() + .unwrap_or_else(|| panic!("{context}: span[{index}]")); + usize::try_from(raw).unwrap_or_else(|_| panic!("{context}: span[{index}] overflows")) + }; + (bound(0), bound(1)) +} + +fn assert_span_bounds(node: &Value, source_len: usize, path: &str) -> usize { + match node { + Value::Object(fields) => { + let own = usize::from(fields.contains_key("span")); + if own == 1 { + let (lo, hi) = json_span(node, path); + assert!(lo <= hi, "{path}: inverted span [{lo}, {hi})"); + assert!( + hi <= source_len, + "{path}: span [{lo}, {hi}) exceeds {source_len} bytes" + ); + } + own + fields + .iter() + .map(|(key, child)| assert_span_bounds(child, source_len, &format!("{path}.{key}"))) + .sum::() + } + Value::Array(items) => items + .iter() + .enumerate() + .map(|(index, child)| { + assert_span_bounds(child, source_len, &format!("{path}[{index}]")) + }) + .sum(), + Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => 0, + } +} + +// The stand-in for a generated declaration name in a shape comparison. +const LANE_NAME_HOLE: &str = ""; + +// Whether a string is one of the declaration names a lane generates. Each case +// numbers its own declaration, so the name differs between any two cases of a +// lane whatever the encoder wrote about the construct under test. Folding the +// name away is what keeps the shape comparison from passing on the index alone. +fn is_lane_name(text: &str) -> bool { + let stem = text.trim_end_matches(|c: char| c.is_ascii_digit()); + stem.len() < text.len() && (stem.ends_with("mutation_") || stem.ends_with("Mutation")) +} + +// The encoded node reduced to its structure: spans erased, generated names +// folded. Two cases of one lane sit at different offsets and carry different +// declaration names, so comparing encodings verbatim would hold no matter what +// the encoder recorded. What survives here is only what the parser understood. +fn lane_shape(node: &Value) -> Value { + match node { + Value::Object(fields) => Value::Object( + fields + .iter() + .filter(|(key, _)| key.as_str() != "span") + .map(|(key, child)| (key.clone(), lane_shape(child))) + .collect(), + ), + Value::Array(items) => Value::Array(items.iter().map(lane_shape).collect()), + Value::String(text) if is_lane_name(text) => Value::String(String::from(LANE_NAME_HOLE)), + other => other.clone(), + } +} + +fn synth_count(node: &Value) -> usize { + match node { + Value::Object(fields) => { + usize::from(fields.get("synth").and_then(Value::as_bool) == Some(true)) + + fields.values().map(synth_count).sum::() + } + Value::Array(items) => items.iter().map(synth_count).sum(), + Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => 0, + } +} + +fn generated_lane_artifact( + label: &str, + lane: &GeneratedLane, + expect_sugar: bool, +) -> (PathBuf, PathBuf) { + let artifact = dump_on(SURFACE_PHASE, &lane.source, &roots(), &Config::from_env()) + .unwrap_or_else(|error| panic!("{label}: Rust parser rejected a generated lane: {error}")); + let doc: Value = serde_json::from_str(&artifact) + .unwrap_or_else(|error| panic!("{label}: generated surface JSON: {error}")); + assert_eq!( + doc["source"]["text"].as_str(), + Some(lane.source.as_str()), + "{label}: artifact source envelope" + ); + let items = doc["items"] + .as_array() + .unwrap_or_else(|| panic!("{label}: items array")); + assert_eq!(items.len(), lane.cases, "{label}: one item per mutation"); + let span_count = assert_span_bounds(&doc, lane.source.len(), "$"); + assert!( + span_count >= lane.cases, + "{label}: recursive span walk found only {span_count} spans" + ); + let mut previous = 0; + for (index, item) in items.iter().enumerate() { + let (lo, _hi) = json_span(item, &format!("{label}.items[{index}]")); + assert!( + lo >= previous, + "{label}: items leave source order at byte {lo}" + ); + previous = lo; + } + if expect_sugar { + assert!( + synth_count(&doc) > 0, + "{label}: sugar lane contains no synthesized node" + ); + } + + // What the encoding keeps, counted. Erasing spans and folding the generated + // declaration names leaves only what the parser understood about each case, + // so the number of distinct shapes says exactly which axes of the lane are + // structural. The check fails in both directions and both are real defects: + // fewer shapes than expected means the encoder dropped a field two cases + // differed in, and an encoding that loses a field cannot stand in for the + // tree it encodes; more shapes than expected means indentation reached the + // tree, and layout must not survive parsing. + let shapes: BTreeSet = items + .iter() + .map(|item| lane_shape(item).to_string()) + .collect(); + assert_eq!( + shapes.len(), + lane.shapes, + "{label}: {} cases encode to {} distinct shapes, expected {}", + lane.cases, + shapes.len(), + lane.shapes + ); + + let path = env::temp_dir().join(format!( + "prism-parity-{label}-{}.surface-syntax.json", + process::id() + )); + let mismatch = env::temp_dir().join(format!( + "prism-parity-{label}-{}.mismatch.json", + process::id() + )); + let _ = fs::remove_file(&mismatch); + fs::write(&path, artifact).expect("write generated mutation oracle"); + (path, mismatch) +} + +#[test] +fn parity_generated_mutation_lanes() { + let labels = [ + "mutation-type", + "mutation-pattern", + "mutation-vertical", + "mutation-cross", + ]; + let files = [ + generated_lane_artifact(labels[0], &type_mutations(), false), + generated_lane_artifact(labels[1], &pattern_mutations(), false), + generated_lane_artifact(labels[2], &vertical_mutations(), true), + generated_lane_artifact(labels[3], &cross_mutations(), true), + ]; + let pairs: Vec<(&Path, &Path)> = files + .iter() + .map(|(artifact, mismatch)| (artifact.as_path(), mismatch.as_path())) + .collect(); + let verdicts = parity_pairs(&pairs); + + for (artifact, mismatch) in &files { + let _ = fs::remove_file(artifact); + let _ = fs::remove_file(mismatch); + } + assert_eq!( + verdicts.len(), + labels.len(), + "one verdict per mutation lane" + ); + for (label, verdict) in labels.into_iter().zip(verdicts) { + assert_eq!( + verdict, OK, + "{label}: the Prism parser must reproduce the Rust surface bytes" + ); + } +} + // The bootstrap smoke: `Syntax.Parse` accepts the parser fixtures themselves, // its own harness included, without the artifact round trip in the way. One // verdict line per file, each of which must lead with the ok token. @@ -189,6 +561,180 @@ fn parity_covers_every_stem() { ); } +// Whose parser the gate is actually judging. +// +// Module resolution is first-hit and a directory root precedes the embedded +// standard library, so a file at `/Syntax/Parse.pr` supplies that module +// to everything resolving under that root. The witness imports the front end by +// name, which means the search path it runs under decides which front end the +// comparison certifies. A harness that ever derived its roots from the tree it +// compares would let a file ship the parser that judges it, and the gate would +// be comparing a parser against itself while still printing `ok`. +// +// Two halves, because either alone proves nothing. The substitution is live, +// shown by performing it. And the harness does not expose it, shown by the +// search path it really uses. +const HOSTILE_FIXTURES: &str = "tests/fixtures/parser/hostile"; + +// One directory per module of the shadow front end, each supplying that module +// and nothing else, so a run that survives names which module failed to matter. +const SHADOWED: [(&str, &str); 3] = [ + ("layout", "Syntax.Layout"), + ("lex", "Syntax.Lex"), + ("parse", "Syntax.Parse"), +]; + +// The family the shadow front end lives in. Guarding the subtree rather than a +// list of module names covers a parser module added later on the day it exists, +// with no second list to keep in step. +const SHADOW_FAMILY: &str = "Syntax"; + +#[test] +fn parity_front_end_is_compiler_owned() { + let artifact = fixture(SYNTAX_FIXTURES).join(format!("types.{SURFACE_PHASE}.json")); + let mismatch = env::temp_dir().join("prism-parity-shadowed.json"); + let _ = fs::remove_file(&mismatch); + let args = witness_args(&[(artifact.as_path(), mismatch.as_path())]); + + for (dir, module) in SHADOWED { + let shadowed = default_roots(&fixture(HOSTILE_FIXTURES).join(dir)); + let outcome = run_witness(&shadowed, args.clone()); + assert!( + outcome.is_err(), + "a root supplying {module} must decide which front end the witness \ + runs, and this one did not: {outcome:?}" + ); + } + + for root in roots() { + let Root::Dir(dir) = root else { continue }; + let family = dir.join(SHADOW_FAMILY); + assert!( + !family.exists(), + "{} would supply the shadow front end ahead of the standard library, \ + so the parser under test could be chosen by the tree it is run over", + family.display() + ); + } +} + +// What the front end costs, asserted as a shape rather than as a number. +// +// The measured Prism-to-Rust parse figure is wall clock from a dedicated +// harness, and wall clock on a loaded machine is not something a gate can stand +// on. Machine steps are: the count is a pure function of the program and its +// input. Even so, a pinned count is the wrong pin. It reseats on every ordinary +// parser edit, and having reseated it says nothing about the shape of the cost, +// which is the only part a budget is really protecting. So the assertion is the +// shape directly: doubling the input doubles the work. +// +// A run pays to load the front end before it parses anything, and that part does +// not grow with the input. Comparing two sizes directly would fold it into the +// answer and drift the ratio toward one, so the comparison is taken between +// three sizes instead: the difference of the differences cancels any cost that +// does not grow, whatever it is, and needs no separate run to measure it. +const COST_UNITS: usize = 12; +const COST_DECLS_PER_UNIT: usize = 3; + +// The subject is identical repetitions, so a parser linear in its input lands on +// two exactly, and the measured value is two to the digit. The band is therefore +// kept narrow enough to mean something rather than widened to whatever passes: a +// pass that added one log factor over the whole input would land at 2.36 and is +// meant to fail here, because a parser acquiring one is a change worth stating +// out loud and widening this band deliberately. +const COST_RATIO_LOW: f64 = 1.85; +const COST_RATIO_HIGH: f64 = 2.30; + +// One repetition of the subject: a signature, a sum declaration, and a match +// over it, each named for its index so no two repetitions are the same text. +// Every spelling here avoids the quote, brace, and backslash that would need +// escaping on the way into the Prism string literal that carries it. +fn cost_unit(index: usize) -> String { + format!( + "fn fa{index}(a : Int, b : Int) : Int = a + b * 2 - 1\n\ + type Tb{index} = Ca{index}(Int) | Cb{index}(String, Bool)\n\ + fn fc{index}(x : Tb{index}) : Int =\n\ + \x20 match x of\n\ + \x20 Ca{index}(n) => n + fa{index}(n, 1)\n\ + \x20 Cb{index}(s, p) => if p then 1 else 0\n" + ) +} + +// The cost harness: parse a subject of `units` repetitions through the shadow +// front end and report how many declarations came back. Everything the run does +// besides parsing is one walk of the result and one printed line, both linear in +// the input and both dwarfed by the parse, so the machine steps the run takes +// track the parser's own cost on that subject. +fn cost_program(units: usize) -> String { + let subject: String = (0..units).map(cost_unit).collect(); + let literal = subject.replace('\n', "\\n"); + format!( + "import Syntax.Parse (..)\n\ + \n\ + fn subject() : String = \"{literal}\"\n\ + \n\ + fn declared(items, n) =\n\ + \x20 match items of\n\ + \x20 Nil => n\n\ + \x20 Cons(_, rest) => declared(rest, n + 1)\n\ + \n\ + fn main() =\n\ + \x20 match parse_source(subject()) of\n\ + \x20 Ok(items) => println(\"parsed {{declared(items, 0)}}\")\n\ + \x20 Err(_) => println(\"refused\")\n" + ) +} + +// Machine steps and the reported line for one subject size. +fn cost_run(units: usize) -> (usize, String) { + let src = with_prelude(&cost_program(units)); + let mut sink = Vec::new(); + let ruler = step_ruler_on( + &src, + &roots(), + &mut sink, + &mut &b""[..], + &Config::from_env(), + ) + .unwrap_or_else(|error| panic!("cost harness at {units} units: {error}")); + let out = String::from_utf8(sink).expect("utf8 cost harness output"); + (ruler.total_steps, out.trim().to_string()) +} + +#[test] +fn parity_front_end_cost_stays_linear() { + let sizes = [COST_UNITS, 2 * COST_UNITS, 4 * COST_UNITS]; + let runs = sizes.map(cost_run); + + // The parser parsed, and parsed all of it. A run that refused, or that + // dropped declarations, would otherwise satisfy any ratio at all. + for (units, (_, line)) in sizes.iter().zip(&runs) { + assert_eq!( + *line, + format!("parsed {}", units * COST_DECLS_PER_UNIT), + "the subject at {units} units" + ); + } + + let [small, mid, large] = runs.map(|(steps, _)| steps); + assert!( + small < mid && mid < large, + "parsing more must cost more: {small}, {mid}, {large}" + ); + + #[expect( + clippy::cast_precision_loss, + reason = "a step count large enough to lose precision here is already \ + orders of magnitude outside the band" + )] + let ratio = (large - mid) as f64 / (mid - small) as f64; + assert!( + (COST_RATIO_LOW..=COST_RATIO_HIGH).contains(&ratio), + "doubling the subject moved the front end's step count by {ratio:.2}x \ + ({small}, {mid}, {large} at {sizes:?} units); linear is 2 and quadratic is 4" + ); +} + // Recursion-depth refusal is exercised across every nesting axis, including // direct let-pattern entries. Each axis is probed one point below the recursion // budget, where the witness must accept, and one point beyond, where @@ -461,18 +1007,25 @@ depth_axis! { // Negative-corpus parity: the committed malformed artifacts are the oracle, // and the witness compares the refusal's semantic projection (code, phase, // span, canonical expected set, related spans; message prose excluded as -// renderer-owned). One case is a named, watched exception: the -// expression-at-EOF expected set still diverges because the atom noteset is -// incomplete, and this test fails the moment it starts agreeing so the -// exception cannot outlive its cause. +// renderer-owned). Every case agrees exactly, expected sets included, so no +// stem carries an exception. +// +// The expression-at-EOF case is the one that reaches the expected sets, and +// neither side of that comparison is authored for the test: the oracle's set is +// the grammar's own first set for an operand, surfaced by `canonical_expected`, +// and the shadow's is whatever the cursor noted where it refused. Requiring +// equality there is what keeps the two parsers agreeing on which tokens can +// begin an operand, and the witness reports the difference as tokens rather +// than as an offset, so a regression names what it added or dropped. const NEGATIVE_WITNESS: &str = "negative_parity.pr"; + const NEGATIVE_STEMS: [(&str, &str); 8] = [ ("malformed_empty_hole", OK), ("malformed_invalid", OK), ("malformed_lex", OK), ("malformed_number_sep", OK), ("malformed_parse", OK), - ("malformed_parse_eof", "expected-set divergence at 362"), + ("malformed_parse_eof", OK), ("malformed_parse_flip", OK), ("malformed_unterm_hole", OK), ]; diff --git a/tests/compiler/parser_receipt.rs b/tests/compiler/parser_receipt.rs new file mode 100644 index 00000000..adf97a3b --- /dev/null +++ b/tests/compiler/parser_receipt.rs @@ -0,0 +1,536 @@ +// The shadow-parser comparison receipt, produced from a run rather than +// hand-written. +// +// `parser_parity.rs` proves the two parsers agree on fixtures, grammar edges, +// and generated mutation matrices. This lane runs them against committed code: +// the stdlib, the examples, the documentation sources, and the language cases, +// which is the corpus a real parser change is judged on. It then records what +// the run established as one content-addressed receipt, so a later run over the +// same parser and the same corpus is a reproduction check instead of a fresh +// unrelated assertion. +// +// The receipt is split the way the store is: the comparison and the +// compiler-work counters go to the immutable certificate layer, where identical +// bytes are the proof; the wall times go to the mutable decision layer, where a +// second run is allowed to differ. +// +// The corpus is bounded by default and complete under +// `PRISM_PARSER_RECEIPT_FULL=1`. The bound is a deterministic stride, never a +// random sample, and the file count rides in the receipt so a reader always +// knows which lane produced the bytes they are holding. + +use std::collections::{BTreeMap, BTreeSet}; +use std::path::{Path, PathBuf}; +use std::{env, fs}; + +use prism::core::work; +use prism::store::disk::{Store, Written}; +use prism::store::receipt::{self, Comparison, ShadowReceipt}; +use prism::{default_roots, dump_on, interpret_io_on_with_args, with_prelude, Config, Root}; +use prism::{PhaseTally, TimingSink}; + +use crate::support::TempDir; + +// The differential witness and the phase whose artifact it consumes, shared with +// the parity lane next door. +const WITNESS: &str = "tests/fixtures/parser/parity.pr"; +const SURFACE_PHASE: &str = "surface-syntax"; +// The canonical Core identity, and the phase whose dump runs the whole front end +// under a caller's config. They are two dumps because they must be: the identity +// surface is deliberately config-independent, so it cannot be the run that +// carries the instrument. +const CORE_HASH_PHASE: &str = "core-hash"; +const CORE_PHASE: &str = "core"; +const OK: &str = "ok"; + +// Where committed Prism lives. Every corpus file the comparison covers comes +// from one of these, in this order, sorted within each. +const CORPUS_DIRS: [&str; 4] = ["lib", "examples", "docs/examples", "tests/cases"]; + +// The two parsers, each named by what it is made of. Symmetric on purpose: a +// receipt that identified one by its sources and the other by a version string +// would attest a comparison only half of which a later reader could re-derive, +// and the half left out is the authority. +const AUTHORITY_SOURCES: [&str; 4] = [ + "crates/prism-syntax/src/grammar.lalrpop", + "crates/prism-syntax/src/ast.rs", + "crates/prism-syntax/src/lex", + "crates/prism-syntax/src/parse", +]; +const AUTHORITY_EXT: &str = "rs"; +const SHADOW_SOURCES: [&str; 1] = ["lib/std/Syntax"]; +const PRISM_EXT: &str = "pr"; + +// The default lane's size, and the knob that runs the whole corpus instead. The +// witness is interpreted Prism, so a complete pass is a nightly-scale run and +// the per-change lane takes a deterministic stride through the same list. +const DEFAULT_CORPUS_FILES: usize = 40; +const FULL_CORPUS_ENV: &str = "PRISM_PARSER_RECEIPT_FULL"; +// Fewer compared files than this and the lane has stopped being evidence, +// whatever verdict it reports. +const MIN_CORPUS_FILES: usize = 24; + +// Where a divergence is left for a human to read. Under `target/` so it is +// ignored by git and swept by `cargo clean`, and a named directory rather than a +// temporary because an artifact that vanishes with the failing process is not a +// located one. +const DIVERGENCE_DIR: &str = "target/parser-divergence"; +const AUTHORITY_SUFFIX: &str = "authority.json"; +const SHADOW_SUFFIX: &str = "shadow.json"; +// How many diverging files the failure message names. Every divergence is +// written out and the reported count is always exact; this bounds the message, +// not the evidence. +const MAX_NAMED_DIVERGENCES: usize = 12; + +// The divergences the committed corpus currently shows, each named by the +// construct it stands for. The Rust parser is authoritative, so a row here is a +// gap in the Prism parser and a debt against parser authority, never a tolerated +// difference. The list is empty: over the whole committed corpus the two parsers +// agree byte for byte, spans and synth bits included. +// +// The set is watched in both directions: a divergence that is not listed fails +// the lane, and a complete sweep also fails when a listed file starts agreeing, +// so the list cannot outlive the gap it records. Empty, the first half is the +// whole gate, and a row may only ever be added with the construct that earned +// it. +const KNOWN_DIVERGENCES: [(&str, &str); 0] = []; + +// The front end runs over these to charge the work counters. Named rather than +// taken off the top of a sorted directory, because the counters only need a real +// compile and an alphabetical prefix would silently hand this lane whichever +// example happens to be the most expensive one in the tree. +const COMPILE_SOURCES: [&str; 3] = [ + "examples/factorial.pr", + "examples/greet.pr", + "examples/fold.pr", +]; + +// The phase the receipt requires evidence of. Elaboration always runs and always +// descends, so a zero here means the instrument did not fire, which is exactly +// the vacuity the receipt refuses to attest. +const EXERCISED: [&str; 1] = ["elaborate"]; + +fn root() -> &'static Path { + Path::new(env!("CARGO_MANIFEST_DIR")) +} + +fn roots() -> Vec { + default_roots(root()) +} + +// Hash a list of labelled byte strings. Length-prefixed so no concatenation of +// two entries can spell a third. +fn identity(parts: &[(&str, &[u8])]) -> String { + let mut h = blake3::Hasher::new(); + for (label, bytes) in parts { + h.update(&(label.len() as u64).to_le_bytes()); + h.update(label.as_bytes()); + h.update(&(bytes.len() as u64).to_le_bytes()); + h.update(bytes); + } + h.finalize().to_hex().to_string() +} + +// Every file with `ext` under `path`, sorted, so a file list is a function of +// the tree and not of directory iteration order. A `path` that names a file is +// taken whatever its extension: the extension filter is how a directory is +// narrowed, not a second opinion about a file the caller named outright. +fn files_under(path: &Path, ext: &str) -> Vec { + if path.is_file() { + return vec![path.to_path_buf()]; + } + let mut out = Vec::new(); + let Ok(entries) = fs::read_dir(path) else { + return out; + }; + let mut entries: Vec = entries.filter_map(|e| e.ok().map(|e| e.path())).collect(); + entries.sort(); + for entry in entries { + if entry.is_dir() { + out.extend(files_under(&entry, ext)); + } else if entry.extension().is_some_and(|e| e == ext) { + out.push(entry); + } + } + out +} + +fn pr_files(dir: &Path) -> Vec { + files_under(dir, PRISM_EXT) +} + +// A parser's artifact identity: the hash of the sources it is built from, each +// entry labelled by its path so moving a file is a change and not a coincidence. +fn artifact_identity(sources: &[&str], ext: &str) -> String { + let files: Vec = sources + .iter() + .flat_map(|s| files_under(&root().join(s), ext)) + .collect(); + assert!( + !files.is_empty(), + "a parser's sources must be findable: {sources:?}" + ); + let read: Vec<(String, Vec)> = files + .iter() + .map(|p| (rel(p), fs::read(p).expect("read a parser source"))) + .collect(); + let parts: Vec<(&str, &[u8])> = read + .iter() + .map(|(name, bytes)| (name.as_str(), bytes.as_slice())) + .collect(); + identity(&parts) +} + +// Whether this run sweeps every committed source rather than a bounded stride. +// Only a complete sweep may judge the watched set stale, since a stride cannot +// distinguish a fixed parser from an unsampled file. +fn full_corpus() -> bool { + env::var_os(FULL_CORPUS_ENV).is_some() +} + +fn corpus_files() -> Vec { + let all: Vec = CORPUS_DIRS + .iter() + .flat_map(|dir| pr_files(&root().join(dir))) + .collect(); + if full_corpus() || all.len() <= DEFAULT_CORPUS_FILES { + return all; + } + // A stride rather than a prefix: a prefix would sample one directory and + // call it the corpus, and a stride keeps every source kind represented while + // staying a pure function of the sorted list. + let stride = all.len().div_ceil(DEFAULT_CORPUS_FILES); + all.into_iter().step_by(stride).collect() +} + +fn rel(path: &Path) -> String { + path.strip_prefix(root()) + .unwrap_or(path) + .display() + .to_string() +} + +// One corpus file's surface artifact, or `None` when the authoritative parser +// rejects it. A rejection is not a comparison: the negative corpus lane next +// door is what pins refusal behavior, and counting a refusal here would let a +// corpus of unparseable files report perfect agreement. +fn surface_artifact(path: &Path, cfg: &Config) -> Option { + let source = fs::read_to_string(path).ok()?; + dump_on(SURFACE_PHASE, &source, &roots(), cfg).ok() +} + +struct Compared { + /// `(relative path, authority artifact bytes)`, in corpus order. + artifacts: Vec<(String, String)>, + /// The shadow's own bytes per file: equal to the authority's where the two + /// agreed, and the encoding the witness wrote out where they did not. + shadow: Vec>, + /// `(relative path, the witness's verdict line)` for each file the two + /// parsers encoded differently, in corpus order. + diverged: Vec<(String, String)>, + /// Files the authoritative parser refused, and so were never compared. + refused: usize, +} + +// Leave a divergence somewhere a human can open it: both encodings of the file, +// side by side, under a stable path. Returns that directory. +fn divergence_dir() -> PathBuf { + let dir = root().join(DIVERGENCE_DIR); + // A stale encoding from an earlier run beside a fresh one is worse than no + // encoding at all, so the directory is emptied rather than added to. + let _ = fs::remove_dir_all(&dir); + fs::create_dir_all(&dir).expect("make a directory for the diverging encodings"); + dir +} + +// Judge a run's divergences against the watched set. Every divergence is a debt +// against parser authority; what this decides is only whether the run found a +// debt nobody had written down, or paid one nobody had crossed off. +fn judge(compared: &Compared) { + let known: BTreeSet<&str> = KNOWN_DIVERGENCES.iter().map(|(name, _)| *name).collect(); + + let fresh: Vec<&(String, String)> = compared + .diverged + .iter() + .filter(|(name, _)| !known.contains(name.as_str())) + .collect(); + if !fresh.is_empty() { + let mut named: Vec = fresh + .iter() + .take(MAX_NAMED_DIVERGENCES) + .map(|(name, verdict)| format!(" {name}: {verdict}")) + .collect(); + let rest = fresh.len() - named.len(); + if rest > 0 { + named.push(format!(" ... and {rest} more, all written out")); + } + panic!( + "the Prism parser diverged from the Rust parser on {} file(s) that are not \ + on the watched list:\n{}\n\ + both encodings of each are under {}/ as *.{AUTHORITY_SUFFIX} and *.{SHADOW_SUFFIX}", + fresh.len(), + named.join("\n"), + DIVERGENCE_DIR + ); + } + + if !full_corpus() { + return; + } + // A complete sweep saw every committed source, so anything still listed but + // no longer diverging is a gap that has been closed and a row that must go. + let compared_names: BTreeSet<&str> = compared + .artifacts + .iter() + .map(|(name, _)| name.as_str()) + .collect(); + let diverged_names: BTreeSet<&str> = compared + .diverged + .iter() + .map(|(name, _)| name.as_str()) + .collect(); + let stale: Vec = KNOWN_DIVERGENCES + .iter() + .filter(|(name, _)| !diverged_names.contains(name)) + .map(|(name, construct)| { + let why = if compared_names.contains(name) { + "now agrees" + } else { + "was never compared (moved, deleted, or refused by the authority)" + }; + format!(" {name} ({construct}): {why}") + }) + .collect(); + assert!( + stale.is_empty(), + "the watched divergence list is stale; remove {} row(s) from KNOWN_DIVERGENCES:\n{}", + stale.len(), + stale.join("\n") + ); +} + +// Run the witness over the corpus in one batch and collect both sides' bytes. +fn compare(dir: &TempDir, cfg: &Config) -> Compared { + let files = corpus_files(); + assert!(!files.is_empty(), "the corpus must not be empty"); + + let mut artifacts = Vec::new(); + let mut refused = 0; + for path in &files { + match surface_artifact(path, cfg) { + Some(json) => artifacts.push((rel(path), json)), + None => refused += 1, + } + } + // The lane's own anti-vacuity floor. A walker that stopped finding sources, or + // a corpus the authority now refuses wholesale, would otherwise leave a + // one-file comparison reporting perfect agreement. + assert!( + artifacts.len() >= MIN_CORPUS_FILES, + "only {} of {} corpus file(s) reached the comparison ({refused} refused); \ + a receipt over that is not evidence", + artifacts.len(), + files.len() + ); + + // The witness takes filesystem paths, so each artifact is staged beside a + // mismatch path it may write its own encoding to. + let staged: Vec<(PathBuf, PathBuf)> = artifacts + .iter() + .enumerate() + .map(|(i, (_, json))| { + let artifact = dir.join(format!("corpus-{i}.surface-syntax.json")); + let mismatch = dir.join(format!("corpus-{i}.mismatch.json")); + fs::write(&artifact, json).expect("stage a surface artifact"); + (artifact, mismatch) + }) + .collect(); + let pairs: Vec<(&Path, &Path)> = staged + .iter() + .map(|(a, m)| (a.as_path(), m.as_path())) + .collect(); + let verdicts = run_witness(&pairs, cfg); + assert_eq!( + verdicts.len(), + artifacts.len(), + "one verdict per compared file" + ); + + let mut shadow = Vec::new(); + let mut diverged = Vec::new(); + let mut out = None; + for (i, verdict) in verdicts.iter().enumerate() { + let (name, authority) = &artifacts[i]; + if verdict == OK { + // Agreement means the shadow produced these exact bytes, so the + // authority's artifact is the shadow's artifact. + shadow.push(authority.clone().into_bytes()); + continue; + } + // The witness writes its own encoding to the mismatch path; where it + // could not, its verdict line is what the run has to say about the file. + let bytes = fs::read(&staged[i].1).unwrap_or_else(|_| verdict.clone().into_bytes()); + let dir = out.get_or_insert_with(divergence_dir); + let stem = name.replace(['/', '\\'], "-"); + fs::write(dir.join(format!("{stem}.{AUTHORITY_SUFFIX}")), authority) + .expect("leave the authority's encoding"); + fs::write(dir.join(format!("{stem}.{SHADOW_SUFFIX}")), &bytes) + .expect("leave the shadow's encoding"); + diverged.push((name.clone(), verdict.clone())); + shadow.push(bytes); + } + Compared { + artifacts, + shadow, + diverged, + refused, + } +} + +fn run_witness(pairs: &[(&Path, &Path)], cfg: &Config) -> Vec { + let src = fs::read_to_string(root().join(WITNESS)).expect("differential witness source"); + let full = with_prelude(&src); + let args = pairs + .iter() + .flat_map(|(a, m)| [a.display().to_string(), m.display().to_string()]) + .collect(); + let mut sink = Vec::new(); + interpret_io_on_with_args(&full, &roots(), &mut sink, &mut &b""[..], cfg, args) + .unwrap_or_else(|error| panic!("differential witness run: {error}")); + String::from_utf8(sink) + .expect("utf8 witness output") + .lines() + .filter(|line| !line.is_empty()) + .map(str::to_string) + .collect() +} + +// Run the front end over a few examples with the work counters live, and return +// what each phase accumulated together with the folded Core identity. +fn compile_for_counters() -> (BTreeMap<&'static str, PhaseTally>, String) { + // Process-wide by design (the counters are global atomics), which is safe + // here because the suite runs a process per test. Under a threaded runner a + // concurrent compile would inflate the deltas, so this lane asserts that the + // counters fired rather than pinning them to an exact figure. + work::enable(); + let sink = TimingSink::new(); + let cfg = Config { + timing: Some(sink.clone()), + ..Config::from_env() + }; + let plain = Config::from_env(); + let mut hashes = Vec::new(); + for path in COMPILE_SOURCES.map(|s| root().join(s)) { + let Ok(source) = fs::read_to_string(&path) else { + continue; + }; + let source = with_prelude(&source); + // The identity dump takes the pre-optimizer Core under a default config on + // purpose, so that a hash never depends on an env-toggled pass. That also + // makes it deaf to the instrument, so the counters come from a second run + // of the same source through the phase that does thread a config. + let Ok(hash) = dump_on(CORE_HASH_PHASE, &source, &roots(), &plain) else { + continue; + }; + if dump_on(CORE_PHASE, &source, &roots(), &cfg).is_ok() { + hashes.push(format!("{}\t{hash}", rel(&path))); + } + } + assert!( + !hashes.is_empty(), + "the front end produced no Core identity to record" + ); + let core = identity(&[("core", hashes.join("\n").as_bytes())]); + (sink.tallies(), core) +} + +#[test] +fn the_corpus_comparison_is_recorded_as_a_receipt() { + let dir = TempDir::new("parser-receipt", "corpus"); + let cfg = Config::from_env(); + let compared = compare(&dir, &cfg); + let (tallies, core_hash) = compile_for_counters(); + + let corpus_manifest: Vec = compared + .artifacts + .iter() + .map(|(name, _)| name.clone()) + .collect(); + let authority_bytes = compared + .artifacts + .iter() + .map(|(_, json)| json.as_bytes().to_vec()) + .collect::>() + .concat(); + + let comparison = Comparison { + authority: artifact_identity(&AUTHORITY_SOURCES, AUTHORITY_EXT), + shadow: artifact_identity(&SHADOW_SOURCES, PRISM_EXT), + corpus: identity(&[("corpus", corpus_manifest.join("\n").as_bytes())]), + corpus_files: compared.artifacts.len(), + syntax_hash_authority: identity(&[("surface", &authority_bytes)]), + syntax_hash_shadow: identity(&[("surface", &compared.shadow.concat())]), + core_hash, + divergences: compared.diverged.len(), + }; + let receipt = ShadowReceipt::new(comparison, &tallies, &EXERCISED) + .expect("the run exercised a phase and covered a corpus"); + + // The comparison is the gate; the receipt only records it. A divergence is a + // located artifact, never a fallback quietly accepted as success: the failure + // names the files and leaves both encodings of each one on disk. + judge(&compared); + // Byte identity and a zero count are the same fact seen twice, so a run that + // reports one without the other has an encoder or a hasher that is lying. + assert_eq!( + receipt.comparison.syntax_hash_authority == receipt.comparison.syntax_hash_shadow, + receipt.comparison.divergences == 0, + "the syntax hashes and the divergence count disagree about whether the run agreed" + ); + // Anti-vacuity, restated where a reader of this file can see it: the + // counters describe work that happened. + let elaborate = receipt + .phases + .get(EXERCISED[0]) + .expect("an exercised phase"); + assert!(elaborate.visits > 0 && elaborate.invocations > 0); + assert!(receipt.max_depth > 0, "a compile descends"); + + let store = Store::open_or_create(dir.store_root()).expect("open a store"); + receipt::emit(&store, &receipt).expect("record the comparison"); + // The reproduction check the store performs for free: the same run, recorded + // twice, must be the same bytes. + assert_eq!(receipt::emit(&store, &receipt).unwrap(), Written::Hit); + receipt::put_timing(&store, &receipt.subject, &tallies).expect("record the run's readings"); + + let stored = receipt::get(&store, &receipt.subject) + .expect("read back") + .expect("a receipt was written") + .expect("it decodes"); + assert_eq!(stored, receipt); + // The other half, under the same subject: a reading for every phase the run + // timed, whether or not it charged the Core counters. + let timed = receipt::get_timing(&store, &receipt.subject).expect("read back the readings"); + assert_eq!(timed.len(), tallies.len()); + assert!(timed.iter().any(|(phase, _, _)| phase == EXERCISED[0])); + + eprintln!( + "shadow parse: {} file(s) compared ({}), {} refused by the authority, \ + {} watched divergence(s), {} phase(s) charged, deepest descent {}", + receipt.comparison.corpus_files, + if full_corpus() { "complete" } else { "stride" }, + compared.refused, + receipt.comparison.divergences, + receipt.phases.len(), + receipt.max_depth + ); + for (name, verdict) in &compared.diverged { + eprintln!(" diverged {name}: {verdict}"); + } + for (phase, work) in &receipt.phases { + eprintln!( + " {phase}: {} invocation(s), {} visit(s), {} rebuilt", + work.invocations, work.visits, work.rebuilt + ); + } +} diff --git a/tests/compiler/resolved_syntax.rs b/tests/compiler/resolved_syntax.rs index 2c731da8..17552b64 100644 --- a/tests/compiler/resolved_syntax.rs +++ b/tests/compiler/resolved_syntax.rs @@ -12,17 +12,35 @@ // The round-trip and traversal oracles run the committed harness // `tests/fixtures/syntax/roundtrip.pr` through the interpreter, reading only the // artifact bytes: no source file or compiler state is consulted, so those gates -// are a pure function of the golden and stay independent of the live exporter -// and the compiler version. The join gate is the one exception: it dumps the -// live `prism-tc-facts-v1` fact table for each stem and checks that every leaf -// of the resolved body carries a fact under the same NodeId. That table is -// sparse (only a node the checker resolved, typed, or gave evidence to appears, -// so an interior let or match may be absent), but a leaf is always a settled -// reference or literal, so the leaf join is total, and diffing it against the -// live checker also proves the committed ids are the identities the current -// compiler assigns. +// are a pure function of the golden and stay independent of the live exporter. +// They are decoder oracles, and a golden serves them for as long as its schema +// holds. +// +// The join gate is live on both sides. It dumps this compiler's resolved tree +// and its `prism-tc-facts-v1` fact table for the same stem, and checks that +// every leaf of the resolved body carries a fact under the same NodeId. That +// table is sparse (only a node the checker resolved, typed, or gave evidence to +// appears, so an interior let or match may be absent), but a leaf is always a +// settled reference or literal, so the leaf join is total. +// +// Both sides are live on purpose. Joining a committed tree against a live table +// tests something weaker than it appears to: the exported ids of a fixture's +// own functions are assigned after the prelude's, so any change to the standard +// library shifts them all, and because the fact table is dense a shifted leaf +// usually still lands on some unrelated node's fact. The gate then passes by +// coincidence and reports agreement between two seams that have drifted apart. +// The stale goldens this replaced had drifted by thirteen ids and still passed, +// until a shift happened to land twelve leaves on the table's sparse gaps. +// Live-versus-live cannot pass that way, and it stays true across a stdlib edit +// without a reseat, since the question is whether the two exporters agree now. +// +// What the goldens still owe is their declared compiler version, checked below. +// A golden that outlives a release is a decoder oracle for bytes this compiler +// no longer emits, which is exactly when a silently added field would go +// unnoticed. Reseat them with `PRISM_ACCEPT_RESOLVED_FIXTURES=1`. use std::collections::HashSet; +use std::env; use std::fmt::Write as _; use std::fs; use std::path::{Path, PathBuf}; @@ -37,6 +55,9 @@ const HARNESS: &str = "roundtrip.pr"; const ARTIFACT: &str = "resolved-syntax"; // The checker's fact-table seam the resolved leaves are joined against. const FACTS_PHASE: &str = "tc-facts"; +// Rewrites the committed goldens from the live exporter, for a reviewed +// boundary change or a release version bump. +const ACCEPT: &str = "PRISM_ACCEPT_RESOLVED_FIXTURES"; // Every resolved-syntax corpus stem, kept sorted. Each is a well-typed, // self-contained fixture whose whole-program export is exactly its own @@ -137,18 +158,25 @@ fn leaf_ids(node: &Value, out: &mut Vec) { } } -// The set of NodeId keys the live checker records a fact under for one stem. -// Generated exactly as the resolved golden was (the bare stem source under the -// prelude, resolved against the same roots), so the two seams' NodeId -// identities coincide and the join is meaningful. -fn tc_facts_ids(stem: &str) -> HashSet { +// One phase dumped for a stem by this compiler: the bare stem source under the +// prelude, resolved against the fixture roots. Both sides of the join are +// produced this way, from the same source through the same roots, so the two +// seams' NodeId identities are comparable by construction. +fn dump_stem(stem: &str, phase: &str) -> String { let src = fs::read_to_string(fixture_dir().join(format!("{stem}.pr"))) .unwrap_or_else(|e| panic!("{stem}: source: {e}")); - let dump = dump_at(FACTS_PHASE, &with_prelude(&src), &fixture_dir()) - .unwrap_or_else(|e| panic!("{stem}.{FACTS_PHASE}: dump: {e}")); - let doc: Value = - serde_json::from_str(&dump).unwrap_or_else(|e| panic!("{stem}.{FACTS_PHASE}: JSON: {e}")); - doc["nodes"] + dump_at(phase, &with_prelude(&src), &fixture_dir()) + .unwrap_or_else(|e| panic!("{stem}.{phase}: dump: {e}")) +} + +fn dump_stem_json(stem: &str, phase: &str) -> Value { + serde_json::from_str(&dump_stem(stem, phase)) + .unwrap_or_else(|e| panic!("{stem}.{phase}: JSON: {e}")) +} + +// The set of NodeId keys the live checker records a fact under for one stem. +fn tc_facts_ids(stem: &str) -> HashSet { + dump_stem_json(stem, FACTS_PHASE)["nodes"] .as_object() .unwrap_or_else(|| panic!("{stem}.{FACTS_PHASE}: nodes object")) .keys() @@ -156,11 +184,13 @@ fn tc_facts_ids(stem: &str) -> HashSet { .collect() } -// The cross-seam join: every leaf of the resolved body carries a fact in the -// live checker's table under the same NodeId, so a Prism consumer can hang the -// type and resolution of each reference off the traversed tree by id. +// The cross-seam join: every leaf of this compiler's resolved body carries a +// fact in its own checker's table under the same NodeId, so a Prism consumer +// can hang the type and resolution of each reference off the traversed tree by +// id. Both dumps are live; see the header for why a committed tree here would +// make the gate pass on coincidence. fn assert_stem_join(stem: &str) { - let doc: Value = serde_json::from_str(&read_golden(stem)).expect("golden JSON"); + let doc = dump_stem_json(stem, ARTIFACT); let mut leaves = Vec::new(); for f in doc["functions"].as_array().expect("functions") { leaf_ids(&f["body"], &mut leaves); @@ -196,6 +226,36 @@ stem_tests! { resolved_roundtrip_stable, resolved_traversal_stable, resolved_join_stable => "stable", } +// Every positive golden was emitted by this compiler. The round trip and the +// traversal read the golden and never the compiler, so without this a golden +// outlives the exporter that wrote it and keeps proving the decoder handles +// bytes nothing emits any more. Checking the declared version is the cheap +// half of that: it catches the release bump, which is when the drift starts. +// Regenerate with the accept env, reviewing the diff like a snapshot. +#[test] +fn resolved_goldens_come_from_this_compiler() { + let accept = env::var(ACCEPT).is_ok(); + let mut stale = Vec::new(); + for stem in STEMS { + if accept { + let dump = dump_stem(stem, ARTIFACT); + fs::write(golden_path(stem), format!("{dump}\n")).expect("rewrite golden"); + continue; + } + let doc: Value = serde_json::from_str(&read_golden(stem)).expect("golden JSON"); + let found = doc["compiler"].as_str().unwrap_or_default().to_owned(); + if found != env!("CARGO_PKG_VERSION") { + stale.push(format!("{stem}: {found}")); + } + } + assert!( + stale.is_empty(), + "resolved-syntax goldens predate this compiler ({}); regenerate with {ACCEPT}=1: {}", + env!("CARGO_PKG_VERSION"), + stale.join(", ") + ); +} + // The static stem list matches the fixture directory exactly, so adding a // corpus file without extending the gate is a failure, not a silent skip. #[test] diff --git a/tests/compiler/store_oracle.rs b/tests/compiler/store_oracle.rs index 7121aa15..6f9d72f8 100644 --- a/tests/compiler/store_oracle.rs +++ b/tests/compiler/store_oracle.rs @@ -1,39 +1,16 @@ -//! The from-scratch versus incremental oracle pair. +//! Compares cold and incremental builds of a multi-definition program. //! -//! This is the content-addressed analogue of the interpreter/native parity gate, -//! and the half of the `hash_parity` invariant the store completes. +//! The tests assert that: //! -//! `hash_parity` proves *equal hash implies a byte-identical artifact* over -//! curated pairs. This file proves the other half over a real multi-definition -//! program driven two ways: built from scratch into a cold store, and built -//! incrementally by editing one definition against a warm store. The acceptance -//! is threefold: +//! (a) a semantic edit moves only its Merkle closure; +//! (b) cold and incremental paths emit identical stored Core and LLVM IR; +//! (c) reformatting or renaming a local writes no new objects. //! -//! (a) editing a definition moves exactly that definition and its Merkle -//! closure (its transitive dependents); every other hash is unchanged, so -//! an incremental build recompiles the closure and nothing more; -//! (b) the emitted artifact is byte-identical between a full cold rebuild and -//! the incremental path (the stored anonymous-Core object per definition, -//! and the whole-program LLVM IR, which the store never perturbs); -//! (c) an edit that only reformats or renames a local yields zero hash movement -//! and writes zero new objects. +//! Verification receipts are reused when the content hash is unchanged and +//! recomputed after semantic edits. //! -//! Verification caching rides on the same identity: a parity pass recorded -//! against a content hash ([`prism::store::verify`]) is reused when a reformat -//! keeps the hash and re-run when a semantic edit moves it, so check cost tracks -//! the Merkle closure rather than the size of the suite. -//! -//! Every assertion is taken in the store's one hashing regime, pre-optimizer -//! elaborated Core: the closure is read from the pre-optimizer dependency graph -//! (`store_def_inputs`) and the per-definition hashes the commit writes (`dump -//! core-hash`), and byte-identity is read from the store's name index and -//! objects. That is deliberate: the store commits the pre-optimizer Core hash -//! (`commit_to_store`, aligned with `dump core-hash` and `store_def_inputs`), so -//! the oracle diffs the same hashes the store keys on, not a second surface that -//! could disagree. Identity is optimizer-independent by design; the optimizer -//! level rides in the verification fingerprint. The store-only assertions compile -//! everywhere; the native build/run demonstration of cached parity is gated on -//! `feature = "native"`. +//! Hashes and closures come from pre-optimizer Core, matching the store's keys. +//! The native cached-parity check is gated on `feature = "native"`. use std::collections::{BTreeMap, BTreeSet}; use std::fs; @@ -546,8 +523,8 @@ fn store_commit_and_rehash_agree_for_optimizer_touched_programs() { } } -// The namespace root names the exact program interface, not just its definition -// bodies: two programs that differ only in a public type's shape must have +// The namespace root includes the complete program interface. Programs that +// differ only in a public type's shape must have // distinct namespace contracts. Folding definitions alone let `Token(Int)` and // `Token(String)` share one contract, so a published root did not name the value // schema under it. diff --git a/tests/compiler/syntax_compat.rs b/tests/compiler/syntax_compat.rs index ce07cd60..8918b8a3 100644 --- a/tests/compiler/syntax_compat.rs +++ b/tests/compiler/syntax_compat.rs @@ -7,9 +7,18 @@ // current reader must decode them, re-encode them to the same bytes, and agree // with the current exporter on everything except the compiler stamp they carry. // +// That last agreement is not the same statement for every schema. The three +// source-local seams are a function of the source their document carries, so the +// current exporter reproduces them byte for byte. The resolved seam is not: it +// numbers nodes with a counter that spans the prelude and every imported module, +// so an unrelated library edit renumbers a document whose source did not change. +// What it promises instead is the same document with that numbering erased, +// which is still the statement worth gating, since a shape change without a +// schema bump shows up there either way. +// // The refusal side matters as much. A document naming a different schema // version is refused whether that version is older or newer than the current -// one; an old artifact is never quietly read under the current tag, and a +// one. An old artifact is never read under the current tag, and a // newer one is never guessed at. The compiler version inside the envelope is // data, not a gate: it records who wrote the document, and a reader that // demanded its own version would make every artifact expire on release day. @@ -19,39 +28,41 @@ use std::path::{Path, PathBuf}; use serde_json::Value; -use prism::{default_roots, dump, interpret_io_on_with_args, with_prelude, Config}; +use prism::{default_roots, dump_on, interpret_io_on_with_args, with_prelude, Config, Error}; const FIXTURE_DIR: &str = "tests/fixtures/syntax"; const RELEASED_DIR: &str = "released"; const HARNESS: &str = "roundtrip.pr"; -// The release whose artifacts are retained, and the name of its directory. -const RETAINED: &str = "0.14.0"; +// The releases whose artifacts are retained, oldest first. A schema is retained +// from the release its matrix row names onward, so this order is what decides +// which directory carries which families. +const RETAINED: &[&str] = &["0.14.0", "0.15.0"]; // The retained corpus: representative sources across declaration forms, string // interpolation, stable families with migrations, and type syntax. -const STEMS: [&str; 4] = ["decls", "interp", "stable", "types"]; +const STEMS: &[&str] = &["decls", "interp", "stable", "types"]; -// The artifact families retained from that release, as (file suffix, harness -// mode). Schema tags live in the matrix below. -const RETAINED_FAMILIES: [(&str, &str); 2] = - [("syntax-tokens", "tokens"), ("surface-syntax", "surface")]; +// The resolved seam needs a program the resolver accepts, and `types.pr` is a +// type-syntax snippet naming types no module defines. It has a token, surface +// and diagnostic form, but no resolved one. +const RESOLVED_STEMS: &[&str] = &["decls", "interp", "stable"]; -// What the current reader promises about one syntax schema. -// -// `Read` means the tag shipped in the named earlier release and its documents -// are still read unchanged; the retained corpus is the evidence. `New` means -// this release introduces the tag, so there is no earlier document to read and -// nothing is retained for it. -// -// The policy allows two states no schema is in yet. A shape change that leaves -// old documents interpretable bumps the tag and gains an explicit upgrade from -// the old one; a change that cannot be upgraded rejects the old tag outright. -// Neither has been needed, because no shipped syntax schema has changed shape. +// The holes a cross-release comparison leaves in a document, and the key whose +// value the second of them replaces. +const VERSION_HOLE: &str = ""; +const NODE_ID_KEY: &str = "\"id\": "; +const NODE_ID_HOLE: &str = ""; + +// How much of a retained document the current exporter reproduces from the +// source that document carries. #[derive(Clone, Copy)] -enum Compat { - Read(&'static str), - New, +enum Export { + // The whole envelope, apart from the compiler stamp. + Bytes, + // The envelope with the node numbering erased as well, because that + // numbering is a property of the compilation and not of the source. + Shape, } struct SchemaRow { @@ -60,29 +71,58 @@ struct SchemaRow { // The schema tag, re-typed here independently of the compiler so an // emitter drift cannot re-pin the value it is checked against. tag: &'static str, - compat: Compat, + // The harness mode that decodes and re-encodes this family. + mode: &'static str, + // The stems retained for this family. + stems: &'static [&'static str], + // The oldest retained release carrying the tag: its documents are still + // read unchanged, and every retained release from that one onward holds the + // evidence. A schema first written by the release under development has no + // earlier document to read and no row here until its release is cut. + // + // The policy allows two states no schema is in yet. A shape change that + // leaves old documents interpretable bumps the tag and gains an explicit + // upgrade from the old one; a change that cannot be upgraded rejects the old + // tag outright. Neither has been needed, because no shipped syntax schema + // has changed shape. + since: &'static str, + export: Export, } -const MATRIX: [SchemaRow; 4] = [ +// A `static` rather than a `const`: the corpus walk below hands out borrows of +// these rows, which a const's per-use temporary could not outlive. +static MATRIX: [SchemaRow; 4] = [ SchemaRow { phase: "syntax-tokens", tag: "prism-syntax-tokens-v1", - compat: Compat::Read(RETAINED), + mode: "tokens", + stems: STEMS, + since: "0.14.0", + export: Export::Bytes, }, SchemaRow { phase: "surface-syntax", tag: "prism-surface-syntax-v1", - compat: Compat::Read(RETAINED), + mode: "surface", + stems: STEMS, + since: "0.14.0", + export: Export::Bytes, }, SchemaRow { phase: "syntax-diagnostics", tag: "prism-syntax-diagnostics-v1", - compat: Compat::New, + mode: "diagnostics", + stems: STEMS, + since: "0.15.0", + export: Export::Bytes, }, SchemaRow { phase: "resolved-syntax", tag: "prism-resolved-syntax-v1", - compat: Compat::New, + mode: "resolved", + stems: RESOLVED_STEMS, + since: "0.15.0", + export: Export::Shape, }, ]; @@ -90,8 +130,31 @@ fn fixture_dir() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join(FIXTURE_DIR) } -fn retained_dir() -> PathBuf { - fixture_dir().join(RELEASED_DIR).join(RETAINED) +fn released_dir(release: &str) -> PathBuf { + fixture_dir().join(RELEASED_DIR).join(release) +} + +fn artifact(release: &str, stem: &str, row: &SchemaRow) -> PathBuf { + released_dir(release).join(format!("{stem}.{}.json", row.phase)) +} + +// The retained releases carrying a family: every release from the one its row +// names onward, since a release cuts every schema it can already write. +fn releases_for(row: &SchemaRow) -> &'static [&'static str] { + let first = RETAINED + .iter() + .position(|r| *r == row.since) + .unwrap_or_else(|| panic!("{}: {} is not a retained release", row.phase, row.since)); + &RETAINED[first..] +} + +// Every artifact the matrix claims, as (release, stem, row). +fn retained_artifacts() -> impl Iterator { + MATRIX.iter().flat_map(|row| { + releases_for(row) + .iter() + .flat_map(move |release| row.stems.iter().map(move |stem| (*release, *stem, row))) + }) } fn read(path: &Path) -> String { @@ -102,14 +165,57 @@ fn json(path: &Path) -> Value { serde_json::from_str(&read(path)).unwrap_or_else(|e| panic!("{}: JSON: {e}", path.display())) } -// The schema tag a phase actually emits. The single-file seams are read from a -// live export; the resolved seam needs a resolved program rather than a snippet, -// so its committed golden carries the tag instead. -fn emitted_tag(phase: &str, src: &str) -> String { - let doc = dump(phase, src).map_or_else( - |_| json(&fixture_dir().join(format!("stable.{phase}.json"))), - |out| serde_json::from_str::(&out).expect("emitted JSON"), +// Replace every node number with a hole. Only `id` carries one, and nothing +// refers to a node by number, so erasing the values leaves every structural +// field of the document still under comparison. +fn erase_node_ids(doc: &str) -> String { + let mut out = String::with_capacity(doc.len()); + let mut rest = doc; + while let Some(at) = rest.find(NODE_ID_KEY) { + let (head, tail) = rest.split_at(at + NODE_ID_KEY.len()); + out.push_str(head); + let digits = tail.len() - tail.trim_start_matches(|c: char| c.is_ascii_digit()).len(); + if digits > 0 { + out.push_str(NODE_ID_HOLE); + } + rest = &tail[digits..]; + } + out.push_str(rest); + out +} + +// Erase what a comparison across releases must not depend on: the compiler +// stamp always, and the node numbering for a schema that does not promise it. +fn comparable(doc: &str, version: &str, export: Export) -> String { + let doc = doc.replace( + &format!("\"compiler\": \"{version}\""), + &format!("\"compiler\": \"{VERSION_HOLE}\""), ); + match export { + Export::Bytes => doc, + Export::Shape => erase_node_ids(&doc), + } +} + +// What the current exporter writes for one source, taken the way the command +// line takes it. The prelude is prepended, because a program that derives an +// instance or names a class does not resolve without it; the exporters rebase +// spans and drop prelude declarations, so the artifact still commits to the +// user's own file and the embedded source can be fed straight back in here. +// +// # Errors +// Propagates a front-end failure so the caller can name the artifact it came +// from. +fn today(phase: &str, source: &str) -> Result { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let full = with_prelude(source); + dump_on(phase, &full, &default_roots(root), &Config::from_env()) +} + +// The schema tag a phase actually emits. +fn emitted_tag(phase: &str, src: &str) -> String { + let out = today(phase, src).unwrap_or_else(|e| panic!("{phase}: dump: {e}")); + let doc: Value = serde_json::from_str(&out).expect("emitted JSON"); doc["schema"] .as_str() .unwrap_or_else(|| panic!("{phase}: no schema tag")) @@ -140,15 +246,19 @@ fn roundtrip(artifact: &Path, mode: &str) -> String { // to the exact bytes the older release wrote: the reader neither rejects the // older stamp nor silently rewrites the document into a newer shape. fn assert_stem_reads(stem: &str) { - for (family, mode) in RETAINED_FAMILIES { - let path = retained_dir().join(format!("{stem}.{family}.json")); + let mut checked = 0; + for (release, _, row) in retained_artifacts().filter(|(_, s, _)| *s == stem) { + let path = artifact(release, stem, row); let released = read(&path); - let out = roundtrip(&path, mode); assert_eq!( - out, released, - "{RETAINED}/{stem}.{family}: a retained release artifact must re-encode byte-identically" + roundtrip(&path, row.mode), + released, + "{release}/{stem}.{}: a retained release artifact must re-encode byte-identically", + row.phase ); + checked += 1; } + assert!(checked > 0, "{stem}: nothing retained to read"); } macro_rules! retained_reads { @@ -167,54 +277,46 @@ retained_reads! { retained_types_still_reads => "types", } -// Every retained artifact carries the stamp of the release it was cut from, so -// the corpus cannot be quietly refreshed into current output and keep claiming -// to test an older format. +// Every retained artifact carries the stamp of the release whose directory it +// sits in, so refreshing the corpus with current output cannot masquerade as an +// older-format test. #[test] fn retained_artifacts_carry_the_released_stamp() { - for stem in STEMS { - for (family, _) in RETAINED_FAMILIES { - let path = retained_dir().join(format!("{stem}.{family}.json")); - let doc = json(&path); - assert_eq!( - doc["compiler"], RETAINED, - "{stem}.{family}: retained artifact is not stamped {RETAINED}" - ); - } + for (release, stem, row) in retained_artifacts() { + let doc = json(&artifact(release, stem, row)); + assert_eq!( + doc["compiler"], release, + "{release}/{stem}.{}: retained artifact is not stamped {release}", + row.phase + ); } } -// The shape statement: the current exporter reproduces a retained artifact exactly, -// apart from the compiler stamp inside the envelope. Comparing the whole -// document rather than a schema tag is what makes the tag's claim honest, since -// an unbumped tag over a drifted shape is precisely the failure this catches. +// The shape statement: the current exporter reproduces a retained artifact from +// the source it carries, to whatever precision that schema promises. Comparing +// the document rather than a schema tag is what makes the tag's claim honest, +// since an unbumped tag over a drifted shape is precisely the failure this +// catches. #[test] fn retained_artifacts_match_todays_export_modulo_stamp() { - for stem in STEMS { - for (family, _) in RETAINED_FAMILIES { - let path = retained_dir().join(format!("{stem}.{family}.json")); - let released = read(&path); - let doc = json(&path); - let source = doc["source"]["text"] - .as_str() - .unwrap_or_else(|| panic!("{stem}.{family}: no embedded source")); - - let today = - dump(family, source).unwrap_or_else(|e| panic!("{stem}.{family}: dump: {e}")); - let today = format!("{today}\n"); - - let stamp = |doc: &str, version: &str| { - doc.replace( - &format!("\"compiler\": \"{version}\""), - "\"compiler\": \"\"", - ) - }; - assert_eq!( - stamp(&today, env!("CARGO_PKG_VERSION")), - stamp(&released, RETAINED), - "{stem}.{family}: the exported shape drifted from {RETAINED} without a schema bump" - ); - } + for (release, stem, row) in retained_artifacts() { + let path = artifact(release, stem, row); + let released = read(&path); + let doc = json(&path); + let source = doc["source"]["text"] + .as_str() + .unwrap_or_else(|| panic!("{release}/{stem}.{}: no embedded source", row.phase)); + + let exported = today(row.phase, source) + .unwrap_or_else(|e| panic!("{release}/{stem}.{}: dump: {e}", row.phase)); + let exported = format!("{exported}\n"); + + assert_eq!( + comparable(&exported, env!("CARGO_PKG_VERSION"), row.export), + comparable(&released, release, row.export), + "{release}/{stem}.{}: the exported shape drifted without a schema bump", + row.phase + ); } } @@ -225,39 +327,40 @@ fn retained_artifacts_match_todays_export_modulo_stamp() { #[test] fn other_schema_versions_are_refused() { let stem = STEMS[0]; - for (family, mode) in RETAINED_FAMILIES { - let path = retained_dir().join(format!("{stem}.{family}.json")); + for row in &MATRIX { + let release = releases_for(row) + .last() + .expect("a retained family carries at least one release"); + let path = artifact(release, stem, row); let released = read(&path); - let tag = MATRIX - .iter() - .find(|r| r.phase == family) - .unwrap_or_else(|| panic!("{family}: not in the compatibility matrix")) - .tag; assert!( - released.contains(tag), - "{stem}.{family}: retained artifact does not carry {tag}" + released.contains(row.tag), + "{release}/{stem}.{}: retained artifact does not carry {}", + row.phase, + row.tag ); for other in ["v0", "v2"] { - let stripped = tag + let stripped = row + .tag .strip_suffix("v1") - .unwrap_or_else(|| panic!("{tag}: schema tag is not version-suffixed")); - let retagged = released.replace(tag, &format!("{stripped}{other}")); - let tmp = std::env::temp_dir().join(format!("prism_compat_{family}_{other}.json")); + .unwrap_or_else(|| panic!("{}: schema tag is not version-suffixed", row.tag)); + let retagged = released.replace(row.tag, &format!("{stripped}{other}")); + let tmp = std::env::temp_dir().join(format!("prism_compat_{}_{other}.json", row.phase)); fs::write(&tmp, &retagged).expect("write retagged artifact"); - let out = roundtrip(&tmp, mode); + let out = roundtrip(&tmp, row.mode); assert!( out.starts_with("decode error: $.schema"), - "{family}: a {other} document was not refused on its tag, got: {out}" + "{}: a {other} document was not refused on its tag, got: {out}", + row.phase ); } } } -// The matrix is the record, so it must describe the schemas that exist. Every -// row's tag is the one the compiler actually emits, every `Read` row has a -// retained corpus stamped with the release it names, and every `New` row has -// nothing retained: a schema introduced here has no older document to read. +// The matrix is the record, so it must describe the schemas that exist: every +// row's tag is the one the compiler actually emits, and every row has a retained +// corpus in each release from the one it names onward. #[test] fn compatibility_matrix_matches_the_corpus() { let src = read(&fixture_dir().join(format!("{}.pr", STEMS[0]))); @@ -269,48 +372,34 @@ fn compatibility_matrix_matches_the_corpus() { row.phase ); - let retained: Vec = STEMS - .iter() - .map(|s| retained_dir().join(format!("{s}.{}.json", row.phase))) - .filter(|p| p.exists()) - .collect(); - match row.compat { - Compat::Read(release) => { - assert_eq!(release, RETAINED, "{}: unknown retained release", row.phase); - assert_eq!( - retained.len(), - STEMS.len(), - "{}: read-compatible schema is missing retained artifacts", - row.phase - ); - } - Compat::New => assert!( - retained.is_empty(), - "{}: a schema introduced in this release has retained artifacts", - row.phase - ), + for (release, stem, _) in retained_artifacts().filter(|(.., r)| r.phase == row.phase) { + let path = artifact(release, stem, row); + assert!( + path.exists(), + "{}: read-compatible schema is missing {}", + row.phase, + path.display() + ); } } - // Nothing sits in the retained corpus that the matrix does not describe. - let expected: Vec = STEMS - .iter() - .flat_map(|s| { - RETAINED_FAMILIES - .iter() - .map(move |(family, _)| format!("{s}.{family}.json")) - }) - .collect(); - let mut found: Vec = fs::read_dir(retained_dir()) - .expect("retained dir") - .filter_map(Result::ok) - .filter_map(|e| e.file_name().into_string().ok()) - .collect(); - found.sort_unstable(); - let mut expected = expected; - expected.sort_unstable(); - assert_eq!( - found, expected, - "the retained corpus and the compatibility matrix have drifted apart" - ); + // Nothing sits in a retained release that the matrix does not describe, and + // nothing the matrix describes is missing from disk. + for release in RETAINED { + let mut expected: Vec = retained_artifacts() + .filter(|(r, ..)| r == release) + .map(|(_, stem, row)| format!("{stem}.{}.json", row.phase)) + .collect(); + let mut found: Vec = fs::read_dir(released_dir(release)) + .unwrap_or_else(|e| panic!("{release}: retained dir: {e}")) + .filter_map(Result::ok) + .filter_map(|e| e.file_name().into_string().ok()) + .collect(); + expected.sort_unstable(); + found.sort_unstable(); + assert_eq!( + found, expected, + "{release}: the retained corpus and the compatibility matrix have drifted apart" + ); + } } diff --git a/tests/compiler/tc_rejection.rs b/tests/compiler/tc_rejection.rs new file mode 100644 index 00000000..6c440d8d --- /dev/null +++ b/tests/compiler/tc_rejection.rs @@ -0,0 +1,107 @@ +// The rejection seam: `dump tc-rejection`. Every other front-end fixture +// requires the checker to accept, so a Prism-written checker could be diffed +// only on what this compiler admits and never on what it refuses. This seam +// exports the negative half: the resolved tree desugar built before +// typechecking, plus the refusal's stable code, owning phase, and user-relative +// span. Coverage: +// +// 1. Determinism, on both verdicts. +// 2. An accepted program reports `accepted`, carries no error row, and its tree +// and source are exactly what `resolved-syntax` exports, so the two seams +// cannot render the same program differently. +// 3. A rejected program reports `rejected` with a type-phase code and a span +// inside the embedded user source, and still carries the resolved tree. +// 4. A program that fails before a resolved tree exists (a parse error) is +// refused outright rather than reported as a rejection with no tree. + +use std::fs; +use std::path::{Path, PathBuf}; + +use serde_json::Value; + +const FIXTURE_DIR: &str = "tests/fixtures/frontend"; +const PHASE: &str = "tc-rejection"; +// Re-typed independently of the emitter so a schema drift cannot re-pin the +// value it is checked against. +const SCHEMA: &str = "prism-tc-rejection-v1"; +const RESOLVED_PHASE: &str = "resolved-syntax"; +const ACCEPTED_STEM: &str = "program"; +const REJECTED_STEM: &str = "malformed_type"; +const UNRESOLVABLE_STEM: &str = "malformed_parse"; + +fn fixture(stem: &str) -> String { + let path: PathBuf = Path::new(env!("CARGO_MANIFEST_DIR")) + .join(FIXTURE_DIR) + .join(format!("{stem}.pr")); + fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display())) +} + +fn dump(stem: &str) -> Value { + let src = fixture(stem); + let out = prism::dump(PHASE, &src).unwrap_or_else(|e| panic!("{stem}: dump: {e}")); + let again = prism::dump(PHASE, &src).unwrap_or_else(|e| panic!("{stem}: dump: {e}")); + assert_eq!(out, again, "{stem}: must be byte-identical across runs"); + let doc: Value = serde_json::from_str(&out).unwrap_or_else(|e| panic!("{stem}: JSON: {e}")); + assert_eq!(doc["schema"], SCHEMA, "{stem}: schema tag"); + assert_eq!( + doc["compiler"], + env!("CARGO_PKG_VERSION"), + "{stem}: version" + ); + doc +} + +// An accepted program: the verdict is the only thing this seam adds, and the +// tree it reports is the one `resolved-syntax` already publishes. +#[test] +fn accepted_program_matches_the_resolved_seam() { + let doc = dump(ACCEPTED_STEM); + assert_eq!(doc["status"], "accepted"); + assert!(doc.get("error").is_none(), "accepted: no error row"); + + let src = fixture(ACCEPTED_STEM); + let resolved: Value = serde_json::from_str( + &prism::dump(RESOLVED_PHASE, &src).expect("resolved-syntax dump of an accepted program"), + ) + .expect("resolved-syntax is JSON"); + assert_eq!(doc["source"], resolved["source"], "embedded source"); + assert_eq!(doc["functions"], resolved["functions"], "resolved tree"); +} + +// A rejected program: the refusal is reported as data, and the resolved tree +// desugar built before the checker ran survives alongside it. +#[test] +fn rejected_program_reports_the_refusal_and_keeps_its_tree() { + let doc = dump(REJECTED_STEM); + assert_eq!(doc["status"], "rejected"); + + let error = &doc["error"]; + assert_eq!(error["phase"], "type", "the checker owns this refusal"); + let code = error["code"].as_str().expect("a stable code"); + assert!( + code.starts_with('E') && code[1..].chars().all(|c| c.is_ascii_digit()), + "malformed code {code}" + ); + + // The span addresses the embedded source, not the prelude-prefixed one. + let text = doc["source"]["text"].as_str().expect("embedded source"); + let span = error["span"].as_array().expect("a primary span"); + let (start, end) = ( + span[0].as_u64().expect("span start"), + span[1].as_u64().expect("span end"), + ); + let len = u64::try_from(text.len()).expect("source length"); + assert!(start < end && end <= len, "span [{start}, {end}]"); + + assert!( + !doc["functions"].as_array().expect("functions").is_empty(), + "the resolved tree of a rejected program" + ); +} + +// A refusal that lands before resolution has no tree to report against, so the +// seam fails rather than claiming a rejection with an empty program. +#[test] +fn unresolvable_program_is_refused_outright() { + assert!(prism::dump(PHASE, &fixture(UNRESOLVABLE_STEM)).is_err()); +} diff --git a/tests/cost_manifest.txt b/tests/cost_manifest.txt index 4bbfc9c5..d241530e 100644 --- a/tests/cost_manifest.txt +++ b/tests/cost_manifest.txt @@ -23,7 +23,7 @@ examples/chaos_swarm.pr 626049 919540 examples/chars.pr 2092 28 examples/class_pattern.pr 170 4 examples/classes.pr 176 3 -examples/cli.pr 25422 1081 +examples/cli.pr 25422 1085 examples/clock.pr 1905 2794 examples/collatz.pr 47353 8 examples/compose.pr 258 4 @@ -39,7 +39,7 @@ examples/eff_amb.pr 441136 91628 examples/eff_exn.pr 170 80 examples/eff_forward.pr 66 39 examples/eff_nontail.pr 138 101 -examples/eff_poly.pr 241 359 +examples/eff_poly.pr 241 109 examples/eff_reader.pr 56 1 examples/eff_rows.pr 560 180 examples/eff_state.pr 224 3 @@ -58,15 +58,15 @@ examples/fold_zoo.pr 2026 48 examples/guards.pr 3063 45 examples/handlers_funval.pr 144 4 examples/hangman.pr 8557 2 -examples/hashmap.pr 4832 242 +examples/hashmap.pr 4832 245 examples/hkt.pr 2226 64 examples/imperative.pr 8083 38 examples/indexing.pr 5200 606 examples/interaction.pr 9023 83 examples/interp.pr 79878 47124 -examples/json.pr 28729 209 +examples/json.pr 32944 194 examples/lambda.pr 408 137 -examples/leaderboard.pr 222512 327063 +examples/leaderboard.pr 206538 303150 examples/lens_derive.pr 121 2 examples/lexer.pr 7548 42 examples/life.pr 211252 308889 @@ -88,23 +88,23 @@ examples/param_effects.pr 305 12 examples/parse_dont_validate.pr 328 31 examples/pendulum.pr 7521186 1239364 examples/pipeline.pr 10397 15333 -examples/player_manual.pr 9612 1609 -examples/polynomial.pr 18822 283 +examples/player_manual.pr 7569 837 +examples/polynomial.pr 18822 318 examples/primes.pr 31249 504 examples/probe.pr 47 0 examples/proptest.pr 1332114 2103887 examples/protocol_ladder.pr 2753 462 examples/queens.pr 207521 16347 examples/rbtree.pr 3857 88 -examples/record_replay.pr 1829 2727 +examples/record_replay.pr 2622 3851 examples/records_demo.pr 1251 24 -examples/recursion_schemes.pr 2784 111 +examples/recursion_schemes.pr 2784 112 examples/recursion_zoo.pr 1297 30 examples/regex.pr 7585 147 examples/replay_concurrent.pr 5225 7725 examples/result_pipeline.pr 10821 94 -examples/same_fringe.pr 1131 385 -examples/scheduler.pr 937 299 +examples/same_fringe.pr 1131 1721 +examples/scheduler.pr 937 1374 examples/scheduler_policy.pr 4887 7219 examples/show.pr 630 13 examples/simd.pr 187 27 @@ -118,7 +118,7 @@ examples/systemf.pr 53017 69855 examples/systemf_dk.pr 62521 85992 examples/tagless.pr 165 0 examples/tensor_ops.pr 22168 2472 -examples/textutil.pr 14485 364 +examples/textutil.pr 13969 366 examples/time.pr 2088 116 examples/transact.pr 623 77 examples/tree.pr 1030 17 @@ -133,22 +133,23 @@ examples/world.pr 70524 204 examples/zipper.pr 445 9 examples/zygo.pr 1765 63 tests/cases/run/accum.pr 2496 0 -tests/cases/run/adapter_observation.pr 671 1051 +tests/cases/run/adapter_observation.pr 882 1351 tests/cases/run/annot.pr 77 2 tests/cases/run/arena_mixed.pr 970 15 tests/cases/run/array_value_semantics.pr 50 2 tests/cases/run/big_drop.pr 7250100 250004 tests/cases/run/big_show_overflow.pr 270084 0 tests/cases/run/bignum.pr 1252 6 -tests/cases/run/blit_seq.pr 1522 31 +tests/cases/run/blit_seq.pr 1614 31 tests/cases/run/block_ifelse.pr 175 0 tests/cases/run/block_match.pr 190 6 tests/cases/run/block_stmts.pr 394 6 tests/cases/run/bool_print.pr 221 1 tests/cases/run/borrow.pr 632 12 -tests/cases/run/buf_shared.pr 760 8 +tests/cases/run/buf_shared.pr 779 9 tests/cases/run/buffer_ops.pr 1519 13 -tests/cases/run/bytes_codec.pr 10938 57 +tests/cases/run/bytes_codec.pr 11922 56 +tests/cases/run/bytes_view.pr 25438 137 tests/cases/run/cancel_await.pr 5471 7950 tests/cases/run/cancel_completed.pr 3290 4769 tests/cases/run/cancel_finalizer.pr 8640 12461 @@ -167,17 +168,17 @@ tests/cases/run/comp_in_annotated_row.pr 2430 890 tests/cases/run/comp_map_once.pr 942 22 tests/cases/run/constrained_mutual.pr 388 9 tests/cases/run/control_effects.pr 7967 130 -tests/cases/run/control_validate.pr 9770 14586 +tests/cases/run/control_validate.pr 9770 587 tests/cases/run/curry.pr 232 7 tests/cases/run/curry_effect.pr 174 7 tests/cases/run/deep_effect_recursion.pr 7000066 3 tests/cases/run/default_args.pr 137 1 tests/cases/run/default_or.pr 1274 118 tests/cases/run/deriving.pr 2348 91 -tests/cases/run/deriving_json.pr 36092 1676 +tests/cases/run/deriving_json.pr 34023 1583 tests/cases/run/deriving_lens.pr 2248 52 tests/cases/run/deriving_phantom.pr 1177 16 -tests/cases/run/deriving_plate.pr 10958 15519 +tests/cases/run/deriving_plate.pr 10958 1675 tests/cases/run/dot_chains.pr 2829 46 tests/cases/run/dot_records.pr 133 2 tests/cases/run/dot_strings.pr 137 1 @@ -187,16 +188,21 @@ tests/cases/run/eff_eta_thunk.pr 46 23 tests/cases/run/eff_fn_list.pr 206 316 tests/cases/run/eff_fuse.pr 126 2 tests/cases/run/eff_pending_arg.pr 58 3 -tests/cases/run/eff_poly_fn_arg.pr 494 698 +tests/cases/run/eff_poly_fn_arg.pr 853 1218 +tests/cases/run/eff_poly_handler_install.pr 203 6 tests/cases/run/eff_row_forward.pr 99 4 +tests/cases/run/eff_row_unwitnessed.pr 106 147 tests/cases/run/eff_two_handlers.pr 66 2 tests/cases/run/effects_demo.pr 191 7 tests/cases/run/effop_tax.pr 3629 1 tests/cases/run/errors.pr 179 75 +tests/cases/run/evidence_residual_row_after_handle.pr 209 6 +tests/cases/run/evidence_residual_row_clause.pr 200 6 tests/cases/run/factorial.pr 212 0 tests/cases/run/fail_guard.pr 180 86 tests/cases/run/fib.pr 4247 0 tests/cases/run/fib_var.pr 6024 15 +tests/cases/run/field_projection_single.pr 20 0 tests/cases/run/final_ctl.pr 1320301 480143 tests/cases/run/fip.pr 666 13 tests/cases/run/fip_inplace.pr 809 19 @@ -217,11 +223,12 @@ tests/cases/run/graph_algorithms.pr 146098 1703 tests/cases/run/guards.pr 1075 40 tests/cases/run/handler_arms_answer_apart.pr 511 107 tests/cases/run/handler_funval.pr 228 5 +tests/cases/run/handler_implicit_return.pr 62 25 tests/cases/run/handler_partial_forward.pr 69 46 tests/cases/run/higher.pr 3270 48 tests/cases/run/ho_row_inst.pr 413 19 tests/cases/run/identifiable.pr 1618 30 -tests/cases/run/incr_diff.pr 139885 205861 +tests/cases/run/incr_diff.pr 143756 211693 tests/cases/run/indexing_ops.pr 1084 401 tests/cases/run/int_boundary.pr 261 0 tests/cases/run/int_low64_neg.pr 9766 4 @@ -244,7 +251,11 @@ tests/cases/run/list_show.pr 662 21 tests/cases/run/list_singleton.pr 809 22 tests/cases/run/local_constrained_let.pr 543 9 tests/cases/run/local_mono_combined.pr 9906 112 +tests/cases/run/local_mono_effectful_helper.pr 3160 147 tests/cases/run/local_mono_escape.pr 187 278 +tests/cases/run/local_mono_nontail_resume.pr 5030 118 +tests/cases/run/local_mono_state_rest.pr 397 114 +tests/cases/run/local_mono_two_entries.pr 4384 319 tests/cases/run/loop_break.pr 27575 13 tests/cases/run/loop_control.pr 9141 42 tests/cases/run/looptest.pr 1437 3 @@ -283,7 +294,7 @@ tests/cases/run/option_chain.pr 248 8 tests/cases/run/or_null.pr 56 0 tests/cases/run/or_null_layout.pr 301 5 tests/cases/run/ord_dispatch.pr 357 6 -tests/cases/run/ordered_wire_stopgap.pr 10917 16396 +tests/cases/run/ordered_wire_stopgap.pr 10701 16038 tests/cases/run/param_eff.pr 93 2 tests/cases/run/param_patterns.pr 413 9 tests/cases/run/parse_float.pr 215 16 @@ -296,26 +307,30 @@ tests/cases/run/primes.pr 31249 504 tests/cases/run/print_structural.pr 383 15 tests/cases/run/quickcheck_detect.pr 1769 2707 tests/cases/run/ranges.pr 9051 134 +tests/cases/run/record_pattern_rest.pr 153 9 tests/cases/run/reflect.pr 26 0 tests/cases/run/reified_under_world.pr 220 297 tests/cases/run/repeat_block.pr 155 1 tests/cases/run/replayable_ok.pr 78 2 tests/cases/run/rewrite_strategies.pr 31727 45001 tests/cases/run/rollback.pr 726 85 +tests/cases/run/row_widen_named_effect_list.pr 205 286 tests/cases/run/row_widen_task_list.pr 6082 8915 tests/cases/run/row_widen_user_effect.pr 149 210 tests/cases/run/scc_effect_row.pr 122 1 tests/cases/run/scientific.pr 86 7 -tests/cases/run/sequence_pull.pr 37231 1309 +tests/cases/run/sequence_pull.pr 37327 1309 tests/cases/run/set_ops.pr 16256 220 tests/cases/run/shadow_prelude.pr 19 0 +tests/cases/run/shadowed_field_binder.pr 369 22 tests/cases/run/show.pr 103 0 tests/cases/run/show_mangle_clash.pr 59 2 tests/cases/run/simd4_edge.pr 307 45 tests/cases/run/simd_edge.pr 199 28 tests/cases/run/sort_demo.pr 129 6 -tests/cases/run/stable_ladder.pr 3261726 5001502 +tests/cases/run/stable_ladder.pr 2273363 3485710 tests/cases/run/stable_migrations.pr 1322 18 +tests/cases/run/str_view.pr 4667 5 tests/cases/run/stream_take.pr 4225 158 tests/cases/run/streams_edge.pr 644 43 tests/cases/run/string_edges.pr 263 6 @@ -323,7 +338,7 @@ tests/cases/run/strings_lib.pr 874 6 tests/cases/run/sugar_if.pr 618 2 tests/cases/run/sugar_letpat.pr 305 3 tests/cases/run/sugar_try.pr 307 6 -tests/cases/run/syntax_hostile.pr 774803 8290 +tests/cases/run/syntax_hostile.pr 897519 8290 tests/cases/run/syntax_walk.pr 1965 71 tests/cases/run/tbuf.pr 212 22 tests/cases/run/teleport.pr 72 2 @@ -345,8 +360,8 @@ tests/cases/run/vec_nat.pr 212 9 tests/cases/run/visibility.pr 59 2 tests/cases/run/while_compound.pr 3567 17 tests/cases/run/while_state.pr 517 225 -tests/cases/run/wire_hostile.pr 32552 48836 -tests/cases/run/wire_laws.pr 12925970 19749600 +tests/cases/run/wire_hostile.pr 29482 43837 +tests/cases/run/wire_laws.pr 9507555 14524894 tests/cases/run/with_handler.pr 249 73 tests/cases/run/with_reader.pr 70 2 tests/cases/run/with_resource.pr 98 2 diff --git a/tests/differential.rs b/tests/differential.rs index 76e2888b..efe95d1a 100644 --- a/tests/differential.rs +++ b/tests/differential.rs @@ -1,6 +1,6 @@ -//! The differential oracles: optimizer-configuration equivalence, typed-spine -//! erasure identity, the Lean model cross-check, replay determinism, and the -//! runtime scrubber and suspension suites. +//! The differential oracles: optimizer-configuration equivalence, effect-tier +//! equivalence, typed-spine erasure identity, the Lean model cross-check, +//! replay determinism, and the runtime scrubber and suspension suites. mod support; @@ -10,6 +10,8 @@ mod determinism; mod gate; #[path = "differential/lean_fuzz.rs"] mod lean_fuzz; +#[path = "tier_equiv/gate.rs"] +mod tier_gate; #[path = "differential/typed_spine.rs"] mod typed_spine; diff --git a/tests/fixtures/arena_hostile.c b/tests/fixtures/arena_hostile.c index a5425345..1bbe8a0b 100644 --- a/tests/fixtures/arena_hostile.c +++ b/tests/fixtures/arena_hostile.c @@ -141,11 +141,13 @@ static void promotion_dag(void) { assert(is_cell(out) && !is_arena(out)); assert(arena_free_deep(out)); assert(op[PRISM_TAG_W] == 4 && op[PRISM_ARITY_W] == 4); - /* The shared arena child was reached by two paths; each got its own - * refcounted copy, structurally equal. */ + /* The shared arena child was reached by two paths; promotion copies it + * once, both paths share the one copy, and the copy's count carries both + * owners. */ long via_mid = ((long *)op[PRISM_HDR_WORDS])[PRISM_HDR_WORDS]; long direct = op[PRISM_HDR_WORDS + 1]; - assert(via_mid != direct && value_eq(via_mid, direct)); + assert(via_mid == direct && value_eq(via_mid, direct)); + assert(((long *)direct)[PRISM_RC_W] == 2); /* The ordinary middle cell was kept (not copied), fields rewritten. */ assert(op[PRISM_HDR_WORDS] == rc_mid); assert(op[PRISM_HDR_WORDS + 3] == rc_leaf); diff --git a/tests/fixtures/bootstrap/hostile/Syntax/Codec.pr b/tests/fixtures/bootstrap/hostile/Syntax/Codec.pr new file mode 100644 index 00000000..5aa53bb5 --- /dev/null +++ b/tests/fixtures/bootstrap/hostile/Syntax/Codec.pr @@ -0,0 +1,10 @@ +-- The same hazard one level down: `Syntax.Codec` lives in the standard +-- library, and a source directory root is searched before the embedded +-- standard library, so a file at this path shadows it for anything resolving +-- against the target's search path. The shadow decodes its input artifacts +-- through this module, so a target that supplied it could decide what the +-- checker believes it was given. + +fn decode_surface(text : String) : String = "" + +fn codec_error_message(text : String) : String = text diff --git a/tests/fixtures/bootstrap/hostile/Tc.pr b/tests/fixtures/bootstrap/hostile/Tc.pr new file mode 100644 index 00000000..428492ad --- /dev/null +++ b/tests/fixtures/bootstrap/hostile/Tc.pr @@ -0,0 +1,10 @@ +-- A checked project's own `Tc` module, sitting where module resolution would +-- find it first. The shadow checker imports `Tc`, so if the shadow ever +-- resolved against the target's search path this file would supply the +-- compiler's own oracle and the project would be choosing the implementation +-- that judges it. It exports none of the names the real checker exports, so +-- that mistake is loud rather than quiet. + +type Verdict = Always | Never + +fn verdict() : Verdict = Always diff --git a/tests/fixtures/bootstrap/hostile/t1.pr b/tests/fixtures/bootstrap/hostile/t1.pr new file mode 100644 index 00000000..ddba1fda --- /dev/null +++ b/tests/fixtures/bootstrap/hostile/t1.pr @@ -0,0 +1,100 @@ +type Box(a) = Box(a) + +type Point = Point { x: Int, y: Int } + +type Ranked = Ranked(forall a. (a) -> a) + +fn id(x) = x + +fn one() : Int = 1 + +fn add(x : Int, y : Int) : Int = x + y + +fn choose(flag : Bool, x : Int, y : Int) : Int = + if flag then + x + else + y + +fn pair(x : Int) : (Int, Bool) = (x, true) + +fn unbox(value : Box(Int)) : Int = + match value of + Box(x) => x + +fn use_id() : Int = id(one()) + +fn local(n : Int) : Int = + let bump = \(x : Int) -> x + 1 + bump(n) + +fn down(n : Int) : Int = + if n == 0 then + 0 + else + down(n - 1) + +fn origin() : Point = Point { x = 0, y = 0 } + +fn sum_point(point : Point) : Int = + match point of + Point { x = x, y = y } => x + y + +-- A written type variable is a quantifier of the declaration, and where it +-- lands in the scheme is part of the spelling: what the body forced the +-- checker to invent is quantified first, and what the signature wrote down +-- after it. `mixed_pair` is the case that can tell those two orders apart. +fn ident(x : a) : a = x + +fn swap(p : (a, b)) : (b, a) = + match p of + (x, y) => (y, x) + +fn use_fn(f : (a) -> b, x : a) : b = f(x) + +fn mixed_pair(x : a, y) = (x, y) + +-- The source names are intentionally not lexical. This distinguishes +-- first-occurrence order from an implementation that sorts written binders. +fn written_order(x : z, y : a) : (z, a) = (x, y) + +-- A local binding generalizes only the classes it owns. `leaked` aliases its +-- parameter, so its Boolean use must constrain the parameter rather than a +-- fresh copy. `kept_poly` owns its identity lambda and retains both uses. +fn leaked(y) = + let g = y + if g then + 1 + else + 2 + +fn kept_poly(n : Int) : (Int, Bool) = + let same = \(w) -> w + (same(n), same(true)) + +fn duplicate(x) = + let y = x + (y, y) + +-- Lists are deliberately outside T1. They count as uncovered syntax without +-- turning an otherwise matching shadow run into a disagreement. +fn later() : List(Int) = [1, 2] + +-- So is a quantifier written inside a signature: the canonical spelling +-- normalizes one leading `forall` and nothing deeper, so an inner binder would +-- keep its written name and two alpha-variants would compare unequal. +fn nested(g : forall a. (a) -> a) : Int = 1 + +-- An applied lower-case head is a higher-kinded variable, which the T1 value +-- representation cannot encode without silently treating `f` as a constructor. +fn higher_kinded(x : f(Int)) : f(Int) = x + +-- Rank-n types are refused at every annotation site, not only in a declaration +-- signature, so the compatibility conversion in the checker cannot erase one. +fn annotated_nested() : Int = (\(value) -> value : forall a. (a) -> a)(1) + +-- Constructor metadata is another route into type conversion. Using a +-- constructor with a rank-n field must be uncovered for the same reason. +fn rankn_field(value : Ranked) : Int = + match value of + Ranked(_f) => 1 diff --git a/tests/fixtures/bootstrap/t1.pr b/tests/fixtures/bootstrap/t1.pr index 65c28a22..ddba1fda 100644 --- a/tests/fixtures/bootstrap/t1.pr +++ b/tests/fixtures/bootstrap/t1.pr @@ -2,6 +2,8 @@ type Box(a) = Box(a) type Point = Point { x: Int, y: Int } +type Ranked = Ranked(forall a. (a) -> a) + fn id(x) = x fn one() : Int = 1 @@ -38,6 +40,61 @@ fn sum_point(point : Point) : Int = match point of Point { x = x, y = y } => x + y +-- A written type variable is a quantifier of the declaration, and where it +-- lands in the scheme is part of the spelling: what the body forced the +-- checker to invent is quantified first, and what the signature wrote down +-- after it. `mixed_pair` is the case that can tell those two orders apart. +fn ident(x : a) : a = x + +fn swap(p : (a, b)) : (b, a) = + match p of + (x, y) => (y, x) + +fn use_fn(f : (a) -> b, x : a) : b = f(x) + +fn mixed_pair(x : a, y) = (x, y) + +-- The source names are intentionally not lexical. This distinguishes +-- first-occurrence order from an implementation that sorts written binders. +fn written_order(x : z, y : a) : (z, a) = (x, y) + +-- A local binding generalizes only the classes it owns. `leaked` aliases its +-- parameter, so its Boolean use must constrain the parameter rather than a +-- fresh copy. `kept_poly` owns its identity lambda and retains both uses. +fn leaked(y) = + let g = y + if g then + 1 + else + 2 + +fn kept_poly(n : Int) : (Int, Bool) = + let same = \(w) -> w + (same(n), same(true)) + +fn duplicate(x) = + let y = x + (y, y) + -- Lists are deliberately outside T1. They count as uncovered syntax without -- turning an otherwise matching shadow run into a disagreement. fn later() : List(Int) = [1, 2] + +-- So is a quantifier written inside a signature: the canonical spelling +-- normalizes one leading `forall` and nothing deeper, so an inner binder would +-- keep its written name and two alpha-variants would compare unequal. +fn nested(g : forall a. (a) -> a) : Int = 1 + +-- An applied lower-case head is a higher-kinded variable, which the T1 value +-- representation cannot encode without silently treating `f` as a constructor. +fn higher_kinded(x : f(Int)) : f(Int) = x + +-- Rank-n types are refused at every annotation site, not only in a declaration +-- signature, so the compatibility conversion in the checker cannot erase one. +fn annotated_nested() : Int = (\(value) -> value : forall a. (a) -> a)(1) + +-- Constructor metadata is another route into type conversion. Using a +-- constructor with a rank-n field must be uncovered for the same reason. +fn rankn_field(value : Ranked) : Int = + match value of + Ranked(_f) => 1 diff --git a/tests/fixtures/bootstrap/t2.pr b/tests/fixtures/bootstrap/t2.pr index 3f804a36..37dc1d73 100644 --- a/tests/fixtures/bootstrap/t2.pr +++ b/tests/fixtures/bootstrap/t2.pr @@ -34,6 +34,67 @@ fn inferred(n : Int) : Int = tick() + n -- ... and settles closed when it performs nothing. fn still_pure(n : Int) : Int = double(n) +-- Written open rows belong to the enclosing declaration. Repeated uses of one +-- spelling share a row class; distinct spellings remain distinct classes. +fn relay(f : (a) -> b ! {Tick | e}, x : a) : b ! {Tick | e} = f(x) + +fn relay2(x : a, f : (a) -> b ! {| e}) : b ! { | e} = f(x) + +fn two_rows( + f : (Int) -> Int ! {| e}, + g : (Bool) -> Bool ! {| r} +) : ((Int) -> Int ! {| e}, (Bool) -> Bool ! {| r}) = + (f, g) + +-- The same source spelling in separate declarations must receive a fresh +-- brand, while the two occurrences within each declaration stay shared. +fn same_row_one(f : (Int) -> Int ! {| e}, x : Int) : Int = f(x) + +fn same_row_two(f : (Int) -> Int ! {| e}, x : Int) : Int = f(x) + +-- Calling the callback and performing another effect widens the callback and +-- ambient rows together. A rigid inference-time tail would reject this case. +fn widened(f : (Int) -> Int ! {Tick | e}, x : Int) : Int = + let _said = say(x) + f(x) + +-- A singleton row binder used only by an uncalled callback is omitted from the +-- canonical display, but remains in the structural scheme: an effectful +-- callback can still instantiate it at the call site. +fn no_call(f : () -> Unit ! {| e}) : Unit ! {Tick | e} = + let _ticked = tick() + () + +fn no_call_effectful() : Unit ! {Tick} = + no_call() fn + let _ticked = tick() + () + +-- Open rows in local lambda annotations need their own lexical row scope, +-- which K1d does not model. The authority accepts this declaration, while the +-- shadow must keep the nested annotation on its strict refusal path. +fn nested_open() : Unit = (\(action : () -> Unit ! {| e}) -> ())(\() -> ()) + +-- Expression ascriptions are a second local scope. They remain fail-closed +-- independently of the declaration-signature support added in K1d. +fn annotated_open() : Unit = (\() -> () : () -> Unit ! {| e})() + +-- Constructor fields also need a declaration-local row scope before they can +-- be represented. The authority accepts the free field tail; the shadow keeps +-- the constructor unavailable instead of erasing it. +type Runner = Runner(() -> Unit ! {| e}) + +fn make_runner() = Runner(\() -> ()) + +-- A hidden singleton binder must not consume a canonical index. The visible +-- row comes first, followed by the two visible type binders. +fn shifted( + unused : () -> Unit ! {| e}, + f : (a) -> b ! {| z}, + x : a +) : b ! { | z} = + f(x) + -- Recursion under a row: the self binding carries the same ambient row. fn countdown(n : Int) : Int ! {Tick} = if n == 0 then @@ -58,11 +119,105 @@ fn apply_to(f : (Int) -> Int, n : Int) : Int = f(n) fn deferred(n : Int) : Int = apply_to(\(x) -> double(x), n) --- A parameterized effect is deliberately outside the checked subset: its --- operations need a binder the shadow has no way to brand, so they stay absent --- from the environment and a body performing one counts as uncovered syntax --- without turning an otherwise matching run into a disagreement. +-- An effect's parameters are its own quantifiers, so an operation of +-- `effect Cell(a)` is a scheme over `a` performing `{Cell(a)}`. Instantiating +-- that scheme at a call mints one class shared by the row and the operation's +-- own types, which is how a `read()` used at `Int` and the row `{Cell(Int)}` +-- come to agree without either being written down. effect Cell(a) read() : a + write(a) : Unit + +effect Pair(a, b) + left() : a + right() : b + +effect Mark(a) + mark() : Unit + +type Sized(n : Nat) = Sized fn later() : Int ! {Cell(Int)} = read() + +fn stash(n : Int) : Unit ! {Cell(Int)} = write(n) + +-- The written row is shared through both a parameter and the performed +-- effect's type argument. Checking the declared upper bound must close only +-- the ambient tail, not this nested row witness. +fn callback_cell(f : () -> Unit ! {| e}) : Unit ! {Cell(() -> Unit ! {| e})} = + write(f) + +-- Nothing here says what the cell holds, so the declaration quantifies over it +-- and the row mentions the same binder the result does. +fn borrowed() = read() + +-- A written binder may occur only in the declared effect row. It is still a +-- rigid declaration variable and the reported scheme quantifies it. +fn marked() : Unit ! {Mark(a)} = mark() + +-- The row is an upper bound. Since the body never performs Mark, its written +-- variable disappears with the unused label rather than becoming vacuous. +fn unmarked() : Unit ! {Mark(a)} = () + +-- Type-level naturals have an exact VTy atom spelling and remain admissible. +fn nat_label() : Unit ! {Mark(Sized(3))} = mark() + +-- A parameterized effect and a bare one meet in one ambient row. +fn ticked_cell() : Int ! {Cell(Int), Tick} = read() + tick() + +-- The arguments are positional, so only an instantiation that lines them up +-- satisfies the row: under `Pair(Int, Bool)` it is `left` that answers with an +-- `Int`, and swapping the row's arguments would swap which one does. +fn use_pair() : (Bool, Int) ! {Pair(Int, Bool)} = (right(), left()) + +-- Canonical quantifiers follow inferred result occurrence, while the applied +-- label retains the effect declaration's positional argument mapping. +fn borrowed_pair() = (right(), left()) + +-- An operation naming a type variable that is not one of the effect's own +-- parameters is deliberately outside the checked subset: `b` needs a binder the +-- effect does not supply. It stays out of the typed environment but remains in +-- the declaration table, so a use is named operation coverage. Admission is +-- decided one operation at a time, so `hold` is unaffected by its neighbour. +effect Loose(a) + stray() : b + hold(a) : Unit + +fn holds() : Unit ! {Loose(Int)} = hold(1) + +fn strays() : Int ! {Loose(Int)} = stray() + +-- Operation schemes that the shadow cannot represent stay in the raw name +-- table, so their uses are explicit operation coverage rather than missing +-- bindings. The first carries an open row through a higher-order argument. +effect Run + run(() -> Int ! {| e}) : Int + +fn run_pure() : Int ! {Run} = run(\() -> 1) + +-- A row literal is a Row-kinded argument. VTy has no Row node, so admitting +-- this operation would erase `{Tick}` to a nominal placeholder. +type Task(e : Row) = Task(Int) + +-- Constructor lookup follows the same fail-closed Row-kind boundary as a +-- written Task type. This prevents a constructor scheme from erasing its row +-- parameter even when the result annotation does not mention it explicitly. +fn make_task() = Task(0) + +-- VTy has no Row-kinded data argument. This accepted authority declaration is +-- deliberately refused by the shadow before it can erase `e` to a type var. +fn task_id(task : Task(e)) : Task(e) = task + +effect Launch + launch(Task({Tick})) : Unit + +fn launch_tick() : Unit ! {Launch} = launch(Task(0)) + +-- Applied effect arguments are checked by the same lossless type boundary. +-- An unboxed tuple is valid Type syntax, but VTy cannot retain its product +-- representation, so this declaration remains deliberately uncovered. +fn unboxed_label() : Unit ! {Cell(#(Int))} = () + +-- Usage facts carry representation/calling constraints that VTy cannot store; +-- they are refused rather than silently erased inside an applied label. +fn usage_label() : Unit ! {Cell(((Int) -> Int) @ once)} = () diff --git a/tests/fixtures/bootstrap/t2_bare_row_label.pr b/tests/fixtures/bootstrap/t2_bare_row_label.pr new file mode 100644 index 00000000..15ca1025 --- /dev/null +++ b/tests/fixtures/bootstrap/t2_bare_row_label.pr @@ -0,0 +1,3 @@ +-- A lowercase row name must use tail syntax. In label position it is an +-- unknown effect, not an implicitly bound row variable. +fn bare_row_label(action : () -> Unit ! {e}) : Unit = () diff --git a/tests/fixtures/bootstrap/t3.pr b/tests/fixtures/bootstrap/t3.pr index cf8aa59b..0d0e3b93 100644 --- a/tests/fixtures/bootstrap/t3.pr +++ b/tests/fixtures/bootstrap/t3.pr @@ -100,20 +100,140 @@ fn nested() : Int = get() resume k => k(2) return r => r --- A partial handler is deliberately outside the checked subset: it leaves the --- operations it does not name in the residual row, which the shadow has no way --- to spell without the effect's full operation table. +-- A partial handler discharges an effect only when its clauses cover every +-- operation the body is known to perform. Here the body performs get and only +-- put is covered, so Store survives in the declaration's row. fn partial_cover() : Int ! {Store} = handle get() with partial { put(_v) resume k => k(()), return r => r } --- A named handler instance is outside the subset for the same kind of reason: --- its operations are dispatched through a binding rather than by name, so the --- effect a clause discharges is not readable from the clause alone. +-- The positive side of the same rule: get is used and get is covered, so +-- Store is discharged even though put is never named. +fn partial_covered() : Int = + handle get() with partial { + get() resume k => k(1), + return r => r + } + +fn stored_probe() : Int ! {Store} = get() + +-- A call through a declared row hides which operations run behind it, so +-- covering get is not evidence that the helper reaches nothing else of Store: +-- the effect survives. +fn partial_opaque() : Int ! {Store} = + handle stored_probe() with partial { + get() resume k => k(1), + return r => r + } + +-- A named handler introduces a fresh private effect and dispatches through its +-- binding, so the directed call is discharged without capturing an ordinary +-- Tick call in the surrounding scope. fn named_instance() : Int = with h <- handler tick() resume k => k(5) return r => r h.tick() + +-- One parameter vector is shared by every operation in the handled body and by +-- every clause. The argument, written value, read result, and clause answers +-- therefore remain one class rather than four independently generalized ones. +effect Cell(a) + read() : a + write(a) : Unit + +fn applied_handler(x) = + handle let _u = write(x) in read() with + once read() => x + once write(_v) => () + return r => r + +-- Linking happens when a function value is called, not only when an operation +-- name is the call head. Its latent applied row must join the handler's class +-- before that row is discharged. +fn function_valued_handler(action : () -> Int ! {Cell(Int)}) : Int = + handle action() with + once read() => 5 + once write(_v) => () + return r => r + +-- An inner anonymous handler shadows the outer argument vector only for its +-- handled body. Its clause runs after that scope is restored, so the inner read +-- answers with a String while the read in its clause reaches the outer Int +-- instance and may be added to one. +fn nested_applied_scope() : String = + handle (handle read() with { + once read() => let _n = read() + 1 in "inner", + once write(_v) => (), + return r => r + }) with + once read() => 7 + once write(_v) => () + return r => r + +-- A named handler is one fresh private instance. Repeated directed calls share +-- its parameter vector, even though the source effect remains parameterized. +fn named_consistent() : Int = + with h <- handler + once read() => 7 + once write(_v) => () + return r => r + let _a = h.write(1) + let _b = h.write(2) + h.read() + +-- Only directed calls use the private instance. A bare operation beside one +-- remains an ordinary ambient Cell call, and the two may carry different type +-- arguments without being conflated or discharged together. +fn named_ambient() : (Int, String) ! {Cell(String)} = + with h <- handler + once read() => 1 + once write(_v) => () + return r => r + let private = h.read() + let ambient = read() + (private, ambient) + +-- A local value may reuse the source operation name without changing what the +-- instance's synthetic method denotes. Directed lookup uses the operation +-- captured when the instance was introduced, not the later value binding. +fn named_operation_shadow() : Int = + with h <- handler + once read() => 3 + once write(_v) => () + return r => r + let read = \() -> "local" + let _local = read + h.read() + +-- Handler clause heads resolve in the operation namespace even when an outer +-- value has the same spelling. Anonymous and named introductions both capture +-- the declared Cell operation rather than the unrelated local function. +fn anonymous_outer_operation_shadow() : Unit = + let read = \() -> "local" + let _local = read + handle write(1) with + once read() => 3 + once write(_v) => () + return r => r + +fn named_outer_operation_shadow() : Int = + let read = \() -> "local" + let _local = read + with h <- handler + once read() => 4 + once write(_v) => () + return r => r + h.read() + +-- The resolver accepts both dot syntax and its explicit UFCS spelling. Once +-- the first argument resolves to an instance, `read(h)` is the same private +-- dispatch as `h.read()` and must reuse the instance vector. +fn named_explicit_instance() : Int = + with h <- handler + once read() => 5 + once write(_v) => () + return r => r + read(h) diff --git a/tests/fixtures/frontend/program.elab-input.json b/tests/fixtures/frontend/program.elab-input.json index 1b120932..11a2c4d2 100644 --- a/tests/fixtures/frontend/program.elab-input.json +++ b/tests/fixtures/frontend/program.elab-input.json @@ -1,6 +1,6 @@ { "schema": "prism-elab-input-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "input": { "imports": [], "types": [ @@ -206,7 +206,7 @@ "evidence": [ "Global(sameInt, [])" ], - "ty": "(Int, Int) -> Bool ! {?r26}" + "ty": "(Int, Int) -> Bool ! {?r32}" }, "14": { "ty": "Int" diff --git a/tests/fixtures/frontend/program.tc-facts.json b/tests/fixtures/frontend/program.tc-facts.json index dc4644bc..c577acb4 100644 --- a/tests/fixtures/frontend/program.tc-facts.json +++ b/tests/fixtures/frontend/program.tc-facts.json @@ -1,6 +1,6 @@ { "schema": "prism-tc-facts-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "decls": [ { "name": "getx", @@ -75,7 +75,7 @@ "evidence": [ "Global(sameInt, [])" ], - "ty": "(Int, Int) -> Bool ! {?r26}" + "ty": "(Int, Int) -> Bool ! {?r32}" }, "14": { "ty": "Int" diff --git a/tests/fixtures/frontend/program.tc-input.json b/tests/fixtures/frontend/program.tc-input.json index 498c846d..d67516ce 100644 --- a/tests/fixtures/frontend/program.tc-input.json +++ b/tests/fixtures/frontend/program.tc-input.json @@ -1,6 +1,6 @@ { "schema": "prism-tc-input-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "imports": [], "types": [ { diff --git a/tests/fixtures/hir/handler_residual.hir.json b/tests/fixtures/hir/handler_residual.hir.json index dbffcbbb..ec633721 100644 --- a/tests/fixtures/hir/handler_residual.hir.json +++ b/tests/fixtures/hir/handler_residual.hir.json @@ -53,7 +53,7 @@ "ty": "Int" }, "11": { - "ty": "(Int) -> Int ! {E, ?r2}" + "ty": "(Int) -> Int ! {E, ?r7}" }, "12": { "ty": "Int" diff --git a/tests/fixtures/hir/handler_residual_open.hir.json b/tests/fixtures/hir/handler_residual_open.hir.json index 6157b5fd..881d02b9 100644 --- a/tests/fixtures/hir/handler_residual_open.hir.json +++ b/tests/fixtures/hir/handler_residual_open.hir.json @@ -42,7 +42,7 @@ "ty": "Int" }, "8": { - "ty": "(Int) -> Int ! {Out, Wrap, ?r3}" + "ty": "(Int) -> Int ! {Out, Wrap, ?r8}" }, "9": { "ty": "Int" diff --git a/tests/fixtures/hir/polymorphic_effects.hir.json b/tests/fixtures/hir/polymorphic_effects.hir.json index 60a11d8f..cd9e80fd 100644 --- a/tests/fixtures/hir/polymorphic_effects.hir.json +++ b/tests/fixtures/hir/polymorphic_effects.hir.json @@ -99,7 +99,7 @@ "ty": "Int" }, "25": { - "ty": "(Int) -> Int ! {?r8}" + "ty": "(Int) -> Int ! {?r9}" }, "26": { "ty": "Int" diff --git a/tests/fixtures/language/soundness/named_instance_argument_mismatch.pr b/tests/fixtures/language/soundness/named_instance_argument_mismatch.pr new file mode 100644 index 00000000..fc870011 --- /dev/null +++ b/tests/fixtures/language/soundness/named_instance_argument_mismatch.pr @@ -0,0 +1,9 @@ +effect Cell(a) + write(a) : Unit + +fn main() = + with h <- handler + write(_value) resume k => k(()) + return r => r + let _first = h.write(1) + h.write("wrong") diff --git a/tests/fixtures/parser/hostile/layout/Syntax/Layout.pr b/tests/fixtures/parser/hostile/layout/Syntax/Layout.pr new file mode 100644 index 00000000..13f62e72 --- /dev/null +++ b/tests/fixtures/parser/hostile/layout/Syntax/Layout.pr @@ -0,0 +1,6 @@ +-- The same hazard one module over: layout insertion decides where a block +-- begins and ends, so a tree that supplied this file would decide what its own +-- source means before either parser saw a token. It exports none of the names +-- the real layout pass exports, so that mistake is loud rather than quiet. + +fn shadowed_layout() : Int = 0 diff --git a/tests/fixtures/parser/hostile/lex/Syntax/Lex.pr b/tests/fixtures/parser/hostile/lex/Syntax/Lex.pr new file mode 100644 index 00000000..5f5bb4ed --- /dev/null +++ b/tests/fixtures/parser/hostile/lex/Syntax/Lex.pr @@ -0,0 +1,8 @@ +-- A tree's own `Syntax.Lex`, sitting where module resolution finds it before +-- the embedded standard library. The differential witness reaches the lexer +-- through this module name, so a search path that included this directory would +-- let the tree under comparison choose the lexer that judges it. It exports none +-- of the names the real lexer exports, so that mistake is loud rather than +-- quiet. + +fn shadowed_lex() : Int = 0 diff --git a/tests/fixtures/parser/hostile/parse/Syntax/Parse.pr b/tests/fixtures/parser/hostile/parse/Syntax/Parse.pr new file mode 100644 index 00000000..3423ac78 --- /dev/null +++ b/tests/fixtures/parser/hostile/parse/Syntax/Parse.pr @@ -0,0 +1,7 @@ +-- The hazard at its sharpest: the module the differential witness imports to +-- re-parse the source it is comparing. A search path that reached this file +-- would hand the gate a parser supplied by the very tree it is judging, and the +-- comparison would certify a parser against itself. It exports none of the names +-- the real parser exports, so that mistake is loud rather than quiet. + +fn shadowed_parse() : Int = 0 diff --git a/tests/fixtures/parser/negative_parity.pr b/tests/fixtures/parser/negative_parity.pr index 80ac9349..c637f2c4 100644 --- a/tests/fixtures/parser/negative_parity.pr +++ b/tests/fixtures/parser/negative_parity.pr @@ -14,6 +14,24 @@ import Syntax.Source (Span) fn sorted(xs : List(String)) : List(String) = sort(xs) +-- Neither expectation set is authored here. The oracle's is the grammar's own +-- first set, surfaced by the host canonicalizer into the artifact; the shadow's +-- is whatever the cursor noted where it refused. Reporting the difference +-- rather than the offset makes a divergence say which tokens it is made of, so +-- a noteset that gains or loses one moves the report instead of leaving it true +-- for a new reason. +fn only_in(xs : List(String), ys : List(String)) : List(String) = + filter(\(x) -> not(elem(x, ys)), xs) + +fn commas(xs : List(String)) : String = + match xs of + Nil => "" + Cons(x, Nil) => x + Cons(x, rest) => "{x}, {commas(rest)}" + +fn expected_gap(got : List(String), want : List(String)) : String = + "missing [{commas(only_in(want, got))}], extra [{commas(only_in(got, want))}]" + fn same_related(a : List(Span), b : List(Span)) : Bool = match (a, b) of (Nil, Nil) => true @@ -41,7 +59,7 @@ fn against(text : String, want : Diagnostic) : String = elif not(d.span == want.span) then "span divergence: got {d.span.lo}:{d.span.hi}, want {want.span.lo}:{want.span.hi}" elif sorted(d.expected) /= sorted(want.expected) then - "expected-set divergence at {d.span.lo}" + "expected-set divergence at {d.span.lo}: {expected_gap(sorted(d.expected), sorted(want.expected))}" elif not(same_related(d.related, want.related)) then "related divergence at {d.span.lo}" else diff --git a/tests/fixtures/parser/parity.pr b/tests/fixtures/parser/parity.pr index a3469fa7..3692adbc 100644 --- a/tests/fixtures/parser/parity.pr +++ b/tests/fixtures/parser/parity.pr @@ -1,13 +1,13 @@ --- Differential witness: consume the Rust parser's canonical surface artifact, --- parse its embedded source again through the Prism-owned frontend, and compare --- the exact re-encoded AST bytes (including spans and synth bits). +-- Differential witness: consume pairs of Rust surface artifacts and mismatch +-- paths, parse each embedded source again through the Prism-owned frontend, and +-- compare the exact re-encoded AST bytes (including spans and synth bits). import Syntax.Lex (..) import Syntax.Codec (..) import Syntax.Parse (..) -fn main() = - match decode_surface(read_file(arg(0))) of +fn replay(artifact : String, mismatch : String) : Unit ! {FileSystem, IO} = + match decode_surface(read_file(artifact)) of Err(e) => println("decode {codec_error_message(e)}") Ok(want) => match parse_source(want.source.text) of @@ -23,5 +23,15 @@ fn main() = if encode_surface(got) == encode_surface(want) then println("ok") else - write_file(arg(1), encode_surface(got)) - println("mismatch {arg(1)}") + write_file(mismatch, encode_surface(got)) + println("mismatch {mismatch}") + +fn replay_pairs(argv : List(String)) : Unit ! {FileSystem, IO} = + match argv of + Nil => () + Cons(artifact, Cons(mismatch, rest)) => + replay(artifact, mismatch) + replay_pairs(rest) + _ => println("expected artifact/mismatch argument pairs") + +fn main() = replay_pairs(args()) diff --git a/tests/fixtures/prism_test/basic.manifest.bin b/tests/fixtures/prism_test/basic.manifest.bin index dae762ad..11c01232 100644 --- a/tests/fixtures/prism_test/basic.manifest.bin +++ b/tests/fixtures/prism_test/basic.manifest.bin @@ -1 +1 @@ -prism-test-manifest-v1Parser::always_failsParserParser@always_fails@73f38ecc6ecb77c0d695a4ee048a1e977c22a39536054351999efed73c25c92f@2a1b2f85023fd6d1db0af841e1798c00b4efda6dad485d8f3d512bbd5bf9af44/Parser::normalize_is_private_but_visible_inlineParser.Parser@normalize_is_private_but_visible_inline@d4a59a9adb98458e1c620b983681a3a9f018816deb5619cf8e47894e3cabce8c@2a1b2f85023fd6d1db0af841e1798c00b4efda6dad485d8f3d512bbd5bf9af44Parser::parse_ok_holdsParserParser@parse_ok_holds@8e1f2002bcbf10cf40c32d6fba49179dde845dc8eaf6bc3a6b5bc664b74a2588@2a1b2f85023fd6d1db0af841e1798c00b4efda6dad485d8f3d512bbd5bf9af44%Unreached::unreached_module_is_tested Unreached$Unreached@unreached_module_is_tested@6b97d2c05d5cf74e193c959816f7488056d032f18af128d2208b5a4886627e01@59831e3d115994b0e20d9d52b077ee104f2097dd0f56f3eaf98a25c14f9e9942,integration::public::public_api_is_reachableintegration::public+integration::public@public_api_is_reachable@43826bfa76e08debc7272c80ab7dbfe977c91f4d5113bc00918bd16c8bf09ba4@559a37dc2c2016f2671cd7f37285cf5ffe137c0eb96b72bac8f1c6f0d61d80d9 \ No newline at end of file +prism-test-manifest-v1Parser::always_failsParserParser@always_fails@73f38ecc6ecb77c0d695a4ee048a1e977c22a39536054351999efed73c25c92f@d9d8da76cd92db78f3396a0e00d9ec46f4a7610711de62ac26263b2bb668351f/Parser::normalize_is_private_but_visible_inlineParser.Parser@normalize_is_private_but_visible_inline@d4a59a9adb98458e1c620b983681a3a9f018816deb5619cf8e47894e3cabce8c@d9d8da76cd92db78f3396a0e00d9ec46f4a7610711de62ac26263b2bb668351fParser::parse_ok_holdsParserParser@parse_ok_holds@8e1f2002bcbf10cf40c32d6fba49179dde845dc8eaf6bc3a6b5bc664b74a2588@d9d8da76cd92db78f3396a0e00d9ec46f4a7610711de62ac26263b2bb668351f%Unreached::unreached_module_is_tested Unreached$Unreached@unreached_module_is_tested@6b97d2c05d5cf74e193c959816f7488056d032f18af128d2208b5a4886627e01@601d9d7c544d7df1eb638832df405ea98230dfb89bf43527e9613ed38fff81a3,integration::public::public_api_is_reachableintegration::public+integration::public@public_api_is_reachable@43826bfa76e08debc7272c80ab7dbfe977c91f4d5113bc00918bd16c8bf09ba4@f5f65999790115f66be61f4913a8b53919f137245be49a449865981f88a2138c \ No newline at end of file diff --git a/tests/fixtures/syntax/classes.resolved-syntax.json b/tests/fixtures/syntax/classes.resolved-syntax.json index 74b938e7..7b14753d 100644 --- a/tests/fixtures/syntax/classes.resolved-syntax.json +++ b/tests/fixtures/syntax/classes.resolved-syntax.json @@ -1,6 +1,6 @@ { "schema": "prism-resolved-syntax-v1", - "compiler": "0.18.0", + "compiler": "0.20.0", "source": { "digest": "91aaf0fef720c662e99baebd264ddf09f2fc3e91feab725bc4a16544f8a48b2a", "text": "-- Classes, instances, canonical designations, constrained functions.\nclass Same(a)\n same : (a, a) -> Bool\n\nclass Bigger(a) given Same(a)\n bigger : (a, a) -> Bool\n\ninstance sameInt : Same(Int)\n fn same(x, y) = x == y\n\ninstance samePair : Same((a, b)) given Same(a), Same(b)\n fn same(p, q) =\n match (p, q) of\n ((a1, b1), (a2, b2)) => same(a1, a2) && same(b1, b2)\n\ncanonical Same(Int) = sameInt\n\nfn alike(x : a, y : a) : Bool given Same(a) = same(x, y)\n\nfn where_helper(n : Int) : Int =\n double + offset\n where\n double = n * 2\n offset = 10\n" diff --git a/tests/fixtures/syntax/classes.surface-syntax.json b/tests/fixtures/syntax/classes.surface-syntax.json index cde614ec..f1f3caa6 100644 --- a/tests/fixtures/syntax/classes.surface-syntax.json +++ b/tests/fixtures/syntax/classes.surface-syntax.json @@ -1,6 +1,6 @@ { "schema": "prism-surface-syntax-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "91aaf0fef720c662e99baebd264ddf09f2fc3e91feab725bc4a16544f8a48b2a", "text": "-- Classes, instances, canonical designations, constrained functions.\nclass Same(a)\n same : (a, a) -> Bool\n\nclass Bigger(a) given Same(a)\n bigger : (a, a) -> Bool\n\ninstance sameInt : Same(Int)\n fn same(x, y) = x == y\n\ninstance samePair : Same((a, b)) given Same(a), Same(b)\n fn same(p, q) =\n match (p, q) of\n ((a1, b1), (a2, b2)) => same(a1, a2) && same(b1, b2)\n\ncanonical Same(Int) = sameInt\n\nfn alike(x : a, y : a) : Bool given Same(a) = same(x, y)\n\nfn where_helper(n : Int) : Int =\n double + offset\n where\n double = n * 2\n offset = 10\n" diff --git a/tests/fixtures/syntax/classes.syntax-diagnostics.json b/tests/fixtures/syntax/classes.syntax-diagnostics.json index 9fc1ecec..88f6a47e 100644 --- a/tests/fixtures/syntax/classes.syntax-diagnostics.json +++ b/tests/fixtures/syntax/classes.syntax-diagnostics.json @@ -1,6 +1,6 @@ { "schema": "prism-syntax-diagnostics-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "91aaf0fef720c662e99baebd264ddf09f2fc3e91feab725bc4a16544f8a48b2a", "text": "-- Classes, instances, canonical designations, constrained functions.\nclass Same(a)\n same : (a, a) -> Bool\n\nclass Bigger(a) given Same(a)\n bigger : (a, a) -> Bool\n\ninstance sameInt : Same(Int)\n fn same(x, y) = x == y\n\ninstance samePair : Same((a, b)) given Same(a), Same(b)\n fn same(p, q) =\n match (p, q) of\n ((a1, b1), (a2, b2)) => same(a1, a2) && same(b1, b2)\n\ncanonical Same(Int) = sameInt\n\nfn alike(x : a, y : a) : Bool given Same(a) = same(x, y)\n\nfn where_helper(n : Int) : Int =\n double + offset\n where\n double = n * 2\n offset = 10\n" diff --git a/tests/fixtures/syntax/classes.syntax-tokens.json b/tests/fixtures/syntax/classes.syntax-tokens.json index 77af9464..a33617c8 100644 --- a/tests/fixtures/syntax/classes.syntax-tokens.json +++ b/tests/fixtures/syntax/classes.syntax-tokens.json @@ -1,6 +1,6 @@ { "schema": "prism-syntax-tokens-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "91aaf0fef720c662e99baebd264ddf09f2fc3e91feab725bc4a16544f8a48b2a", "text": "-- Classes, instances, canonical designations, constrained functions.\nclass Same(a)\n same : (a, a) -> Bool\n\nclass Bigger(a) given Same(a)\n bigger : (a, a) -> Bool\n\ninstance sameInt : Same(Int)\n fn same(x, y) = x == y\n\ninstance samePair : Same((a, b)) given Same(a), Same(b)\n fn same(p, q) =\n match (p, q) of\n ((a1, b1), (a2, b2)) => same(a1, a2) && same(b1, b2)\n\ncanonical Same(Int) = sameInt\n\nfn alike(x : a, y : a) : Bool given Same(a) = same(x, y)\n\nfn where_helper(n : Int) : Int =\n double + offset\n where\n double = n * 2\n offset = 10\n" diff --git a/tests/fixtures/syntax/contracts.resolved-syntax.json b/tests/fixtures/syntax/contracts.resolved-syntax.json index cc73cc67..77c63a8d 100644 --- a/tests/fixtures/syntax/contracts.resolved-syntax.json +++ b/tests/fixtures/syntax/contracts.resolved-syntax.json @@ -1,6 +1,6 @@ { "schema": "prism-resolved-syntax-v1", - "compiler": "0.18.0", + "compiler": "0.20.0", "source": { "digest": "c025e67e984f56f67a95de415733f5b802c38515484bf092520c1cb9639993d1", "text": "-- Verification surface: logic fns, contracts, totality, disciplines.\nlogic fn nonneg(x : Int) : Bool = x >= 0\n\nfn clamp(x : Int, lo : Int, hi : Int) : Int\n requires lo <= hi\n ensures |r| nonneg(r)\n =\n if x < lo then\n lo\n elif x > hi then\n hi\n else\n x\n\ntotal fn down(n : Int) : Int\n decreases n\n =\n if n <= 0 then\n 0\n else\n down(n - 1)\n\nassume total fn trusted(n : Int) : Int = n\n\ntest fn clamp_holds() = assert_eq(clamp(5, 0, 3), 3)\n\nfip fn swap(p : (Int, Int)) : (Int, Int) =\n match p of\n (a, b) => (b, a)\n" diff --git a/tests/fixtures/syntax/contracts.surface-syntax.json b/tests/fixtures/syntax/contracts.surface-syntax.json index fadf630a..f27fb13c 100644 --- a/tests/fixtures/syntax/contracts.surface-syntax.json +++ b/tests/fixtures/syntax/contracts.surface-syntax.json @@ -1,6 +1,6 @@ { "schema": "prism-surface-syntax-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "c025e67e984f56f67a95de415733f5b802c38515484bf092520c1cb9639993d1", "text": "-- Verification surface: logic fns, contracts, totality, disciplines.\nlogic fn nonneg(x : Int) : Bool = x >= 0\n\nfn clamp(x : Int, lo : Int, hi : Int) : Int\n requires lo <= hi\n ensures |r| nonneg(r)\n =\n if x < lo then\n lo\n elif x > hi then\n hi\n else\n x\n\ntotal fn down(n : Int) : Int\n decreases n\n =\n if n <= 0 then\n 0\n else\n down(n - 1)\n\nassume total fn trusted(n : Int) : Int = n\n\ntest fn clamp_holds() = assert_eq(clamp(5, 0, 3), 3)\n\nfip fn swap(p : (Int, Int)) : (Int, Int) =\n match p of\n (a, b) => (b, a)\n" diff --git a/tests/fixtures/syntax/contracts.syntax-diagnostics.json b/tests/fixtures/syntax/contracts.syntax-diagnostics.json index 5abee719..a28cae69 100644 --- a/tests/fixtures/syntax/contracts.syntax-diagnostics.json +++ b/tests/fixtures/syntax/contracts.syntax-diagnostics.json @@ -1,6 +1,6 @@ { "schema": "prism-syntax-diagnostics-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "c025e67e984f56f67a95de415733f5b802c38515484bf092520c1cb9639993d1", "text": "-- Verification surface: logic fns, contracts, totality, disciplines.\nlogic fn nonneg(x : Int) : Bool = x >= 0\n\nfn clamp(x : Int, lo : Int, hi : Int) : Int\n requires lo <= hi\n ensures |r| nonneg(r)\n =\n if x < lo then\n lo\n elif x > hi then\n hi\n else\n x\n\ntotal fn down(n : Int) : Int\n decreases n\n =\n if n <= 0 then\n 0\n else\n down(n - 1)\n\nassume total fn trusted(n : Int) : Int = n\n\ntest fn clamp_holds() = assert_eq(clamp(5, 0, 3), 3)\n\nfip fn swap(p : (Int, Int)) : (Int, Int) =\n match p of\n (a, b) => (b, a)\n" diff --git a/tests/fixtures/syntax/contracts.syntax-tokens.json b/tests/fixtures/syntax/contracts.syntax-tokens.json index d0d0653a..1d2a01ae 100644 --- a/tests/fixtures/syntax/contracts.syntax-tokens.json +++ b/tests/fixtures/syntax/contracts.syntax-tokens.json @@ -1,6 +1,6 @@ { "schema": "prism-syntax-tokens-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "c025e67e984f56f67a95de415733f5b802c38515484bf092520c1cb9639993d1", "text": "-- Verification surface: logic fns, contracts, totality, disciplines.\nlogic fn nonneg(x : Int) : Bool = x >= 0\n\nfn clamp(x : Int, lo : Int, hi : Int) : Int\n requires lo <= hi\n ensures |r| nonneg(r)\n =\n if x < lo then\n lo\n elif x > hi then\n hi\n else\n x\n\ntotal fn down(n : Int) : Int\n decreases n\n =\n if n <= 0 then\n 0\n else\n down(n - 1)\n\nassume total fn trusted(n : Int) : Int = n\n\ntest fn clamp_holds() = assert_eq(clamp(5, 0, 3), 3)\n\nfip fn swap(p : (Int, Int)) : (Int, Int) =\n match p of\n (a, b) => (b, a)\n" diff --git a/tests/fixtures/syntax/decls.resolved-syntax.json b/tests/fixtures/syntax/decls.resolved-syntax.json index 6b9869ac..27f9216b 100644 --- a/tests/fixtures/syntax/decls.resolved-syntax.json +++ b/tests/fixtures/syntax/decls.resolved-syntax.json @@ -1,6 +1,6 @@ { "schema": "prism-resolved-syntax-v1", - "compiler": "0.18.0", + "compiler": "0.20.0", "source": { "digest": "c4c274309273958eba2bb8f5bf75f63b47455c5d720cabc3e66e11bee4d86564", "text": "-- Declaration families: imports, datatypes, aliases, constants, visibility.\nimport Data.List (map, filter)\nimport Json as J\nimport Data.Map (..)\n\npub type Color = Red | Green | Blue deriving (Eq, Show)\n\nopaque type Token = MkToken(Int)\n\nnewtype Meters = Meters(Float) deriving (Eq)\n\ntype Point = Point { x: Int, y: Int }\n\nalias Pair(a) = (a, a)\n\nerror NotFound(String)\n\nlet limit = 512\n\ndeprecated \"use limit\"\nlet old_limit = 256\n\npub fn origin() : Point = Point { x = 0, y = 0 }\n" diff --git a/tests/fixtures/syntax/decls.surface-syntax.json b/tests/fixtures/syntax/decls.surface-syntax.json index 486ccc26..09d9cc6d 100644 --- a/tests/fixtures/syntax/decls.surface-syntax.json +++ b/tests/fixtures/syntax/decls.surface-syntax.json @@ -1,6 +1,6 @@ { "schema": "prism-surface-syntax-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "c4c274309273958eba2bb8f5bf75f63b47455c5d720cabc3e66e11bee4d86564", "text": "-- Declaration families: imports, datatypes, aliases, constants, visibility.\nimport Data.List (map, filter)\nimport Json as J\nimport Data.Map (..)\n\npub type Color = Red | Green | Blue deriving (Eq, Show)\n\nopaque type Token = MkToken(Int)\n\nnewtype Meters = Meters(Float) deriving (Eq)\n\ntype Point = Point { x: Int, y: Int }\n\nalias Pair(a) = (a, a)\n\nerror NotFound(String)\n\nlet limit = 512\n\ndeprecated \"use limit\"\nlet old_limit = 256\n\npub fn origin() : Point = Point { x = 0, y = 0 }\n" diff --git a/tests/fixtures/syntax/decls.syntax-diagnostics.json b/tests/fixtures/syntax/decls.syntax-diagnostics.json index 88cb559a..07b6c5b3 100644 --- a/tests/fixtures/syntax/decls.syntax-diagnostics.json +++ b/tests/fixtures/syntax/decls.syntax-diagnostics.json @@ -1,6 +1,6 @@ { "schema": "prism-syntax-diagnostics-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "c4c274309273958eba2bb8f5bf75f63b47455c5d720cabc3e66e11bee4d86564", "text": "-- Declaration families: imports, datatypes, aliases, constants, visibility.\nimport Data.List (map, filter)\nimport Json as J\nimport Data.Map (..)\n\npub type Color = Red | Green | Blue deriving (Eq, Show)\n\nopaque type Token = MkToken(Int)\n\nnewtype Meters = Meters(Float) deriving (Eq)\n\ntype Point = Point { x: Int, y: Int }\n\nalias Pair(a) = (a, a)\n\nerror NotFound(String)\n\nlet limit = 512\n\ndeprecated \"use limit\"\nlet old_limit = 256\n\npub fn origin() : Point = Point { x = 0, y = 0 }\n" diff --git a/tests/fixtures/syntax/decls.syntax-tokens.json b/tests/fixtures/syntax/decls.syntax-tokens.json index 92246ae2..48c51af8 100644 --- a/tests/fixtures/syntax/decls.syntax-tokens.json +++ b/tests/fixtures/syntax/decls.syntax-tokens.json @@ -1,6 +1,6 @@ { "schema": "prism-syntax-tokens-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "c4c274309273958eba2bb8f5bf75f63b47455c5d720cabc3e66e11bee4d86564", "text": "-- Declaration families: imports, datatypes, aliases, constants, visibility.\nimport Data.List (map, filter)\nimport Json as J\nimport Data.Map (..)\n\npub type Color = Red | Green | Blue deriving (Eq, Show)\n\nopaque type Token = MkToken(Int)\n\nnewtype Meters = Meters(Float) deriving (Eq)\n\ntype Point = Point { x: Int, y: Int }\n\nalias Pair(a) = (a, a)\n\nerror NotFound(String)\n\nlet limit = 512\n\ndeprecated \"use limit\"\nlet old_limit = 256\n\npub fn origin() : Point = Point { x = 0, y = 0 }\n" diff --git a/tests/fixtures/syntax/effects.surface-syntax.json b/tests/fixtures/syntax/effects.surface-syntax.json index 565fe954..1fa8317c 100644 --- a/tests/fixtures/syntax/effects.surface-syntax.json +++ b/tests/fixtures/syntax/effects.surface-syntax.json @@ -1,6 +1,6 @@ { "schema": "prism-surface-syntax-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "1a75ebd0c18a7e9c7e73bfe2a4459147b401efd68545e115ad927342c0ca5f44", "text": "-- Effects and control: graded ops, handlers, masks, loops, state, errors.\neffect Ask\n ask() : Int\n\neffect Log\n never fail(String) : Int\n once emit(Int) : Unit\n\nerror Boom(String)\n\nfn handled() : Int =\n handle ask() + ask() with\n ask() resume k => k(21)\n return r => r\n\nfn sugar_arms() : Int =\n handle emit(ask()) with\n once ask() => 7\n never fail(msg) => 0\n val base = 5\n once emit(v) => ()\n return r => base()\n\nfn named() : Int =\n with f <- handler\n ask() resume k => k(1)\n return r => r\n f.ask()\n\nfn tunneled() : Int =\n handle mask(ask()) with\n ask() resume k => k(2)\n return r => r\n\nfn thrower(n : Int) : Int =\n if n < 0 then\n throw Boom(\"negative\")\n else\n n\n\nfn catcher() : Int = try thrower(-1) catch { Boom(msg) => 0 }\n\nfn looping() : Int =\n var acc := 0\n var i := 0\n while i < 10 do\n i += 1\n if i % 2 == 0 then\n continue\n if i > 7 then\n break\n acc := acc + i\n for v in [1, 2, 3],\n if v > 1,\n let w = v * 2 do\n acc += w\n loop\n break\n acc\n\nfn early(n : Int) : Int =\n if n == 0 then\n return 99\n n + 1\n\nfn atomic() : Int =\n var balance := 100\n let r =\n transact\n balance -= 40\n guard(balance >= 80)\n 1\n else\n 0\n probe \"trace\" do println(\"attempted\")\n balance\n\nfn grid_write() : List(List(Int)) =\n var grid := [[0, 1], [2, 3]]\n grid[0][1] := 9\n grid\n" diff --git a/tests/fixtures/syntax/effects.syntax-diagnostics.json b/tests/fixtures/syntax/effects.syntax-diagnostics.json index e534843e..3ed4e07b 100644 --- a/tests/fixtures/syntax/effects.syntax-diagnostics.json +++ b/tests/fixtures/syntax/effects.syntax-diagnostics.json @@ -1,6 +1,6 @@ { "schema": "prism-syntax-diagnostics-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "1a75ebd0c18a7e9c7e73bfe2a4459147b401efd68545e115ad927342c0ca5f44", "text": "-- Effects and control: graded ops, handlers, masks, loops, state, errors.\neffect Ask\n ask() : Int\n\neffect Log\n never fail(String) : Int\n once emit(Int) : Unit\n\nerror Boom(String)\n\nfn handled() : Int =\n handle ask() + ask() with\n ask() resume k => k(21)\n return r => r\n\nfn sugar_arms() : Int =\n handle emit(ask()) with\n once ask() => 7\n never fail(msg) => 0\n val base = 5\n once emit(v) => ()\n return r => base()\n\nfn named() : Int =\n with f <- handler\n ask() resume k => k(1)\n return r => r\n f.ask()\n\nfn tunneled() : Int =\n handle mask(ask()) with\n ask() resume k => k(2)\n return r => r\n\nfn thrower(n : Int) : Int =\n if n < 0 then\n throw Boom(\"negative\")\n else\n n\n\nfn catcher() : Int = try thrower(-1) catch { Boom(msg) => 0 }\n\nfn looping() : Int =\n var acc := 0\n var i := 0\n while i < 10 do\n i += 1\n if i % 2 == 0 then\n continue\n if i > 7 then\n break\n acc := acc + i\n for v in [1, 2, 3],\n if v > 1,\n let w = v * 2 do\n acc += w\n loop\n break\n acc\n\nfn early(n : Int) : Int =\n if n == 0 then\n return 99\n n + 1\n\nfn atomic() : Int =\n var balance := 100\n let r =\n transact\n balance -= 40\n guard(balance >= 80)\n 1\n else\n 0\n probe \"trace\" do println(\"attempted\")\n balance\n\nfn grid_write() : List(List(Int)) =\n var grid := [[0, 1], [2, 3]]\n grid[0][1] := 9\n grid\n" diff --git a/tests/fixtures/syntax/effects.syntax-tokens.json b/tests/fixtures/syntax/effects.syntax-tokens.json index 9adf21fc..c97495e2 100644 --- a/tests/fixtures/syntax/effects.syntax-tokens.json +++ b/tests/fixtures/syntax/effects.syntax-tokens.json @@ -1,6 +1,6 @@ { "schema": "prism-syntax-tokens-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "1a75ebd0c18a7e9c7e73bfe2a4459147b401efd68545e115ad927342c0ca5f44", "text": "-- Effects and control: graded ops, handlers, masks, loops, state, errors.\neffect Ask\n ask() : Int\n\neffect Log\n never fail(String) : Int\n once emit(Int) : Unit\n\nerror Boom(String)\n\nfn handled() : Int =\n handle ask() + ask() with\n ask() resume k => k(21)\n return r => r\n\nfn sugar_arms() : Int =\n handle emit(ask()) with\n once ask() => 7\n never fail(msg) => 0\n val base = 5\n once emit(v) => ()\n return r => base()\n\nfn named() : Int =\n with f <- handler\n ask() resume k => k(1)\n return r => r\n f.ask()\n\nfn tunneled() : Int =\n handle mask(ask()) with\n ask() resume k => k(2)\n return r => r\n\nfn thrower(n : Int) : Int =\n if n < 0 then\n throw Boom(\"negative\")\n else\n n\n\nfn catcher() : Int = try thrower(-1) catch { Boom(msg) => 0 }\n\nfn looping() : Int =\n var acc := 0\n var i := 0\n while i < 10 do\n i += 1\n if i % 2 == 0 then\n continue\n if i > 7 then\n break\n acc := acc + i\n for v in [1, 2, 3],\n if v > 1,\n let w = v * 2 do\n acc += w\n loop\n break\n acc\n\nfn early(n : Int) : Int =\n if n == 0 then\n return 99\n n + 1\n\nfn atomic() : Int =\n var balance := 100\n let r =\n transact\n balance -= 40\n guard(balance >= 80)\n 1\n else\n 0\n probe \"trace\" do println(\"attempted\")\n balance\n\nfn grid_write() : List(List(Int)) =\n var grid := [[0, 1], [2, 3]]\n grid[0][1] := 9\n grid\n" diff --git a/tests/fixtures/syntax/exprs.surface-syntax.json b/tests/fixtures/syntax/exprs.surface-syntax.json index 0a5584b3..49682d04 100644 --- a/tests/fixtures/syntax/exprs.surface-syntax.json +++ b/tests/fixtures/syntax/exprs.surface-syntax.json @@ -1,6 +1,6 @@ { "schema": "prism-surface-syntax-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "315cd0cf1b228c9cac33697d29700821a508568b5d66084badb33ef0c4e1714c", "text": "-- Expression forms: operators, records, paths, ranges, comprehensions.\ntype Wheel = Wheel { psi: Float }\n\ntype Car = Car { name: String, wheels: List(Wheel) }\n\nfn arith(x : Int, y : Int) : Int = -x + y * 2 - x % 3 + x ^ 2\n\nfn logic_ops(a : Bool, b : Bool) : Bool = a && b || not(a)\n\nfn compare(x : Int, y : Int) : Bool =\n x < y && x <= y && x > y || x >= y || x == y || x /= y\n\nfn pipes(xs : List(Int)) : Int = xs |> map(\\(v) -> v + 1) |> length\n\nfn composed(f : (Int) -> Int, g : (Int) -> Int) : (Int) -> Int = f >> g << f\n\nfn records(c : Car) : Car =\n let bumped =\n { c\n | name = \"vx\"\n , wheels.each.psi ~ \\(p) -> p + 1.0\n }\n { bumped | wheels[0].psi = 30.0 }\n\nfn reads(c : Car) : List(Float) = c.[wheels.each.psi]\n\nfn collections() : List(Int) =\n let squares = [v * v for v in [1..5], if v % 2 == 1, let w = v + 1]\n let pair = (1, \"two\")\n let evens = [2, 4..10]\n append(squares, evens)\n\nfn stringy(name : String) : String = \"hello {name}, {1 + 2} wide\"\n\nfn indexed(xs : List(Int)) : Int = xs[0] ?? 0\n\nfn chained(c : Option(Car)) : String = c?.name ?? \"none\"\n\nfn multikey(t : Tensor(Float)) : Float = t[1, 2]\n\nfn holey(x : Int) : Int = ?fill\n\ntype Lot = Parked(Int) | Vacant\n\nfn fallback(l : Lot) : Int =\n let Parked(n) = l else 0\n n + 1\n\nfn fallback_line(l : Lot) : Int =\n let Parked(n) = l else 7\n n * 2\n" diff --git a/tests/fixtures/syntax/exprs.syntax-diagnostics.json b/tests/fixtures/syntax/exprs.syntax-diagnostics.json index 5d2bbfc8..edbef3a7 100644 --- a/tests/fixtures/syntax/exprs.syntax-diagnostics.json +++ b/tests/fixtures/syntax/exprs.syntax-diagnostics.json @@ -1,6 +1,6 @@ { "schema": "prism-syntax-diagnostics-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "315cd0cf1b228c9cac33697d29700821a508568b5d66084badb33ef0c4e1714c", "text": "-- Expression forms: operators, records, paths, ranges, comprehensions.\ntype Wheel = Wheel { psi: Float }\n\ntype Car = Car { name: String, wheels: List(Wheel) }\n\nfn arith(x : Int, y : Int) : Int = -x + y * 2 - x % 3 + x ^ 2\n\nfn logic_ops(a : Bool, b : Bool) : Bool = a && b || not(a)\n\nfn compare(x : Int, y : Int) : Bool =\n x < y && x <= y && x > y || x >= y || x == y || x /= y\n\nfn pipes(xs : List(Int)) : Int = xs |> map(\\(v) -> v + 1) |> length\n\nfn composed(f : (Int) -> Int, g : (Int) -> Int) : (Int) -> Int = f >> g << f\n\nfn records(c : Car) : Car =\n let bumped =\n { c\n | name = \"vx\"\n , wheels.each.psi ~ \\(p) -> p + 1.0\n }\n { bumped | wheels[0].psi = 30.0 }\n\nfn reads(c : Car) : List(Float) = c.[wheels.each.psi]\n\nfn collections() : List(Int) =\n let squares = [v * v for v in [1..5], if v % 2 == 1, let w = v + 1]\n let pair = (1, \"two\")\n let evens = [2, 4..10]\n append(squares, evens)\n\nfn stringy(name : String) : String = \"hello {name}, {1 + 2} wide\"\n\nfn indexed(xs : List(Int)) : Int = xs[0] ?? 0\n\nfn chained(c : Option(Car)) : String = c?.name ?? \"none\"\n\nfn multikey(t : Tensor(Float)) : Float = t[1, 2]\n\nfn holey(x : Int) : Int = ?fill\n\ntype Lot = Parked(Int) | Vacant\n\nfn fallback(l : Lot) : Int =\n let Parked(n) = l else 0\n n + 1\n\nfn fallback_line(l : Lot) : Int =\n let Parked(n) = l else 7\n n * 2\n" diff --git a/tests/fixtures/syntax/exprs.syntax-tokens.json b/tests/fixtures/syntax/exprs.syntax-tokens.json index 8a8f23ee..bcac1828 100644 --- a/tests/fixtures/syntax/exprs.syntax-tokens.json +++ b/tests/fixtures/syntax/exprs.syntax-tokens.json @@ -1,6 +1,6 @@ { "schema": "prism-syntax-tokens-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "315cd0cf1b228c9cac33697d29700821a508568b5d66084badb33ef0c4e1714c", "text": "-- Expression forms: operators, records, paths, ranges, comprehensions.\ntype Wheel = Wheel { psi: Float }\n\ntype Car = Car { name: String, wheels: List(Wheel) }\n\nfn arith(x : Int, y : Int) : Int = -x + y * 2 - x % 3 + x ^ 2\n\nfn logic_ops(a : Bool, b : Bool) : Bool = a && b || not(a)\n\nfn compare(x : Int, y : Int) : Bool =\n x < y && x <= y && x > y || x >= y || x == y || x /= y\n\nfn pipes(xs : List(Int)) : Int = xs |> map(\\(v) -> v + 1) |> length\n\nfn composed(f : (Int) -> Int, g : (Int) -> Int) : (Int) -> Int = f >> g << f\n\nfn records(c : Car) : Car =\n let bumped =\n { c\n | name = \"vx\"\n , wheels.each.psi ~ \\(p) -> p + 1.0\n }\n { bumped | wheels[0].psi = 30.0 }\n\nfn reads(c : Car) : List(Float) = c.[wheels.each.psi]\n\nfn collections() : List(Int) =\n let squares = [v * v for v in [1..5], if v % 2 == 1, let w = v + 1]\n let pair = (1, \"two\")\n let evens = [2, 4..10]\n append(squares, evens)\n\nfn stringy(name : String) : String = \"hello {name}, {1 + 2} wide\"\n\nfn indexed(xs : List(Int)) : Int = xs[0] ?? 0\n\nfn chained(c : Option(Car)) : String = c?.name ?? \"none\"\n\nfn multikey(t : Tensor(Float)) : Float = t[1, 2]\n\nfn holey(x : Int) : Int = ?fill\n\ntype Lot = Parked(Int) | Vacant\n\nfn fallback(l : Lot) : Int =\n let Parked(n) = l else 0\n n + 1\n\nfn fallback_line(l : Lot) : Int =\n let Parked(n) = l else 7\n n * 2\n" diff --git a/tests/fixtures/syntax/interp.surface-syntax.json b/tests/fixtures/syntax/interp.surface-syntax.json index e58c109d..8741dc86 100644 --- a/tests/fixtures/syntax/interp.surface-syntax.json +++ b/tests/fixtures/syntax/interp.surface-syntax.json @@ -1,6 +1,6 @@ { "schema": "prism-surface-syntax-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "985da653818178a2b85a9be956094a5acc7ae10b7cc450d6abf24e4d8cdc08ff", "text": "-- Interpolation and layout edges: nested holes, bare-indent bodies, blanks.\neffect Env\n read(String) : String\n\nclass Render(a)\n render : (a) -> String\n\ninstance renderInt : Render(Int)\n fn render(v) = \"v={v}\"\n\nfn nested(name : String) : String =\n \"outer {concat(\"inner {name} deep\", read(\"home\"))} tail\"\n\nfn multiline() : Int =\n let a = let b = 1 in let c = 2 in b + c\n a\n\n-- The raw form is the same token and the same kind of value; only its spelling\n-- differs. Its body has no holes and no escapes, so a brace is a brace and a\n-- backslash is a backslash, and the indentation every line shares belongs to\n-- this file rather than to the string.\nfn raw() : String = r\"\"\"\n {not_a_hole} and \\n\n one step further in\n \"\"\"\n" diff --git a/tests/fixtures/syntax/interp.syntax-diagnostics.json b/tests/fixtures/syntax/interp.syntax-diagnostics.json index 60ceea76..c3405c4c 100644 --- a/tests/fixtures/syntax/interp.syntax-diagnostics.json +++ b/tests/fixtures/syntax/interp.syntax-diagnostics.json @@ -1,6 +1,6 @@ { "schema": "prism-syntax-diagnostics-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "985da653818178a2b85a9be956094a5acc7ae10b7cc450d6abf24e4d8cdc08ff", "text": "-- Interpolation and layout edges: nested holes, bare-indent bodies, blanks.\neffect Env\n read(String) : String\n\nclass Render(a)\n render : (a) -> String\n\ninstance renderInt : Render(Int)\n fn render(v) = \"v={v}\"\n\nfn nested(name : String) : String =\n \"outer {concat(\"inner {name} deep\", read(\"home\"))} tail\"\n\nfn multiline() : Int =\n let a = let b = 1 in let c = 2 in b + c\n a\n\n-- The raw form is the same token and the same kind of value; only its spelling\n-- differs. Its body has no holes and no escapes, so a brace is a brace and a\n-- backslash is a backslash, and the indentation every line shares belongs to\n-- this file rather than to the string.\nfn raw() : String = r\"\"\"\n {not_a_hole} and \\n\n one step further in\n \"\"\"\n" diff --git a/tests/fixtures/syntax/interp.syntax-tokens.json b/tests/fixtures/syntax/interp.syntax-tokens.json index 560fd4b0..ff8a5a2d 100644 --- a/tests/fixtures/syntax/interp.syntax-tokens.json +++ b/tests/fixtures/syntax/interp.syntax-tokens.json @@ -1,6 +1,6 @@ { "schema": "prism-syntax-tokens-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "985da653818178a2b85a9be956094a5acc7ae10b7cc450d6abf24e4d8cdc08ff", "text": "-- Interpolation and layout edges: nested holes, bare-indent bodies, blanks.\neffect Env\n read(String) : String\n\nclass Render(a)\n render : (a) -> String\n\ninstance renderInt : Render(Int)\n fn render(v) = \"v={v}\"\n\nfn nested(name : String) : String =\n \"outer {concat(\"inner {name} deep\", read(\"home\"))} tail\"\n\nfn multiline() : Int =\n let a = let b = 1 in let c = 2 in b + c\n a\n\n-- The raw form is the same token and the same kind of value; only its spelling\n-- differs. Its body has no holes and no escapes, so a brace is a brace and a\n-- backslash is a backslash, and the indentation every line shares belongs to\n-- this file rather than to the string.\nfn raw() : String = r\"\"\"\n {not_a_hole} and \\n\n one step further in\n \"\"\"\n" diff --git a/tests/fixtures/syntax/malformed_empty_hole.syntax-diagnostics.json b/tests/fixtures/syntax/malformed_empty_hole.syntax-diagnostics.json index beedffc1..5a0144fc 100644 --- a/tests/fixtures/syntax/malformed_empty_hole.syntax-diagnostics.json +++ b/tests/fixtures/syntax/malformed_empty_hole.syntax-diagnostics.json @@ -1,6 +1,6 @@ { "schema": "prism-syntax-diagnostics-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "e58d35529c8c7e03570b152a0654de3d3e22bc112b27977d6668fc070eb90145", "text": "-- An interpolation hole with no expression: the lexer refuses this file at the\n-- opening brace, the compiler's `LexError::EmptyHole` (E7001).\nfn main() = println(\"value = {}\")\n" diff --git a/tests/fixtures/syntax/malformed_invalid.syntax-diagnostics.json b/tests/fixtures/syntax/malformed_invalid.syntax-diagnostics.json index 7a92ada8..fb0d4cc3 100644 --- a/tests/fixtures/syntax/malformed_invalid.syntax-diagnostics.json +++ b/tests/fixtures/syntax/malformed_invalid.syntax-diagnostics.json @@ -1,6 +1,6 @@ { "schema": "prism-syntax-diagnostics-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "5864fcf0b577338014f14188f1c2a562fb8021da0b5ef51bcc2ec2510491ce37", "text": "-- An unknown backslash escape in a string: the lexer refuses this file. The\n-- fault lifts to the opening quote, the compiler's `LexError::Invalid` (E7000).\nfn main() = println(\"\\y\")\n" diff --git a/tests/fixtures/syntax/malformed_lex.syntax-diagnostics.json b/tests/fixtures/syntax/malformed_lex.syntax-diagnostics.json index 266675c5..adef993d 100644 --- a/tests/fixtures/syntax/malformed_lex.syntax-diagnostics.json +++ b/tests/fixtures/syntax/malformed_lex.syntax-diagnostics.json @@ -1,6 +1,6 @@ { "schema": "prism-syntax-diagnostics-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "41358ee7e6ea6932261367885e0bb97e10b5ae0d0f9d51d621e376736953fae9", "text": "-- An unterminated string literal: the lexer refuses this file.\nfn main() = println(\"oops\n" diff --git a/tests/fixtures/syntax/malformed_number_sep.syntax-diagnostics.json b/tests/fixtures/syntax/malformed_number_sep.syntax-diagnostics.json index bf46d750..c6bdba58 100644 --- a/tests/fixtures/syntax/malformed_number_sep.syntax-diagnostics.json +++ b/tests/fixtures/syntax/malformed_number_sep.syntax-diagnostics.json @@ -1,6 +1,6 @@ { "schema": "prism-syntax-diagnostics-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "f34b567df8b88104214207b5384b9d23c70713c6d6bf4f088ccfd7584909d6a4", "text": "-- A misplaced digit separator: the lexer refuses this file at the `_`, the\n-- compiler's `LexError::NumberSeparator` (E7004). A `_` must sit between two\n-- digits, so a trailing one is malformed.\nfn main() = println(show_int(1_))\n" diff --git a/tests/fixtures/syntax/malformed_parse.syntax-diagnostics.json b/tests/fixtures/syntax/malformed_parse.syntax-diagnostics.json index 5a5a49a6..7269529b 100644 --- a/tests/fixtures/syntax/malformed_parse.syntax-diagnostics.json +++ b/tests/fixtures/syntax/malformed_parse.syntax-diagnostics.json @@ -1,6 +1,6 @@ { "schema": "prism-syntax-diagnostics-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "9ee3f1af77ae784c00842c163f840a7d6d7b0d676951e73b2b7583f949daa0a9", "text": "-- Lexes fine but cannot parse: a declaration missing its parameter list.\nfn broken( = 1\n" diff --git a/tests/fixtures/syntax/malformed_parse_eof.syntax-diagnostics.json b/tests/fixtures/syntax/malformed_parse_eof.syntax-diagnostics.json index 8ffa3bc4..7a3ccb26 100644 --- a/tests/fixtures/syntax/malformed_parse_eof.syntax-diagnostics.json +++ b/tests/fixtures/syntax/malformed_parse_eof.syntax-diagnostics.json @@ -1,6 +1,6 @@ { "schema": "prism-syntax-diagnostics-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "1ce0f0b6af63047ffd618954b6094feff7739ca8d4338cbe5f42390299a024e1", "text": "-- Lexes fine but ends early: a binary operator still waiting for its right\n-- operand when the source stops. The layout pass closes the open block before\n-- the stream ends, so the fault is the general parse code at a zero-width\n-- caret on the virtual closer, not the exhausted-stream code, which only the\n-- expression entry can reach.\nfn broken() : Int = 1 +\n" diff --git a/tests/fixtures/syntax/malformed_parse_flip.syntax-diagnostics.json b/tests/fixtures/syntax/malformed_parse_flip.syntax-diagnostics.json index 6ce567d4..7b04a877 100644 --- a/tests/fixtures/syntax/malformed_parse_flip.syntax-diagnostics.json +++ b/tests/fixtures/syntax/malformed_parse_flip.syntax-diagnostics.json @@ -1,6 +1,6 @@ { "schema": "prism-syntax-diagnostics-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "d7e9722b9825121eb97e7f53668b2f17437da2b8b501932982ba9d6d7ecb2007", "text": "-- The retired brace-delimited class body, recognized in full so the deliberate\n-- diagnostic names the layout rewrite instead of degrading to an unexpected\n-- token, and carries no generic expectation set.\nclass Show(a) { show : (a) -> String }\n" diff --git a/tests/fixtures/syntax/malformed_unterm_hole.syntax-diagnostics.json b/tests/fixtures/syntax/malformed_unterm_hole.syntax-diagnostics.json index 7227e0ad..8ee36ac3 100644 --- a/tests/fixtures/syntax/malformed_unterm_hole.syntax-diagnostics.json +++ b/tests/fixtures/syntax/malformed_unterm_hole.syntax-diagnostics.json @@ -1,6 +1,6 @@ { "schema": "prism-syntax-diagnostics-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "73fdc75464613467c12aeef960688e2c39ebd141027ed22b6be1a7763ca1b9a9", "text": "-- An interpolation hole that runs off the end of input: the lexer refuses this\n-- file at the opening brace, the compiler's `LexError::UnterminatedHole`\n-- (E7002). The string never closes because the hole stays open.\nfn main() = println(\"value = {x\n" diff --git a/tests/fixtures/syntax/patterns.resolved-syntax.json b/tests/fixtures/syntax/patterns.resolved-syntax.json index fbe8179e..016d359f 100644 --- a/tests/fixtures/syntax/patterns.resolved-syntax.json +++ b/tests/fixtures/syntax/patterns.resolved-syntax.json @@ -1,6 +1,6 @@ { "schema": "prism-resolved-syntax-v1", - "compiler": "0.18.0", + "compiler": "0.20.0", "source": { "digest": "4af039e91ba99d57a978bb8d61f6cb3a8193a72f0c1e18d29d2a910e9c10dff8", "text": "-- Pattern forms: constructors, tuples, records, literals, guards, views,\n-- alternations, and parameters written as patterns.\ntype Point = Point { x: Int, y: Int }\n\ntype Shape = Dot | Line(Point, Point) | Poly(List(Point))\n\npattern Diag(n) for Point =\n view \\(p) -> if p.x == p.y then Some(p.x) else None\n make \\(n) -> Point { x = n, y = n }\n\nfn classify(s : Shape) : Int =\n match s of\n Dot => 0\n Line(Point { x = 0, y = 0 }, _) => 1\n Line(a, b) if a.x == b.x => 2\n Poly(Nil) => 3\n Poly(Cons(p, Nil)) => p.x\n _ => 9\n\nfn literals(v : Int, c : Char, f : Float, b : Bool) : Int =\n match (v, c, f, b) of\n (0, 'a', 1.5, true) => 1\n (n, _, _, false) => n\n _ => 2\n\nfn record_rest(p : Point) : Int =\n match p of\n Point { x = 0, .. } => 0\n Point { x = x, y = y } => x + y\n\nfn views(p : Point) : Int =\n match p of\n Diag(n) if n > 3 => n * 2\n Diag(n) => n\n _ => 0\n\nfn alternation(s : Shape) : Int =\n match s of\n Dot | Poly(Nil) => 0\n Line(Point { x = 0, y = 0 } | Point { x = 1, y = 1 }, _) => 1\n Line(p, _) | Poly(Cons(p, _)) => p.x\n\nfn alt_literals(c : Char, n : Int) : Int =\n match (c, n) of\n ('a' | 'b' | 'c', 0 | 1) => 1\n (_, _) => 2\n\nfn param_pats(Point { x = x, .. }, (a, b), borrow q : Point) : Int =\n let f = \\(Point { x = u, y = v }) -> u + v\n x + a + b + q.y + f(q)\n" diff --git a/tests/fixtures/syntax/patterns.surface-syntax.json b/tests/fixtures/syntax/patterns.surface-syntax.json index 1d5bdef8..0fbfb861 100644 --- a/tests/fixtures/syntax/patterns.surface-syntax.json +++ b/tests/fixtures/syntax/patterns.surface-syntax.json @@ -1,6 +1,6 @@ { "schema": "prism-surface-syntax-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "4af039e91ba99d57a978bb8d61f6cb3a8193a72f0c1e18d29d2a910e9c10dff8", "text": "-- Pattern forms: constructors, tuples, records, literals, guards, views,\n-- alternations, and parameters written as patterns.\ntype Point = Point { x: Int, y: Int }\n\ntype Shape = Dot | Line(Point, Point) | Poly(List(Point))\n\npattern Diag(n) for Point =\n view \\(p) -> if p.x == p.y then Some(p.x) else None\n make \\(n) -> Point { x = n, y = n }\n\nfn classify(s : Shape) : Int =\n match s of\n Dot => 0\n Line(Point { x = 0, y = 0 }, _) => 1\n Line(a, b) if a.x == b.x => 2\n Poly(Nil) => 3\n Poly(Cons(p, Nil)) => p.x\n _ => 9\n\nfn literals(v : Int, c : Char, f : Float, b : Bool) : Int =\n match (v, c, f, b) of\n (0, 'a', 1.5, true) => 1\n (n, _, _, false) => n\n _ => 2\n\nfn record_rest(p : Point) : Int =\n match p of\n Point { x = 0, .. } => 0\n Point { x = x, y = y } => x + y\n\nfn views(p : Point) : Int =\n match p of\n Diag(n) if n > 3 => n * 2\n Diag(n) => n\n _ => 0\n\nfn alternation(s : Shape) : Int =\n match s of\n Dot | Poly(Nil) => 0\n Line(Point { x = 0, y = 0 } | Point { x = 1, y = 1 }, _) => 1\n Line(p, _) | Poly(Cons(p, _)) => p.x\n\nfn alt_literals(c : Char, n : Int) : Int =\n match (c, n) of\n ('a' | 'b' | 'c', 0 | 1) => 1\n (_, _) => 2\n\nfn param_pats(Point { x = x, .. }, (a, b), borrow q : Point) : Int =\n let f = \\(Point { x = u, y = v }) -> u + v\n x + a + b + q.y + f(q)\n" diff --git a/tests/fixtures/syntax/patterns.syntax-diagnostics.json b/tests/fixtures/syntax/patterns.syntax-diagnostics.json index f5187301..86b763e0 100644 --- a/tests/fixtures/syntax/patterns.syntax-diagnostics.json +++ b/tests/fixtures/syntax/patterns.syntax-diagnostics.json @@ -1,6 +1,6 @@ { "schema": "prism-syntax-diagnostics-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "4af039e91ba99d57a978bb8d61f6cb3a8193a72f0c1e18d29d2a910e9c10dff8", "text": "-- Pattern forms: constructors, tuples, records, literals, guards, views,\n-- alternations, and parameters written as patterns.\ntype Point = Point { x: Int, y: Int }\n\ntype Shape = Dot | Line(Point, Point) | Poly(List(Point))\n\npattern Diag(n) for Point =\n view \\(p) -> if p.x == p.y then Some(p.x) else None\n make \\(n) -> Point { x = n, y = n }\n\nfn classify(s : Shape) : Int =\n match s of\n Dot => 0\n Line(Point { x = 0, y = 0 }, _) => 1\n Line(a, b) if a.x == b.x => 2\n Poly(Nil) => 3\n Poly(Cons(p, Nil)) => p.x\n _ => 9\n\nfn literals(v : Int, c : Char, f : Float, b : Bool) : Int =\n match (v, c, f, b) of\n (0, 'a', 1.5, true) => 1\n (n, _, _, false) => n\n _ => 2\n\nfn record_rest(p : Point) : Int =\n match p of\n Point { x = 0, .. } => 0\n Point { x = x, y = y } => x + y\n\nfn views(p : Point) : Int =\n match p of\n Diag(n) if n > 3 => n * 2\n Diag(n) => n\n _ => 0\n\nfn alternation(s : Shape) : Int =\n match s of\n Dot | Poly(Nil) => 0\n Line(Point { x = 0, y = 0 } | Point { x = 1, y = 1 }, _) => 1\n Line(p, _) | Poly(Cons(p, _)) => p.x\n\nfn alt_literals(c : Char, n : Int) : Int =\n match (c, n) of\n ('a' | 'b' | 'c', 0 | 1) => 1\n (_, _) => 2\n\nfn param_pats(Point { x = x, .. }, (a, b), borrow q : Point) : Int =\n let f = \\(Point { x = u, y = v }) -> u + v\n x + a + b + q.y + f(q)\n" diff --git a/tests/fixtures/syntax/patterns.syntax-tokens.json b/tests/fixtures/syntax/patterns.syntax-tokens.json index a8134e3e..890cbe64 100644 --- a/tests/fixtures/syntax/patterns.syntax-tokens.json +++ b/tests/fixtures/syntax/patterns.syntax-tokens.json @@ -1,6 +1,6 @@ { "schema": "prism-syntax-tokens-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "4af039e91ba99d57a978bb8d61f6cb3a8193a72f0c1e18d29d2a910e9c10dff8", "text": "-- Pattern forms: constructors, tuples, records, literals, guards, views,\n-- alternations, and parameters written as patterns.\ntype Point = Point { x: Int, y: Int }\n\ntype Shape = Dot | Line(Point, Point) | Poly(List(Point))\n\npattern Diag(n) for Point =\n view \\(p) -> if p.x == p.y then Some(p.x) else None\n make \\(n) -> Point { x = n, y = n }\n\nfn classify(s : Shape) : Int =\n match s of\n Dot => 0\n Line(Point { x = 0, y = 0 }, _) => 1\n Line(a, b) if a.x == b.x => 2\n Poly(Nil) => 3\n Poly(Cons(p, Nil)) => p.x\n _ => 9\n\nfn literals(v : Int, c : Char, f : Float, b : Bool) : Int =\n match (v, c, f, b) of\n (0, 'a', 1.5, true) => 1\n (n, _, _, false) => n\n _ => 2\n\nfn record_rest(p : Point) : Int =\n match p of\n Point { x = 0, .. } => 0\n Point { x = x, y = y } => x + y\n\nfn views(p : Point) : Int =\n match p of\n Diag(n) if n > 3 => n * 2\n Diag(n) => n\n _ => 0\n\nfn alternation(s : Shape) : Int =\n match s of\n Dot | Poly(Nil) => 0\n Line(Point { x = 0, y = 0 } | Point { x = 1, y = 1 }, _) => 1\n Line(p, _) | Poly(Cons(p, _)) => p.x\n\nfn alt_literals(c : Char, n : Int) : Int =\n match (c, n) of\n ('a' | 'b' | 'c', 0 | 1) => 1\n (_, _) => 2\n\nfn param_pats(Point { x = x, .. }, (a, b), borrow q : Point) : Int =\n let f = \\(Point { x = u, y = v }) -> u + v\n x + a + b + q.y + f(q)\n" diff --git a/tests/fixtures/syntax/released/0.15.0/decls.resolved-syntax.json b/tests/fixtures/syntax/released/0.15.0/decls.resolved-syntax.json new file mode 100644 index 00000000..8a43ace6 --- /dev/null +++ b/tests/fixtures/syntax/released/0.15.0/decls.resolved-syntax.json @@ -0,0 +1,64 @@ +{ + "schema": "prism-resolved-syntax-v1", + "compiler": "0.15.0", + "source": { + "digest": "f8ee1e543384e46f4ba1a050c3697b7a7280750acc419a266d278f32eac64a20", + "text": "-- Declaration families: imports, datatypes, aliases, constants, visibility.\nimport Data.List (map, filter)\n\nimport Json as J\n\nimport Data.Map (..)\n\npub type Color = Red | Green | Blue deriving (Eq, Show)\n\nopaque type Token = MkToken(Int)\n\nnewtype Meters = Meters(Float) deriving (Eq)\n\ntype Point = Point { x: Int, y: Int }\n\nalias Pair(a) = (a, a)\n\nerror NotFound(String)\n\nlet limit = 512\n\ndeprecated \"use limit\"\nlet old_limit = 256\n\npub fn origin() : Point = Point { x = 0, y = 0 }\n" + }, + "functions": [ + { + "name": "limit", + "params": [], + "body": { + "id": 1297, + "kind": "int", + "span": [ + 385, + 388 + ] + } + }, + { + "name": "old_limit", + "params": [], + "body": { + "id": 1298, + "kind": "int", + "span": [ + 429, + 432 + ] + } + }, + { + "name": "origin", + "params": [], + "body": { + "id": 1299, + "kind": "record", + "span": [ + 460, + 482 + ], + "children": [ + { + "id": 1300, + "kind": "int", + "span": [ + 472, + 473 + ] + }, + { + "id": 1301, + "kind": "int", + "span": [ + 479, + 480 + ] + } + ] + } + } + ] +} diff --git a/tests/fixtures/syntax/released/0.15.0/decls.surface-syntax.json b/tests/fixtures/syntax/released/0.15.0/decls.surface-syntax.json new file mode 100644 index 00000000..99299ac4 --- /dev/null +++ b/tests/fixtures/syntax/released/0.15.0/decls.surface-syntax.json @@ -0,0 +1,272 @@ +{ + "schema": "prism-surface-syntax-v1", + "compiler": "0.15.0", + "source": { + "digest": "f8ee1e543384e46f4ba1a050c3697b7a7280750acc419a266d278f32eac64a20", + "text": "-- Declaration families: imports, datatypes, aliases, constants, visibility.\nimport Data.List (map, filter)\n\nimport Json as J\n\nimport Data.Map (..)\n\npub type Color = Red | Green | Blue deriving (Eq, Show)\n\nopaque type Token = MkToken(Int)\n\nnewtype Meters = Meters(Float) deriving (Eq)\n\ntype Point = Point { x: Int, y: Int }\n\nalias Pair(a) = (a, a)\n\nerror NotFound(String)\n\nlet limit = 512\n\ndeprecated \"use limit\"\nlet old_limit = 256\n\npub fn origin() : Point = Point { x = 0, y = 0 }\n" + }, + "items": [ + { + "kind": "import", + "names": [ + "map", + "filter" + ], + "path": [ + "Data", + "List" + ], + "span": [ + 77, + 107 + ] + }, + { + "alias": "J", + "kind": "import", + "path": [ + "Json" + ], + "span": [ + 109, + 125 + ] + }, + { + "glob": true, + "kind": "import", + "path": [ + "Data", + "Map" + ], + "span": [ + 127, + 147 + ] + }, + { + "ctors": [ + { + "name": "Red" + }, + { + "name": "Green" + }, + { + "name": "Blue" + } + ], + "deriving": [ + { + "name": "Eq", + "span": [ + 195, + 197 + ] + }, + { + "name": "Show", + "span": [ + 199, + 203 + ] + } + ], + "kind": "data", + "name": "Color", + "span": [ + 153, + 204 + ], + "vis": "pub" + }, + { + "ctors": [ + { + "args": [ + { + "kind": "int" + } + ], + "name": "MkToken" + } + ], + "kind": "data", + "name": "Token", + "span": [ + 213, + 238 + ], + "vis": "opaque" + }, + { + "ctors": [ + { + "args": [ + { + "kind": "float" + } + ], + "name": "Meters" + } + ], + "deriving": [ + { + "name": "Eq", + "span": [ + 281, + 283 + ] + } + ], + "kind": "newtype", + "name": "Meters", + "span": [ + 240, + 284 + ] + }, + { + "ctors": [ + { + "fields": [ + { + "name": "x", + "ty": { + "kind": "int" + } + }, + { + "name": "y", + "ty": { + "kind": "int" + } + } + ], + "name": "Point" + } + ], + "kind": "data", + "name": "Point", + "span": [ + 286, + 323 + ] + }, + { + "kind": "type-synonym", + "name": "Pair", + "params": [ + "a" + ], + "span": [ + 325, + 347 + ], + "ty": { + "items": [ + { + "kind": "var", + "name": "a" + }, + { + "kind": "var", + "name": "a" + } + ], + "kind": "tuple" + } + }, + { + "kind": "error", + "name": "NotFound", + "params": [ + { + "kind": "str" + } + ], + "span": [ + 349, + 371 + ] + }, + { + "body": { + "kind": "int", + "span": [ + 385, + 388 + ], + "value": "512" + }, + "kind": "const", + "name": "limit", + "span": [ + 373, + 388 + ] + }, + { + "body": { + "kind": "int", + "span": [ + 429, + 432 + ], + "value": "256" + }, + "deprecated": "use limit", + "kind": "const", + "name": "old_limit", + "span": [ + 413, + 432 + ] + }, + { + "body": { + "fields": [ + { + "name": "x", + "value": { + "kind": "int", + "span": [ + 472, + 473 + ], + "value": "0" + } + }, + { + "name": "y", + "value": { + "kind": "int", + "span": [ + 479, + 480 + ], + "value": "0" + } + } + ], + "kind": "record", + "name": "Point", + "span": [ + 460, + 482 + ] + }, + "kind": "fn", + "name": "origin", + "ret": { + "kind": "con", + "name": "Point" + }, + "span": [ + 438, + 482 + ], + "vis": "pub" + } + ] +} diff --git a/tests/fixtures/syntax/released/0.15.0/decls.syntax-diagnostics.json b/tests/fixtures/syntax/released/0.15.0/decls.syntax-diagnostics.json new file mode 100644 index 00000000..141f984b --- /dev/null +++ b/tests/fixtures/syntax/released/0.15.0/decls.syntax-diagnostics.json @@ -0,0 +1,9 @@ +{ + "schema": "prism-syntax-diagnostics-v1", + "compiler": "0.15.0", + "source": { + "digest": "f8ee1e543384e46f4ba1a050c3697b7a7280750acc419a266d278f32eac64a20", + "text": "-- Declaration families: imports, datatypes, aliases, constants, visibility.\nimport Data.List (map, filter)\n\nimport Json as J\n\nimport Data.Map (..)\n\npub type Color = Red | Green | Blue deriving (Eq, Show)\n\nopaque type Token = MkToken(Int)\n\nnewtype Meters = Meters(Float) deriving (Eq)\n\ntype Point = Point { x: Int, y: Int }\n\nalias Pair(a) = (a, a)\n\nerror NotFound(String)\n\nlet limit = 512\n\ndeprecated \"use limit\"\nlet old_limit = 256\n\npub fn origin() : Point = Point { x = 0, y = 0 }\n" + }, + "diagnostics": [] +} diff --git a/tests/fixtures/syntax/released/0.15.0/decls.syntax-tokens.json b/tests/fixtures/syntax/released/0.15.0/decls.syntax-tokens.json new file mode 100644 index 00000000..05cef5e5 --- /dev/null +++ b/tests/fixtures/syntax/released/0.15.0/decls.syntax-tokens.json @@ -0,0 +1,1772 @@ +{ + "schema": "prism-syntax-tokens-v1", + "compiler": "0.15.0", + "source": { + "digest": "f8ee1e543384e46f4ba1a050c3697b7a7280750acc419a266d278f32eac64a20", + "text": "-- Declaration families: imports, datatypes, aliases, constants, visibility.\nimport Data.List (map, filter)\n\nimport Json as J\n\nimport Data.Map (..)\n\npub type Color = Red | Green | Blue deriving (Eq, Show)\n\nopaque type Token = MkToken(Int)\n\nnewtype Meters = Meters(Float) deriving (Eq)\n\ntype Point = Point { x: Int, y: Int }\n\nalias Pair(a) = (a, a)\n\nerror NotFound(String)\n\nlet limit = 512\n\ndeprecated \"use limit\"\nlet old_limit = 256\n\npub fn origin() : Point = Point { x = 0, y = 0 }\n" + }, + "raw": [ + { + "kind": "import", + "span": [ + 77, + 83 + ] + }, + { + "kind": "qual", + "span": [ + 84, + 93 + ], + "value": "Data.List" + }, + { + "kind": "(", + "span": [ + 94, + 95 + ] + }, + { + "kind": "ident", + "span": [ + 95, + 98 + ], + "value": "map" + }, + { + "kind": ",", + "span": [ + 98, + 99 + ] + }, + { + "kind": "ident", + "span": [ + 100, + 106 + ], + "value": "filter" + }, + { + "kind": ")", + "span": [ + 106, + 107 + ] + }, + { + "kind": "import", + "span": [ + 109, + 115 + ] + }, + { + "kind": "uid", + "span": [ + 116, + 120 + ], + "value": "Json" + }, + { + "kind": "as", + "span": [ + 121, + 123 + ] + }, + { + "kind": "uid", + "span": [ + 124, + 125 + ], + "value": "J" + }, + { + "kind": "import", + "span": [ + 127, + 133 + ] + }, + { + "kind": "qual", + "span": [ + 134, + 142 + ], + "value": "Data.Map" + }, + { + "kind": "(", + "span": [ + 143, + 144 + ] + }, + { + "kind": "..", + "span": [ + 144, + 146 + ] + }, + { + "kind": ")", + "span": [ + 146, + 147 + ] + }, + { + "kind": "pub", + "span": [ + 149, + 152 + ] + }, + { + "kind": "type", + "span": [ + 153, + 157 + ] + }, + { + "kind": "uid", + "span": [ + 158, + 163 + ], + "value": "Color" + }, + { + "kind": "=", + "span": [ + 164, + 165 + ] + }, + { + "kind": "uid", + "span": [ + 166, + 169 + ], + "value": "Red" + }, + { + "kind": "|", + "span": [ + 170, + 171 + ] + }, + { + "kind": "uid", + "span": [ + 172, + 177 + ], + "value": "Green" + }, + { + "kind": "|", + "span": [ + 178, + 179 + ] + }, + { + "kind": "uid", + "span": [ + 180, + 184 + ], + "value": "Blue" + }, + { + "kind": "deriving", + "span": [ + 185, + 193 + ] + }, + { + "kind": "(", + "span": [ + 194, + 195 + ] + }, + { + "kind": "uid", + "span": [ + 195, + 197 + ], + "value": "Eq" + }, + { + "kind": ",", + "span": [ + 197, + 198 + ] + }, + { + "kind": "uid", + "span": [ + 199, + 203 + ], + "value": "Show" + }, + { + "kind": ")", + "span": [ + 203, + 204 + ] + }, + { + "kind": "opaque", + "span": [ + 206, + 212 + ] + }, + { + "kind": "type", + "span": [ + 213, + 217 + ] + }, + { + "kind": "uid", + "span": [ + 218, + 223 + ], + "value": "Token" + }, + { + "kind": "=", + "span": [ + 224, + 225 + ] + }, + { + "kind": "uid", + "span": [ + 226, + 233 + ], + "value": "MkToken" + }, + { + "kind": "(", + "span": [ + 233, + 234 + ] + }, + { + "kind": "Int", + "span": [ + 234, + 237 + ] + }, + { + "kind": ")", + "span": [ + 237, + 238 + ] + }, + { + "kind": "newtype", + "span": [ + 240, + 247 + ] + }, + { + "kind": "uid", + "span": [ + 248, + 254 + ], + "value": "Meters" + }, + { + "kind": "=", + "span": [ + 255, + 256 + ] + }, + { + "kind": "uid", + "span": [ + 257, + 263 + ], + "value": "Meters" + }, + { + "kind": "(", + "span": [ + 263, + 264 + ] + }, + { + "kind": "Float", + "span": [ + 264, + 269 + ] + }, + { + "kind": ")", + "span": [ + 269, + 270 + ] + }, + { + "kind": "deriving", + "span": [ + 271, + 279 + ] + }, + { + "kind": "(", + "span": [ + 280, + 281 + ] + }, + { + "kind": "uid", + "span": [ + 281, + 283 + ], + "value": "Eq" + }, + { + "kind": ")", + "span": [ + 283, + 284 + ] + }, + { + "kind": "type", + "span": [ + 286, + 290 + ] + }, + { + "kind": "uid", + "span": [ + 291, + 296 + ], + "value": "Point" + }, + { + "kind": "=", + "span": [ + 297, + 298 + ] + }, + { + "kind": "uid", + "span": [ + 299, + 304 + ], + "value": "Point" + }, + { + "kind": "{", + "span": [ + 305, + 306 + ] + }, + { + "kind": "ident", + "span": [ + 307, + 308 + ], + "value": "x" + }, + { + "kind": ":", + "span": [ + 308, + 309 + ] + }, + { + "kind": "Int", + "span": [ + 310, + 313 + ] + }, + { + "kind": ",", + "span": [ + 313, + 314 + ] + }, + { + "kind": "ident", + "span": [ + 315, + 316 + ], + "value": "y" + }, + { + "kind": ":", + "span": [ + 316, + 317 + ] + }, + { + "kind": "Int", + "span": [ + 318, + 321 + ] + }, + { + "kind": "}", + "span": [ + 322, + 323 + ] + }, + { + "kind": "alias", + "span": [ + 325, + 330 + ] + }, + { + "kind": "uid", + "span": [ + 331, + 335 + ], + "value": "Pair" + }, + { + "kind": "(", + "span": [ + 335, + 336 + ] + }, + { + "kind": "ident", + "span": [ + 336, + 337 + ], + "value": "a" + }, + { + "kind": ")", + "span": [ + 337, + 338 + ] + }, + { + "kind": "=", + "span": [ + 339, + 340 + ] + }, + { + "kind": "(", + "span": [ + 341, + 342 + ] + }, + { + "kind": "ident", + "span": [ + 342, + 343 + ], + "value": "a" + }, + { + "kind": ",", + "span": [ + 343, + 344 + ] + }, + { + "kind": "ident", + "span": [ + 345, + 346 + ], + "value": "a" + }, + { + "kind": ")", + "span": [ + 346, + 347 + ] + }, + { + "kind": "error", + "span": [ + 349, + 354 + ] + }, + { + "kind": "uid", + "span": [ + 355, + 363 + ], + "value": "NotFound" + }, + { + "kind": "(", + "span": [ + 363, + 364 + ] + }, + { + "kind": "String", + "span": [ + 364, + 370 + ] + }, + { + "kind": ")", + "span": [ + 370, + 371 + ] + }, + { + "kind": "let", + "span": [ + 373, + 376 + ] + }, + { + "kind": "ident", + "span": [ + 377, + 382 + ], + "value": "limit" + }, + { + "kind": "=", + "span": [ + 383, + 384 + ] + }, + { + "kind": "int", + "span": [ + 385, + 388 + ], + "value": "512" + }, + { + "kind": "ident", + "span": [ + 390, + 400 + ], + "value": "deprecated" + }, + { + "kind": "str", + "span": [ + 401, + 412 + ], + "value": "use limit" + }, + { + "kind": "let", + "span": [ + 413, + 416 + ] + }, + { + "kind": "ident", + "span": [ + 417, + 426 + ], + "value": "old_limit" + }, + { + "kind": "=", + "span": [ + 427, + 428 + ] + }, + { + "kind": "int", + "span": [ + 429, + 432 + ], + "value": "256" + }, + { + "kind": "pub", + "span": [ + 434, + 437 + ] + }, + { + "kind": "fn", + "span": [ + 438, + 440 + ] + }, + { + "kind": "ident", + "span": [ + 441, + 447 + ], + "value": "origin" + }, + { + "kind": "(", + "span": [ + 447, + 448 + ] + }, + { + "kind": ")", + "span": [ + 448, + 449 + ] + }, + { + "kind": ":", + "span": [ + 450, + 451 + ] + }, + { + "kind": "uid", + "span": [ + 452, + 457 + ], + "value": "Point" + }, + { + "kind": "=", + "span": [ + 458, + 459 + ] + }, + { + "kind": "uid", + "span": [ + 460, + 465 + ], + "value": "Point" + }, + { + "kind": "{", + "span": [ + 466, + 467 + ] + }, + { + "kind": "ident", + "span": [ + 468, + 469 + ], + "value": "x" + }, + { + "kind": "=", + "span": [ + 470, + 471 + ] + }, + { + "kind": "int", + "span": [ + 472, + 473 + ], + "value": "0" + }, + { + "kind": ",", + "span": [ + 473, + 474 + ] + }, + { + "kind": "ident", + "span": [ + 475, + 476 + ], + "value": "y" + }, + { + "kind": "=", + "span": [ + 477, + 478 + ] + }, + { + "kind": "int", + "span": [ + 479, + 480 + ], + "value": "0" + }, + { + "kind": "}", + "span": [ + 481, + 482 + ] + } + ], + "parse": [ + { + "kind": "v{", + "span": [ + 77, + 77 + ] + }, + { + "kind": "import", + "span": [ + 77, + 83 + ] + }, + { + "kind": "qual", + "span": [ + 84, + 93 + ], + "value": "Data.List" + }, + { + "kind": "(", + "span": [ + 94, + 95 + ] + }, + { + "kind": "ident", + "span": [ + 95, + 98 + ], + "value": "map" + }, + { + "kind": ",", + "span": [ + 98, + 99 + ] + }, + { + "kind": "ident", + "span": [ + 100, + 106 + ], + "value": "filter" + }, + { + "kind": ")", + "span": [ + 106, + 107 + ] + }, + { + "kind": "v;", + "span": [ + 107, + 107 + ] + }, + { + "kind": "import", + "span": [ + 109, + 115 + ] + }, + { + "kind": "uid", + "span": [ + 116, + 120 + ], + "value": "Json" + }, + { + "kind": "as", + "span": [ + 121, + 123 + ] + }, + { + "kind": "uid", + "span": [ + 124, + 125 + ], + "value": "J" + }, + { + "kind": "v;", + "span": [ + 125, + 125 + ] + }, + { + "kind": "import", + "span": [ + 127, + 133 + ] + }, + { + "kind": "qual", + "span": [ + 134, + 142 + ], + "value": "Data.Map" + }, + { + "kind": "(", + "span": [ + 143, + 144 + ] + }, + { + "kind": "..", + "span": [ + 144, + 146 + ] + }, + { + "kind": ")", + "span": [ + 146, + 147 + ] + }, + { + "kind": "v;", + "span": [ + 147, + 147 + ] + }, + { + "kind": "pub", + "span": [ + 149, + 152 + ] + }, + { + "kind": "type", + "span": [ + 153, + 157 + ] + }, + { + "kind": "uid", + "span": [ + 158, + 163 + ], + "value": "Color" + }, + { + "kind": "=", + "span": [ + 164, + 165 + ] + }, + { + "kind": "uid", + "span": [ + 166, + 169 + ], + "value": "Red" + }, + { + "kind": "|", + "span": [ + 170, + 171 + ] + }, + { + "kind": "uid", + "span": [ + 172, + 177 + ], + "value": "Green" + }, + { + "kind": "|", + "span": [ + 178, + 179 + ] + }, + { + "kind": "uid", + "span": [ + 180, + 184 + ], + "value": "Blue" + }, + { + "kind": "deriving", + "span": [ + 185, + 193 + ] + }, + { + "kind": "(", + "span": [ + 194, + 195 + ] + }, + { + "kind": "uid", + "span": [ + 195, + 197 + ], + "value": "Eq" + }, + { + "kind": ",", + "span": [ + 197, + 198 + ] + }, + { + "kind": "uid", + "span": [ + 199, + 203 + ], + "value": "Show" + }, + { + "kind": ")", + "span": [ + 203, + 204 + ] + }, + { + "kind": "v;", + "span": [ + 204, + 204 + ] + }, + { + "kind": "opaque", + "span": [ + 206, + 212 + ] + }, + { + "kind": "type", + "span": [ + 213, + 217 + ] + }, + { + "kind": "uid", + "span": [ + 218, + 223 + ], + "value": "Token" + }, + { + "kind": "=", + "span": [ + 224, + 225 + ] + }, + { + "kind": "uid", + "span": [ + 226, + 233 + ], + "value": "MkToken" + }, + { + "kind": "(", + "span": [ + 233, + 234 + ] + }, + { + "kind": "Int", + "span": [ + 234, + 237 + ] + }, + { + "kind": ")", + "span": [ + 237, + 238 + ] + }, + { + "kind": "v;", + "span": [ + 238, + 238 + ] + }, + { + "kind": "newtype", + "span": [ + 240, + 247 + ] + }, + { + "kind": "uid", + "span": [ + 248, + 254 + ], + "value": "Meters" + }, + { + "kind": "=", + "span": [ + 255, + 256 + ] + }, + { + "kind": "uid", + "span": [ + 257, + 263 + ], + "value": "Meters" + }, + { + "kind": "(", + "span": [ + 263, + 264 + ] + }, + { + "kind": "Float", + "span": [ + 264, + 269 + ] + }, + { + "kind": ")", + "span": [ + 269, + 270 + ] + }, + { + "kind": "deriving", + "span": [ + 271, + 279 + ] + }, + { + "kind": "(", + "span": [ + 280, + 281 + ] + }, + { + "kind": "uid", + "span": [ + 281, + 283 + ], + "value": "Eq" + }, + { + "kind": ")", + "span": [ + 283, + 284 + ] + }, + { + "kind": "v;", + "span": [ + 284, + 284 + ] + }, + { + "kind": "type", + "span": [ + 286, + 290 + ] + }, + { + "kind": "uid", + "span": [ + 291, + 296 + ], + "value": "Point" + }, + { + "kind": "=", + "span": [ + 297, + 298 + ] + }, + { + "kind": "uid", + "span": [ + 299, + 304 + ], + "value": "Point" + }, + { + "kind": "{", + "span": [ + 305, + 306 + ] + }, + { + "kind": "ident", + "span": [ + 307, + 308 + ], + "value": "x" + }, + { + "kind": ":", + "span": [ + 308, + 309 + ] + }, + { + "kind": "Int", + "span": [ + 310, + 313 + ] + }, + { + "kind": ",", + "span": [ + 313, + 314 + ] + }, + { + "kind": "ident", + "span": [ + 315, + 316 + ], + "value": "y" + }, + { + "kind": ":", + "span": [ + 316, + 317 + ] + }, + { + "kind": "Int", + "span": [ + 318, + 321 + ] + }, + { + "kind": "}", + "span": [ + 322, + 323 + ] + }, + { + "kind": "v;", + "span": [ + 323, + 323 + ] + }, + { + "kind": "alias", + "span": [ + 325, + 330 + ] + }, + { + "kind": "uid", + "span": [ + 331, + 335 + ], + "value": "Pair" + }, + { + "kind": "(", + "span": [ + 335, + 336 + ] + }, + { + "kind": "ident", + "span": [ + 336, + 337 + ], + "value": "a" + }, + { + "kind": ")", + "span": [ + 337, + 338 + ] + }, + { + "kind": "=", + "span": [ + 339, + 340 + ] + }, + { + "kind": "(", + "span": [ + 341, + 342 + ] + }, + { + "kind": "ident", + "span": [ + 342, + 343 + ], + "value": "a" + }, + { + "kind": ",", + "span": [ + 343, + 344 + ] + }, + { + "kind": "ident", + "span": [ + 345, + 346 + ], + "value": "a" + }, + { + "kind": ")", + "span": [ + 346, + 347 + ] + }, + { + "kind": "v;", + "span": [ + 347, + 347 + ] + }, + { + "kind": "error", + "span": [ + 349, + 354 + ] + }, + { + "kind": "uid", + "span": [ + 355, + 363 + ], + "value": "NotFound" + }, + { + "kind": "(", + "span": [ + 363, + 364 + ] + }, + { + "kind": "String", + "span": [ + 364, + 370 + ] + }, + { + "kind": ")", + "span": [ + 370, + 371 + ] + }, + { + "kind": "v;", + "span": [ + 371, + 371 + ] + }, + { + "kind": "let", + "span": [ + 373, + 376 + ] + }, + { + "kind": "ident", + "span": [ + 377, + 382 + ], + "value": "limit" + }, + { + "kind": "=", + "span": [ + 383, + 384 + ] + }, + { + "kind": "int", + "span": [ + 385, + 388 + ], + "value": "512" + }, + { + "kind": "v;", + "span": [ + 388, + 388 + ] + }, + { + "kind": "ident", + "span": [ + 390, + 400 + ], + "value": "deprecated" + }, + { + "kind": "str", + "span": [ + 401, + 412 + ], + "value": "use limit" + }, + { + "kind": "v;", + "span": [ + 412, + 412 + ] + }, + { + "kind": "let", + "span": [ + 413, + 416 + ] + }, + { + "kind": "ident", + "span": [ + 417, + 426 + ], + "value": "old_limit" + }, + { + "kind": "=", + "span": [ + 427, + 428 + ] + }, + { + "kind": "int", + "span": [ + 429, + 432 + ], + "value": "256" + }, + { + "kind": "v;", + "span": [ + 432, + 432 + ] + }, + { + "kind": "pub", + "span": [ + 434, + 437 + ] + }, + { + "kind": "fn", + "span": [ + 438, + 440 + ] + }, + { + "kind": "ident", + "span": [ + 441, + 447 + ], + "value": "origin" + }, + { + "kind": "(", + "span": [ + 447, + 448 + ] + }, + { + "kind": ")", + "span": [ + 448, + 449 + ] + }, + { + "kind": ":", + "span": [ + 450, + 451 + ] + }, + { + "kind": "uid", + "span": [ + 452, + 457 + ], + "value": "Point" + }, + { + "kind": "=", + "span": [ + 458, + 459 + ] + }, + { + "kind": "uid", + "span": [ + 460, + 465 + ], + "value": "Point" + }, + { + "kind": "{", + "span": [ + 466, + 467 + ] + }, + { + "kind": "ident", + "span": [ + 468, + 469 + ], + "value": "x" + }, + { + "kind": "=", + "span": [ + 470, + 471 + ] + }, + { + "kind": "int", + "span": [ + 472, + 473 + ], + "value": "0" + }, + { + "kind": ",", + "span": [ + 473, + 474 + ] + }, + { + "kind": "ident", + "span": [ + 475, + 476 + ], + "value": "y" + }, + { + "kind": "=", + "span": [ + 477, + 478 + ] + }, + { + "kind": "int", + "span": [ + 479, + 480 + ], + "value": "0" + }, + { + "kind": "}", + "span": [ + 481, + 482 + ] + }, + { + "kind": "v}", + "span": [ + 482, + 482 + ] + } + ], + "trivia": [ + { + "kind": "comment", + "span": [ + 0, + 76 + ] + }, + { + "kind": "blank", + "span": [ + 107, + 109 + ] + }, + { + "kind": "blank", + "span": [ + 125, + 127 + ] + }, + { + "kind": "blank", + "span": [ + 147, + 149 + ] + }, + { + "kind": "blank", + "span": [ + 204, + 206 + ] + }, + { + "kind": "blank", + "span": [ + 238, + 240 + ] + }, + { + "kind": "blank", + "span": [ + 284, + 286 + ] + }, + { + "kind": "blank", + "span": [ + 323, + 325 + ] + }, + { + "kind": "blank", + "span": [ + 347, + 349 + ] + }, + { + "kind": "blank", + "span": [ + 371, + 373 + ] + }, + { + "kind": "blank", + "span": [ + 388, + 390 + ] + }, + { + "kind": "blank", + "span": [ + 432, + 434 + ] + } + ] +} diff --git a/tests/fixtures/syntax/released/0.15.0/interp.resolved-syntax.json b/tests/fixtures/syntax/released/0.15.0/interp.resolved-syntax.json new file mode 100644 index 00000000..15ff4b2a --- /dev/null +++ b/tests/fixtures/syntax/released/0.15.0/interp.resolved-syntax.json @@ -0,0 +1,295 @@ +{ + "schema": "prism-resolved-syntax-v1", + "compiler": "0.15.0", + "source": { + "digest": "d916367af5ed0ae848f8847fade024e1809a5b1aa1650bec6ef1ef6ffb3f8f73", + "text": "-- Interpolation and layout edges: nested holes, bare-indent bodies, blanks.\neffect Env\n read(String) : String\n\nclass Render(a)\n render : (a) -> String\n\ninstance renderInt : Render(Int)\n fn render(v) = \"v={v}\"\n\nfn nested(name : String) : String =\n \"outer {concat(\"inner {name} deep\", read(\"home\"))} tail\"\n\nfn multiline() : Int =\n let a = let b = 1 in let c = 2 in b + c\n a\n" + }, + "functions": [ + { + "name": "nested", + "params": [ + { + "name": "name", + "borrow": false + } + ], + "body": { + "id": 1297, + "kind": "call", + "span": [ + 252, + 308 + ], + "children": [ + { + "id": 1298, + "kind": "var", + "span": [ + 252, + 252 + ] + }, + { + "id": 1299, + "kind": "str", + "span": [ + 252, + 252 + ] + }, + { + "id": 1300, + "kind": "call", + "span": [ + 252, + 252 + ], + "children": [ + { + "id": 1301, + "kind": "var", + "span": [ + 252, + 252 + ] + }, + { + "id": 1302, + "kind": "call", + "span": [ + 301, + 301 + ], + "children": [ + { + "id": 1303, + "kind": "var", + "span": [ + 301, + 301 + ] + }, + { + "id": 1304, + "kind": "call", + "span": [ + 260, + 301 + ], + "children": [ + { + "id": 1305, + "kind": "var", + "span": [ + 260, + 266 + ] + }, + { + "id": 1306, + "kind": "call", + "span": [ + 267, + 286 + ], + "children": [ + { + "id": 1307, + "kind": "var", + "span": [ + 267, + 267 + ] + }, + { + "id": 1308, + "kind": "str", + "span": [ + 267, + 267 + ] + }, + { + "id": 1309, + "kind": "call", + "span": [ + 267, + 267 + ], + "children": [ + { + "id": 1310, + "kind": "var", + "span": [ + 267, + 267 + ] + }, + { + "id": 1311, + "kind": "call", + "span": [ + 279, + 279 + ], + "children": [ + { + "id": 1312, + "kind": "var", + "span": [ + 279, + 279 + ] + }, + { + "id": 1313, + "kind": "var", + "span": [ + 275, + 279 + ] + } + ] + }, + { + "id": 1314, + "kind": "str", + "span": [ + 267, + 267 + ] + } + ] + } + ] + }, + { + "id": 1315, + "kind": "call", + "span": [ + 288, + 300 + ], + "children": [ + { + "id": 1316, + "kind": "var", + "span": [ + 288, + 292 + ] + }, + { + "id": 1317, + "kind": "str", + "span": [ + 293, + 299 + ] + } + ] + } + ] + } + ] + }, + { + "id": 1318, + "kind": "str", + "span": [ + 252, + 252 + ] + } + ] + } + ] + } + }, + { + "name": "multiline", + "params": [], + "body": { + "id": 1319, + "kind": "let", + "span": [ + 335, + 378 + ], + "children": [ + { + "id": 1320, + "kind": "let", + "span": [ + 343, + 374 + ], + "children": [ + { + "id": 1321, + "kind": "int", + "span": [ + 351, + 352 + ] + }, + { + "id": 1322, + "kind": "let", + "span": [ + 356, + 374 + ], + "children": [ + { + "id": 1323, + "kind": "int", + "span": [ + 364, + 365 + ] + }, + { + "id": 1324, + "kind": "bin", + "span": [ + 369, + 374 + ], + "children": [ + { + "id": 1325, + "kind": "var", + "span": [ + 369, + 370 + ] + }, + { + "id": 1326, + "kind": "var", + "span": [ + 373, + 374 + ] + } + ] + } + ] + } + ] + }, + { + "id": 1327, + "kind": "var", + "span": [ + 377, + 378 + ] + } + ] + } + } + ] +} diff --git a/tests/fixtures/syntax/released/0.15.0/interp.surface-syntax.json b/tests/fixtures/syntax/released/0.15.0/interp.surface-syntax.json new file mode 100644 index 00000000..84c97b2b --- /dev/null +++ b/tests/fixtures/syntax/released/0.15.0/interp.surface-syntax.json @@ -0,0 +1,346 @@ +{ + "schema": "prism-surface-syntax-v1", + "compiler": "0.15.0", + "source": { + "digest": "d916367af5ed0ae848f8847fade024e1809a5b1aa1650bec6ef1ef6ffb3f8f73", + "text": "-- Interpolation and layout edges: nested holes, bare-indent bodies, blanks.\neffect Env\n read(String) : String\n\nclass Render(a)\n render : (a) -> String\n\ninstance renderInt : Render(Int)\n fn render(v) = \"v={v}\"\n\nfn nested(name : String) : String =\n \"outer {concat(\"inner {name} deep\", read(\"home\"))} tail\"\n\nfn multiline() : Int =\n let a = let b = 1 in let c = 2 in b + c\n a\n" + }, + "items": [ + { + "kind": "effect", + "name": "Env", + "ops": [ + { + "name": "read", + "params": [ + { + "kind": "str" + } + ], + "ret": { + "kind": "str" + } + } + ], + "span": [ + 77, + 111 + ] + }, + { + "kind": "class", + "methods": [ + { + "name": "render", + "ty": { + "effects": { + "labels": [] + }, + "kind": "fun", + "params": [ + { + "kind": "var", + "name": "a" + } + ], + "ret": { + "kind": "str" + } + } + } + ], + "name": "Render", + "param": "a", + "span": [ + 113, + 153 + ] + }, + { + "class": "Render", + "head": { + "kind": "int" + }, + "kind": "instance", + "methods": [ + { + "body": { + "args": [ + { + "kind": "str", + "span": [ + 205, + 205 + ], + "value": "v=" + }, + { + "kind": "var", + "name": "v", + "span": [ + 209, + 210 + ] + }, + { + "kind": "str", + "span": [ + 205, + 205 + ], + "value": "" + } + ], + "head": { + "kind": "marker", + "marker": "interp", + "span": [ + 205, + 205 + ] + }, + "kind": "call", + "span": [ + 205, + 212 + ] + }, + "kind": "fn", + "name": "render", + "params": [ + { + "name": "v" + } + ], + "span": [ + 190, + 212 + ] + } + ], + "name": "renderInt", + "span": [ + 155, + 212 + ] + }, + { + "body": { + "args": [ + { + "kind": "str", + "span": [ + 252, + 252 + ], + "value": "outer " + }, + { + "args": [ + { + "args": [ + { + "kind": "str", + "span": [ + 267, + 267 + ], + "value": "inner " + }, + { + "kind": "var", + "name": "name", + "span": [ + 275, + 279 + ] + }, + { + "kind": "str", + "span": [ + 267, + 267 + ], + "value": " deep" + } + ], + "head": { + "kind": "marker", + "marker": "interp", + "span": [ + 267, + 267 + ] + }, + "kind": "call", + "span": [ + 267, + 286 + ] + }, + { + "args": [ + { + "kind": "str", + "span": [ + 293, + 299 + ], + "value": "home" + } + ], + "head": { + "kind": "var", + "name": "read", + "span": [ + 288, + 292 + ] + }, + "kind": "call", + "span": [ + 288, + 300 + ] + } + ], + "head": { + "kind": "var", + "name": "concat", + "span": [ + 260, + 266 + ] + }, + "kind": "call", + "span": [ + 260, + 301 + ] + }, + { + "kind": "str", + "span": [ + 252, + 252 + ], + "value": " tail" + } + ], + "head": { + "kind": "marker", + "marker": "interp", + "span": [ + 252, + 252 + ] + }, + "kind": "call", + "span": [ + 252, + 308 + ] + }, + "kind": "fn", + "name": "nested", + "params": [ + { + "name": "name", + "ty": { + "kind": "str" + } + } + ], + "ret": { + "kind": "str" + }, + "span": [ + 214, + 308 + ] + }, + { + "body": { + "body": { + "kind": "var", + "name": "a", + "span": [ + 377, + 378 + ] + }, + "kind": "let", + "name": "a", + "span": [ + 335, + 378 + ], + "value": { + "body": { + "body": { + "kind": "bin", + "lhs": { + "kind": "var", + "name": "b", + "span": [ + 369, + 370 + ] + }, + "op": "+", + "rhs": { + "kind": "var", + "name": "c", + "span": [ + 373, + 374 + ] + }, + "span": [ + 369, + 374 + ] + }, + "kind": "let", + "name": "c", + "span": [ + 356, + 374 + ], + "value": { + "kind": "int", + "span": [ + 364, + 365 + ], + "value": "2" + } + }, + "kind": "let", + "name": "b", + "span": [ + 343, + 374 + ], + "value": { + "kind": "int", + "span": [ + 351, + 352 + ], + "value": "1" + } + } + }, + "kind": "fn", + "name": "multiline", + "ret": { + "kind": "int" + }, + "span": [ + 310, + 378 + ] + } + ] +} diff --git a/tests/fixtures/syntax/released/0.15.0/interp.syntax-diagnostics.json b/tests/fixtures/syntax/released/0.15.0/interp.syntax-diagnostics.json new file mode 100644 index 00000000..b7514ef9 --- /dev/null +++ b/tests/fixtures/syntax/released/0.15.0/interp.syntax-diagnostics.json @@ -0,0 +1,9 @@ +{ + "schema": "prism-syntax-diagnostics-v1", + "compiler": "0.15.0", + "source": { + "digest": "d916367af5ed0ae848f8847fade024e1809a5b1aa1650bec6ef1ef6ffb3f8f73", + "text": "-- Interpolation and layout edges: nested holes, bare-indent bodies, blanks.\neffect Env\n read(String) : String\n\nclass Render(a)\n render : (a) -> String\n\ninstance renderInt : Render(Int)\n fn render(v) = \"v={v}\"\n\nfn nested(name : String) : String =\n \"outer {concat(\"inner {name} deep\", read(\"home\"))} tail\"\n\nfn multiline() : Int =\n let a = let b = 1 in let c = 2 in b + c\n a\n" + }, + "diagnostics": [] +} diff --git a/tests/fixtures/syntax/released/0.15.0/interp.syntax-tokens.json b/tests/fixtures/syntax/released/0.15.0/interp.syntax-tokens.json new file mode 100644 index 00000000..ab79382c --- /dev/null +++ b/tests/fixtures/syntax/released/0.15.0/interp.syntax-tokens.json @@ -0,0 +1,1394 @@ +{ + "schema": "prism-syntax-tokens-v1", + "compiler": "0.15.0", + "source": { + "digest": "d916367af5ed0ae848f8847fade024e1809a5b1aa1650bec6ef1ef6ffb3f8f73", + "text": "-- Interpolation and layout edges: nested holes, bare-indent bodies, blanks.\neffect Env\n read(String) : String\n\nclass Render(a)\n render : (a) -> String\n\ninstance renderInt : Render(Int)\n fn render(v) = \"v={v}\"\n\nfn nested(name : String) : String =\n \"outer {concat(\"inner {name} deep\", read(\"home\"))} tail\"\n\nfn multiline() : Int =\n let a = let b = 1 in let c = 2 in b + c\n a\n" + }, + "raw": [ + { + "kind": "effect", + "span": [ + 77, + 83 + ] + }, + { + "kind": "uid", + "span": [ + 84, + 87 + ], + "value": "Env" + }, + { + "kind": "ident", + "span": [ + 90, + 94 + ], + "value": "read" + }, + { + "kind": "(", + "span": [ + 94, + 95 + ] + }, + { + "kind": "String", + "span": [ + 95, + 101 + ] + }, + { + "kind": ")", + "span": [ + 101, + 102 + ] + }, + { + "kind": ":", + "span": [ + 103, + 104 + ] + }, + { + "kind": "String", + "span": [ + 105, + 111 + ] + }, + { + "kind": "class", + "span": [ + 113, + 118 + ] + }, + { + "kind": "uid", + "span": [ + 119, + 125 + ], + "value": "Render" + }, + { + "kind": "(", + "span": [ + 125, + 126 + ] + }, + { + "kind": "ident", + "span": [ + 126, + 127 + ], + "value": "a" + }, + { + "kind": ")", + "span": [ + 127, + 128 + ] + }, + { + "kind": "ident", + "span": [ + 131, + 137 + ], + "value": "render" + }, + { + "kind": ":", + "span": [ + 138, + 139 + ] + }, + { + "kind": "(", + "span": [ + 140, + 141 + ] + }, + { + "kind": "ident", + "span": [ + 141, + 142 + ], + "value": "a" + }, + { + "kind": ")", + "span": [ + 142, + 143 + ] + }, + { + "kind": "->", + "span": [ + 144, + 146 + ] + }, + { + "kind": "String", + "span": [ + 147, + 153 + ] + }, + { + "kind": "instance", + "span": [ + 155, + 163 + ] + }, + { + "kind": "ident", + "span": [ + 164, + 173 + ], + "value": "renderInt" + }, + { + "kind": ":", + "span": [ + 174, + 175 + ] + }, + { + "kind": "uid", + "span": [ + 176, + 182 + ], + "value": "Render" + }, + { + "kind": "(", + "span": [ + 182, + 183 + ] + }, + { + "kind": "Int", + "span": [ + 183, + 186 + ] + }, + { + "kind": ")", + "span": [ + 186, + 187 + ] + }, + { + "kind": "fn", + "span": [ + 190, + 192 + ] + }, + { + "kind": "ident", + "span": [ + 193, + 199 + ], + "value": "render" + }, + { + "kind": "(", + "span": [ + 199, + 200 + ] + }, + { + "kind": "ident", + "span": [ + 200, + 201 + ], + "value": "v" + }, + { + "kind": ")", + "span": [ + 201, + 202 + ] + }, + { + "kind": "=", + "span": [ + 203, + 204 + ] + }, + { + "kind": "istart", + "span": [ + 205, + 209 + ], + "value": "v=" + }, + { + "kind": "ident", + "span": [ + 209, + 210 + ], + "value": "v" + }, + { + "kind": "iend", + "span": [ + 210, + 212 + ], + "value": "" + }, + { + "kind": "fn", + "span": [ + 214, + 216 + ] + }, + { + "kind": "ident", + "span": [ + 217, + 223 + ], + "value": "nested" + }, + { + "kind": "(", + "span": [ + 223, + 224 + ] + }, + { + "kind": "ident", + "span": [ + 224, + 228 + ], + "value": "name" + }, + { + "kind": ":", + "span": [ + 229, + 230 + ] + }, + { + "kind": "String", + "span": [ + 231, + 237 + ] + }, + { + "kind": ")", + "span": [ + 237, + 238 + ] + }, + { + "kind": ":", + "span": [ + 239, + 240 + ] + }, + { + "kind": "String", + "span": [ + 241, + 247 + ] + }, + { + "kind": "=", + "span": [ + 248, + 249 + ] + }, + { + "kind": "istart", + "span": [ + 252, + 260 + ], + "value": "outer " + }, + { + "kind": "ident", + "span": [ + 260, + 266 + ], + "value": "concat" + }, + { + "kind": "(", + "span": [ + 266, + 267 + ] + }, + { + "kind": "istart", + "span": [ + 267, + 275 + ], + "value": "inner " + }, + { + "kind": "ident", + "span": [ + 275, + 279 + ], + "value": "name" + }, + { + "kind": "iend", + "span": [ + 279, + 286 + ], + "value": " deep" + }, + { + "kind": ",", + "span": [ + 286, + 287 + ] + }, + { + "kind": "ident", + "span": [ + 288, + 292 + ], + "value": "read" + }, + { + "kind": "(", + "span": [ + 292, + 293 + ] + }, + { + "kind": "str", + "span": [ + 293, + 299 + ], + "value": "home" + }, + { + "kind": ")", + "span": [ + 299, + 300 + ] + }, + { + "kind": ")", + "span": [ + 300, + 301 + ] + }, + { + "kind": "iend", + "span": [ + 301, + 308 + ], + "value": " tail" + }, + { + "kind": "fn", + "span": [ + 310, + 312 + ] + }, + { + "kind": "ident", + "span": [ + 313, + 322 + ], + "value": "multiline" + }, + { + "kind": "(", + "span": [ + 322, + 323 + ] + }, + { + "kind": ")", + "span": [ + 323, + 324 + ] + }, + { + "kind": ":", + "span": [ + 325, + 326 + ] + }, + { + "kind": "Int", + "span": [ + 327, + 330 + ] + }, + { + "kind": "=", + "span": [ + 331, + 332 + ] + }, + { + "kind": "let", + "span": [ + 335, + 338 + ] + }, + { + "kind": "ident", + "span": [ + 339, + 340 + ], + "value": "a" + }, + { + "kind": "=", + "span": [ + 341, + 342 + ] + }, + { + "kind": "let", + "span": [ + 343, + 346 + ] + }, + { + "kind": "ident", + "span": [ + 347, + 348 + ], + "value": "b" + }, + { + "kind": "=", + "span": [ + 349, + 350 + ] + }, + { + "kind": "int", + "span": [ + 351, + 352 + ], + "value": "1" + }, + { + "kind": "in", + "span": [ + 353, + 355 + ] + }, + { + "kind": "let", + "span": [ + 356, + 359 + ] + }, + { + "kind": "ident", + "span": [ + 360, + 361 + ], + "value": "c" + }, + { + "kind": "=", + "span": [ + 362, + 363 + ] + }, + { + "kind": "int", + "span": [ + 364, + 365 + ], + "value": "2" + }, + { + "kind": "in", + "span": [ + 366, + 368 + ] + }, + { + "kind": "ident", + "span": [ + 369, + 370 + ], + "value": "b" + }, + { + "kind": "+", + "span": [ + 371, + 372 + ] + }, + { + "kind": "ident", + "span": [ + 373, + 374 + ], + "value": "c" + }, + { + "kind": "ident", + "span": [ + 377, + 378 + ], + "value": "a" + } + ], + "parse": [ + { + "kind": "v{", + "span": [ + 77, + 77 + ] + }, + { + "kind": "effect", + "span": [ + 77, + 83 + ] + }, + { + "kind": "uid", + "span": [ + 84, + 87 + ], + "value": "Env" + }, + { + "kind": "v{", + "span": [ + 90, + 90 + ] + }, + { + "kind": "ident", + "span": [ + 90, + 94 + ], + "value": "read" + }, + { + "kind": "(", + "span": [ + 94, + 95 + ] + }, + { + "kind": "String", + "span": [ + 95, + 101 + ] + }, + { + "kind": ")", + "span": [ + 101, + 102 + ] + }, + { + "kind": ":", + "span": [ + 103, + 104 + ] + }, + { + "kind": "String", + "span": [ + 105, + 111 + ] + }, + { + "kind": "v}", + "span": [ + 111, + 111 + ] + }, + { + "kind": "v;", + "span": [ + 111, + 111 + ] + }, + { + "kind": "class", + "span": [ + 113, + 118 + ] + }, + { + "kind": "uid", + "span": [ + 119, + 125 + ], + "value": "Render" + }, + { + "kind": "(", + "span": [ + 125, + 126 + ] + }, + { + "kind": "ident", + "span": [ + 126, + 127 + ], + "value": "a" + }, + { + "kind": ")", + "span": [ + 127, + 128 + ] + }, + { + "kind": "v{", + "span": [ + 131, + 131 + ] + }, + { + "kind": "ident", + "span": [ + 131, + 137 + ], + "value": "render" + }, + { + "kind": ":", + "span": [ + 138, + 139 + ] + }, + { + "kind": "(", + "span": [ + 140, + 141 + ] + }, + { + "kind": "ident", + "span": [ + 141, + 142 + ], + "value": "a" + }, + { + "kind": ")", + "span": [ + 142, + 143 + ] + }, + { + "kind": "->", + "span": [ + 144, + 146 + ] + }, + { + "kind": "String", + "span": [ + 147, + 153 + ] + }, + { + "kind": "v}", + "span": [ + 153, + 153 + ] + }, + { + "kind": "v;", + "span": [ + 153, + 153 + ] + }, + { + "kind": "instance", + "span": [ + 155, + 163 + ] + }, + { + "kind": "ident", + "span": [ + 164, + 173 + ], + "value": "renderInt" + }, + { + "kind": ":", + "span": [ + 174, + 175 + ] + }, + { + "kind": "uid", + "span": [ + 176, + 182 + ], + "value": "Render" + }, + { + "kind": "(", + "span": [ + 182, + 183 + ] + }, + { + "kind": "Int", + "span": [ + 183, + 186 + ] + }, + { + "kind": ")", + "span": [ + 186, + 187 + ] + }, + { + "kind": "v{", + "span": [ + 190, + 190 + ] + }, + { + "kind": "fn", + "span": [ + 190, + 192 + ] + }, + { + "kind": "ident", + "span": [ + 193, + 199 + ], + "value": "render" + }, + { + "kind": "(", + "span": [ + 199, + 200 + ] + }, + { + "kind": "ident", + "span": [ + 200, + 201 + ], + "value": "v" + }, + { + "kind": ")", + "span": [ + 201, + 202 + ] + }, + { + "kind": "=", + "span": [ + 203, + 204 + ] + }, + { + "kind": "istart", + "span": [ + 205, + 209 + ], + "value": "v=" + }, + { + "kind": "ident", + "span": [ + 209, + 210 + ], + "value": "v" + }, + { + "kind": "iend", + "span": [ + 210, + 212 + ], + "value": "" + }, + { + "kind": "v}", + "span": [ + 212, + 212 + ] + }, + { + "kind": "v;", + "span": [ + 212, + 212 + ] + }, + { + "kind": "fn", + "span": [ + 214, + 216 + ] + }, + { + "kind": "ident", + "span": [ + 217, + 223 + ], + "value": "nested" + }, + { + "kind": "(", + "span": [ + 223, + 224 + ] + }, + { + "kind": "ident", + "span": [ + 224, + 228 + ], + "value": "name" + }, + { + "kind": ":", + "span": [ + 229, + 230 + ] + }, + { + "kind": "String", + "span": [ + 231, + 237 + ] + }, + { + "kind": ")", + "span": [ + 237, + 238 + ] + }, + { + "kind": ":", + "span": [ + 239, + 240 + ] + }, + { + "kind": "String", + "span": [ + 241, + 247 + ] + }, + { + "kind": "=", + "span": [ + 248, + 249 + ] + }, + { + "kind": "v{", + "span": [ + 252, + 252 + ] + }, + { + "kind": "istart", + "span": [ + 252, + 260 + ], + "value": "outer " + }, + { + "kind": "ident", + "span": [ + 260, + 266 + ], + "value": "concat" + }, + { + "kind": "(", + "span": [ + 266, + 267 + ] + }, + { + "kind": "istart", + "span": [ + 267, + 275 + ], + "value": "inner " + }, + { + "kind": "ident", + "span": [ + 275, + 279 + ], + "value": "name" + }, + { + "kind": "iend", + "span": [ + 279, + 286 + ], + "value": " deep" + }, + { + "kind": ",", + "span": [ + 286, + 287 + ] + }, + { + "kind": "ident", + "span": [ + 288, + 292 + ], + "value": "read" + }, + { + "kind": "(", + "span": [ + 292, + 293 + ] + }, + { + "kind": "str", + "span": [ + 293, + 299 + ], + "value": "home" + }, + { + "kind": ")", + "span": [ + 299, + 300 + ] + }, + { + "kind": ")", + "span": [ + 300, + 301 + ] + }, + { + "kind": "iend", + "span": [ + 301, + 308 + ], + "value": " tail" + }, + { + "kind": "v}", + "span": [ + 308, + 308 + ] + }, + { + "kind": "v;", + "span": [ + 308, + 308 + ] + }, + { + "kind": "fn", + "span": [ + 310, + 312 + ] + }, + { + "kind": "ident", + "span": [ + 313, + 322 + ], + "value": "multiline" + }, + { + "kind": "(", + "span": [ + 322, + 323 + ] + }, + { + "kind": ")", + "span": [ + 323, + 324 + ] + }, + { + "kind": ":", + "span": [ + 325, + 326 + ] + }, + { + "kind": "Int", + "span": [ + 327, + 330 + ] + }, + { + "kind": "=", + "span": [ + 331, + 332 + ] + }, + { + "kind": "v{", + "span": [ + 335, + 335 + ] + }, + { + "kind": "let", + "span": [ + 335, + 338 + ] + }, + { + "kind": "ident", + "span": [ + 339, + 340 + ], + "value": "a" + }, + { + "kind": "=", + "span": [ + 341, + 342 + ] + }, + { + "kind": "let", + "span": [ + 343, + 346 + ] + }, + { + "kind": "ident", + "span": [ + 347, + 348 + ], + "value": "b" + }, + { + "kind": "=", + "span": [ + 349, + 350 + ] + }, + { + "kind": "int", + "span": [ + 351, + 352 + ], + "value": "1" + }, + { + "kind": "in", + "span": [ + 353, + 355 + ] + }, + { + "kind": "let", + "span": [ + 356, + 359 + ] + }, + { + "kind": "ident", + "span": [ + 360, + 361 + ], + "value": "c" + }, + { + "kind": "=", + "span": [ + 362, + 363 + ] + }, + { + "kind": "int", + "span": [ + 364, + 365 + ], + "value": "2" + }, + { + "kind": "in", + "span": [ + 366, + 368 + ] + }, + { + "kind": "ident", + "span": [ + 369, + 370 + ], + "value": "b" + }, + { + "kind": "+", + "span": [ + 371, + 372 + ] + }, + { + "kind": "ident", + "span": [ + 373, + 374 + ], + "value": "c" + }, + { + "kind": "v;", + "span": [ + 374, + 374 + ] + }, + { + "kind": "ident", + "span": [ + 377, + 378 + ], + "value": "a" + }, + { + "kind": "v}", + "span": [ + 378, + 378 + ] + }, + { + "kind": "v}", + "span": [ + 378, + 378 + ] + } + ], + "trivia": [ + { + "kind": "comment", + "span": [ + 0, + 76 + ] + }, + { + "kind": "blank", + "span": [ + 111, + 113 + ] + }, + { + "kind": "blank", + "span": [ + 153, + 155 + ] + }, + { + "kind": "blank", + "span": [ + 212, + 214 + ] + }, + { + "kind": "blank", + "span": [ + 308, + 310 + ] + } + ] +} diff --git a/tests/fixtures/syntax/released/0.15.0/stable.resolved-syntax.json b/tests/fixtures/syntax/released/0.15.0/stable.resolved-syntax.json new file mode 100644 index 00000000..061e5428 --- /dev/null +++ b/tests/fixtures/syntax/released/0.15.0/stable.resolved-syntax.json @@ -0,0 +1,1209 @@ +{ + "schema": "prism-resolved-syntax-v1", + "compiler": "0.15.0", + "source": { + "digest": "8f7f59322ffaa712c60fff1c424bd7c02c640d52fa9fe7652f33833222054821", + "text": "-- A stable family: rungs, defaults, a migrations table with an override.\nimport Wire (..)\n\nstable Save {\n V1 = { hero: String, depth: Int },\n V2 = { ..V1, fog: Int = 30 },\n V3 = { ..V2, mist: Int = 5 },\n migrations {\n V1 -> V2 = auto\n V2 -> V3 = version(upgrade = \\(s) -> Save { hero = s.hero, depth = s.depth, fog = s.fog, mist = 7 }, downgrade = auto)\n V1 -> V3 = auto\n }\n}\n\nfn current() : Save = Save { hero = \"Ada\", depth = 12, fog = 30, mist = 99 }\n" + }, + "functions": [ + { + "name": "current", + "params": [], + "body": { + "id": 1297, + "kind": "record", + "span": [ + 414, + 468 + ], + "children": [ + { + "id": 1298, + "kind": "str", + "span": [ + 428, + 433 + ] + }, + { + "id": 1299, + "kind": "int", + "span": [ + 443, + 445 + ] + }, + { + "id": 1300, + "kind": "int", + "span": [ + 453, + 455 + ] + }, + { + "id": 1301, + "kind": "int", + "span": [ + 464, + 466 + ] + } + ] + } + }, + { + "name": "upgrade_Save_V1_V2", + "params": [ + { + "name": "v1", + "borrow": false + } + ], + "body": { + "id": 3801, + "kind": "record", + "span": [ + 145, + 173 + ], + "children": [ + { + "id": 3802, + "kind": "field", + "span": [ + 145, + 173 + ], + "children": [ + { + "id": 3803, + "kind": "var", + "span": [ + 145, + 173 + ] + } + ] + }, + { + "id": 3804, + "kind": "field", + "span": [ + 145, + 173 + ], + "children": [ + { + "id": 3805, + "kind": "var", + "span": [ + 145, + 173 + ] + } + ] + }, + { + "id": 3806, + "kind": "int", + "span": [ + 169, + 171 + ] + } + ] + } + }, + { + "name": "downgrade_Save_V2_V1", + "params": [ + { + "name": "v2", + "borrow": false + } + ], + "body": { + "id": 3807, + "kind": "tuple", + "span": [ + 145, + 173 + ], + "children": [ + { + "id": 3808, + "kind": "record", + "span": [ + 145, + 173 + ], + "children": [ + { + "id": 3809, + "kind": "field", + "span": [ + 145, + 173 + ], + "children": [ + { + "id": 3810, + "kind": "var", + "span": [ + 145, + 173 + ] + } + ] + }, + { + "id": 3811, + "kind": "field", + "span": [ + 145, + 173 + ], + "children": [ + { + "id": 3812, + "kind": "var", + "span": [ + 145, + 173 + ] + } + ] + } + ] + }, + { + "id": 3813, + "kind": "call", + "span": [ + 145, + 173 + ], + "children": [ + { + "id": 3814, + "kind": "var", + "span": [ + 145, + 173 + ] + }, + { + "id": 3815, + "kind": "list", + "span": [ + 145, + 173 + ], + "children": [ + { + "id": 3816, + "kind": "str", + "span": [ + 145, + 173 + ] + } + ] + } + ] + } + ] + } + }, + { + "name": "upgrade_Save_V2_V3", + "params": [ + { + "name": "s", + "borrow": false + } + ], + "body": { + "id": 3817, + "kind": "record", + "span": [ + 283, + 345 + ], + "children": [ + { + "id": 3818, + "kind": "field", + "span": [ + 297, + 303 + ], + "children": [ + { + "id": 3819, + "kind": "var", + "span": [ + 297, + 298 + ] + } + ] + }, + { + "id": 3820, + "kind": "field", + "span": [ + 313, + 320 + ], + "children": [ + { + "id": 3821, + "kind": "var", + "span": [ + 313, + 314 + ] + } + ] + }, + { + "id": 3822, + "kind": "field", + "span": [ + 328, + 333 + ], + "children": [ + { + "id": 3823, + "kind": "var", + "span": [ + 328, + 329 + ] + } + ] + }, + { + "id": 3824, + "kind": "int", + "span": [ + 342, + 343 + ] + } + ] + } + }, + { + "name": "downgrade_Save_V3_V2", + "params": [ + { + "name": "v3", + "borrow": false + } + ], + "body": { + "id": 3825, + "kind": "tuple", + "span": [ + 177, + 205 + ], + "children": [ + { + "id": 3826, + "kind": "record", + "span": [ + 177, + 205 + ], + "children": [ + { + "id": 3827, + "kind": "field", + "span": [ + 177, + 205 + ], + "children": [ + { + "id": 3828, + "kind": "var", + "span": [ + 177, + 205 + ] + } + ] + }, + { + "id": 3829, + "kind": "field", + "span": [ + 177, + 205 + ], + "children": [ + { + "id": 3830, + "kind": "var", + "span": [ + 177, + 205 + ] + } + ] + }, + { + "id": 3831, + "kind": "field", + "span": [ + 177, + 205 + ], + "children": [ + { + "id": 3832, + "kind": "var", + "span": [ + 177, + 205 + ] + } + ] + } + ] + }, + { + "id": 3833, + "kind": "call", + "span": [ + 177, + 205 + ], + "children": [ + { + "id": 3834, + "kind": "var", + "span": [ + 177, + 205 + ] + }, + { + "id": 3835, + "kind": "list", + "span": [ + 177, + 205 + ], + "children": [ + { + "id": 3836, + "kind": "str", + "span": [ + 177, + 205 + ] + } + ] + } + ] + } + ] + } + }, + { + "name": "upgrade_route_Save_V1", + "params": [ + { + "name": "v1", + "borrow": false + } + ], + "body": { + "id": 3837, + "kind": "call", + "span": [ + 108, + 141 + ], + "children": [ + { + "id": 3838, + "kind": "var", + "span": [ + 108, + 141 + ] + }, + { + "id": 3839, + "kind": "call", + "span": [ + 108, + 141 + ], + "children": [ + { + "id": 3840, + "kind": "var", + "span": [ + 108, + 141 + ] + }, + { + "id": 3841, + "kind": "var", + "span": [ + 108, + 141 + ] + } + ] + } + ] + } + }, + { + "name": "downgrade_route_Save_V1", + "params": [ + { + "name": "v3", + "borrow": false + } + ], + "body": { + "id": 3842, + "kind": "call", + "span": [ + 108, + 141 + ], + "children": [ + { + "id": 3843, + "kind": "call", + "span": [ + 108, + 141 + ], + "children": [ + { + "id": 3844, + "kind": "var", + "span": [ + 108, + 141 + ] + }, + { + "id": 3845, + "kind": "var", + "span": [ + 108, + 141 + ] + }, + { + "id": 3846, + "kind": "var", + "span": [ + 108, + 141 + ] + } + ] + }, + { + "id": 3847, + "kind": "var", + "span": [ + 108, + 141 + ] + } + ] + } + }, + { + "name": "upgrade_route_Save_V2", + "params": [ + { + "name": "v2", + "borrow": false + } + ], + "body": { + "id": 3848, + "kind": "call", + "span": [ + 145, + 173 + ], + "children": [ + { + "id": 3849, + "kind": "var", + "span": [ + 145, + 173 + ] + }, + { + "id": 3850, + "kind": "var", + "span": [ + 145, + 173 + ] + } + ] + } + }, + { + "name": "downgrade_route_Save_V2", + "params": [ + { + "name": "v3", + "borrow": false + } + ], + "body": { + "id": 3851, + "kind": "call", + "span": [ + 145, + 173 + ], + "children": [ + { + "id": 3852, + "kind": "var", + "span": [ + 145, + 173 + ] + }, + { + "id": 3853, + "kind": "var", + "span": [ + 145, + 173 + ] + } + ] + } + }, + { + "name": "wire_encode_Save", + "params": [ + { + "name": "_x", + "borrow": false + } + ], + "body": { + "id": 3854, + "kind": "call", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3855, + "kind": "var", + "span": [ + 92, + 390 + ] + }, + { + "id": 3856, + "kind": "str", + "span": [ + 92, + 390 + ] + }, + { + "id": 3857, + "kind": "var", + "span": [ + 92, + 390 + ] + } + ] + } + }, + { + "name": "wire_decode_Save", + "params": [ + { + "name": "_bs", + "borrow": false + } + ], + "body": { + "id": 3858, + "kind": "call", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3859, + "kind": "var", + "span": [ + 92, + 390 + ] + }, + { + "id": 3860, + "kind": "var", + "span": [ + 92, + 390 + ] + }, + { + "id": 3861, + "kind": "str", + "span": [ + 92, + 390 + ] + } + ] + } + }, + { + "name": "decode_ladder_Save", + "params": [ + { + "name": "_bs", + "borrow": false + } + ], + "body": { + "id": 3862, + "kind": "match", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3863, + "kind": "call", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3864, + "kind": "var", + "span": [ + 92, + 390 + ] + }, + { + "id": 3865, + "kind": "var", + "span": [ + 92, + 390 + ] + } + ] + }, + { + "id": 3866, + "kind": "if", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3867, + "kind": "bin", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3868, + "kind": "var", + "span": [ + 92, + 390 + ] + }, + { + "id": 3869, + "kind": "str", + "span": [ + 92, + 390 + ] + } + ] + }, + { + "id": 3870, + "kind": "match", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3871, + "kind": "call", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3872, + "kind": "var", + "span": [ + 92, + 390 + ] + }, + { + "id": 3873, + "kind": "var", + "span": [ + 92, + 390 + ] + } + ] + }, + { + "id": 3874, + "kind": "if", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3875, + "kind": "call", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3876, + "kind": "var", + "span": [ + 92, + 390 + ] + }, + { + "id": 3877, + "kind": "var", + "span": [ + 92, + 390 + ] + } + ] + }, + { + "id": 3878, + "kind": "call", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3879, + "kind": "var", + "span": [ + 92, + 390 + ] + }, + { + "id": 3880, + "kind": "call", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3881, + "kind": "var", + "span": [ + 92, + 390 + ] + }, + { + "id": 3882, + "kind": "var", + "span": [ + 92, + 390 + ] + } + ] + } + ] + }, + { + "id": 3883, + "kind": "call", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3884, + "kind": "var", + "span": [ + 92, + 390 + ] + } + ] + } + ] + } + ] + }, + { + "id": 3885, + "kind": "if", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3886, + "kind": "bin", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3887, + "kind": "var", + "span": [ + 92, + 390 + ] + }, + { + "id": 3888, + "kind": "str", + "span": [ + 92, + 390 + ] + } + ] + }, + { + "id": 3889, + "kind": "match", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3890, + "kind": "call", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3891, + "kind": "var", + "span": [ + 92, + 390 + ] + }, + { + "id": 3892, + "kind": "var", + "span": [ + 92, + 390 + ] + } + ] + }, + { + "id": 3893, + "kind": "if", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3894, + "kind": "call", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3895, + "kind": "var", + "span": [ + 92, + 390 + ] + }, + { + "id": 3896, + "kind": "var", + "span": [ + 92, + 390 + ] + } + ] + }, + { + "id": 3897, + "kind": "call", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3898, + "kind": "var", + "span": [ + 92, + 390 + ] + }, + { + "id": 3899, + "kind": "var", + "span": [ + 92, + 390 + ] + } + ] + }, + { + "id": 3900, + "kind": "call", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3901, + "kind": "var", + "span": [ + 92, + 390 + ] + } + ] + } + ] + } + ] + }, + { + "id": 3902, + "kind": "if", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3903, + "kind": "bin", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3904, + "kind": "var", + "span": [ + 92, + 390 + ] + }, + { + "id": 3905, + "kind": "str", + "span": [ + 92, + 390 + ] + } + ] + }, + { + "id": 3906, + "kind": "match", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3907, + "kind": "call", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3908, + "kind": "var", + "span": [ + 92, + 390 + ] + }, + { + "id": 3909, + "kind": "var", + "span": [ + 92, + 390 + ] + } + ] + }, + { + "id": 3910, + "kind": "if", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3911, + "kind": "call", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3912, + "kind": "var", + "span": [ + 92, + 390 + ] + }, + { + "id": 3913, + "kind": "var", + "span": [ + 92, + 390 + ] + } + ] + }, + { + "id": 3914, + "kind": "var", + "span": [ + 92, + 390 + ] + }, + { + "id": 3915, + "kind": "call", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3916, + "kind": "var", + "span": [ + 92, + 390 + ] + } + ] + } + ] + } + ] + }, + { + "id": 3917, + "kind": "call", + "span": [ + 92, + 390 + ], + "children": [ + { + "id": 3918, + "kind": "var", + "span": [ + 92, + 390 + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + } + ] +} diff --git a/tests/fixtures/syntax/released/0.15.0/stable.surface-syntax.json b/tests/fixtures/syntax/released/0.15.0/stable.surface-syntax.json new file mode 100644 index 00000000..0b1e7050 --- /dev/null +++ b/tests/fixtures/syntax/released/0.15.0/stable.surface-syntax.json @@ -0,0 +1,287 @@ +{ + "schema": "prism-surface-syntax-v1", + "compiler": "0.15.0", + "source": { + "digest": "8f7f59322ffaa712c60fff1c424bd7c02c640d52fa9fe7652f33833222054821", + "text": "-- A stable family: rungs, defaults, a migrations table with an override.\nimport Wire (..)\n\nstable Save {\n V1 = { hero: String, depth: Int },\n V2 = { ..V1, fog: Int = 30 },\n V3 = { ..V2, mist: Int = 5 },\n migrations {\n V1 -> V2 = auto\n V2 -> V3 = version(upgrade = \\(s) -> Save { hero = s.hero, depth = s.depth, fog = s.fog, mist = 7 }, downgrade = auto)\n V1 -> V3 = auto\n }\n}\n\nfn current() : Save = Save { hero = \"Ada\", depth = 12, fog = 30, mist = 99 }\n" + }, + "items": [ + { + "glob": true, + "kind": "import", + "path": [ + "Wire" + ], + "span": [ + 74, + 90 + ] + }, + { + "kind": "stable", + "migrations": [ + { + "from": "V1", + "route": "auto", + "span": [ + 226, + 241 + ], + "to": "V2" + }, + { + "from": "V2", + "route": { + "downgrade": "auto", + "upgrade": { + "body": { + "fields": [ + { + "name": "hero", + "value": { + "expr": { + "kind": "var", + "name": "s", + "span": [ + 297, + 298 + ] + }, + "kind": "field", + "name": "hero", + "span": [ + 297, + 303 + ] + } + }, + { + "name": "depth", + "value": { + "expr": { + "kind": "var", + "name": "s", + "span": [ + 313, + 314 + ] + }, + "kind": "field", + "name": "depth", + "span": [ + 313, + 320 + ] + } + }, + { + "name": "fog", + "value": { + "expr": { + "kind": "var", + "name": "s", + "span": [ + 328, + 329 + ] + }, + "kind": "field", + "name": "fog", + "span": [ + 328, + 333 + ] + } + }, + { + "name": "mist", + "value": { + "kind": "int", + "span": [ + 342, + 343 + ], + "value": "7" + } + } + ], + "kind": "record", + "name": "Save", + "span": [ + 283, + 345 + ] + }, + "kind": "lam", + "params": [ + { + "name": "s" + } + ], + "span": [ + 275, + 345 + ] + } + }, + "span": [ + 246, + 364 + ], + "to": "V3" + }, + { + "from": "V1", + "route": "auto", + "span": [ + 369, + 384 + ], + "to": "V3" + } + ], + "name": "Save", + "rungs": [ + { + "fields": [ + { + "name": "hero", + "ty": { + "kind": "str" + } + }, + { + "name": "depth", + "ty": { + "kind": "int" + } + } + ], + "name": "V1", + "span": [ + 108, + 141 + ] + }, + { + "base": "V1", + "fields": [ + { + "default": { + "kind": "int", + "span": [ + 169, + 171 + ], + "value": "30" + }, + "name": "fog", + "ty": { + "kind": "int" + } + } + ], + "name": "V2", + "span": [ + 145, + 173 + ] + }, + { + "base": "V2", + "fields": [ + { + "default": { + "kind": "int", + "span": [ + 202, + 203 + ], + "value": "5" + }, + "name": "mist", + "ty": { + "kind": "int" + } + } + ], + "name": "V3", + "span": [ + 177, + 205 + ] + } + ], + "span": [ + 92, + 390 + ] + }, + { + "body": { + "fields": [ + { + "name": "hero", + "value": { + "kind": "str", + "span": [ + 428, + 433 + ], + "value": "Ada" + } + }, + { + "name": "depth", + "value": { + "kind": "int", + "span": [ + 443, + 445 + ], + "value": "12" + } + }, + { + "name": "fog", + "value": { + "kind": "int", + "span": [ + 453, + 455 + ], + "value": "30" + } + }, + { + "name": "mist", + "value": { + "kind": "int", + "span": [ + 464, + 466 + ], + "value": "99" + } + } + ], + "kind": "record", + "name": "Save", + "span": [ + 414, + 468 + ] + }, + "kind": "fn", + "name": "current", + "ret": { + "kind": "con", + "name": "Save" + }, + "span": [ + 392, + 468 + ] + } + ] +} diff --git a/tests/fixtures/syntax/released/0.15.0/stable.syntax-diagnostics.json b/tests/fixtures/syntax/released/0.15.0/stable.syntax-diagnostics.json new file mode 100644 index 00000000..f2f4f912 --- /dev/null +++ b/tests/fixtures/syntax/released/0.15.0/stable.syntax-diagnostics.json @@ -0,0 +1,9 @@ +{ + "schema": "prism-syntax-diagnostics-v1", + "compiler": "0.15.0", + "source": { + "digest": "8f7f59322ffaa712c60fff1c424bd7c02c640d52fa9fe7652f33833222054821", + "text": "-- A stable family: rungs, defaults, a migrations table with an override.\nimport Wire (..)\n\nstable Save {\n V1 = { hero: String, depth: Int },\n V2 = { ..V1, fog: Int = 30 },\n V3 = { ..V2, mist: Int = 5 },\n migrations {\n V1 -> V2 = auto\n V2 -> V3 = version(upgrade = \\(s) -> Save { hero = s.hero, depth = s.depth, fog = s.fog, mist = 7 }, downgrade = auto)\n V1 -> V3 = auto\n }\n}\n\nfn current() : Save = Save { hero = \"Ada\", depth = 12, fog = 30, mist = 99 }\n" + }, + "diagnostics": [] +} diff --git a/tests/fixtures/syntax/released/0.15.0/stable.syntax-tokens.json b/tests/fixtures/syntax/released/0.15.0/stable.syntax-tokens.json new file mode 100644 index 00000000..5c5a8a33 --- /dev/null +++ b/tests/fixtures/syntax/released/0.15.0/stable.syntax-tokens.json @@ -0,0 +1,1941 @@ +{ + "schema": "prism-syntax-tokens-v1", + "compiler": "0.15.0", + "source": { + "digest": "8f7f59322ffaa712c60fff1c424bd7c02c640d52fa9fe7652f33833222054821", + "text": "-- A stable family: rungs, defaults, a migrations table with an override.\nimport Wire (..)\n\nstable Save {\n V1 = { hero: String, depth: Int },\n V2 = { ..V1, fog: Int = 30 },\n V3 = { ..V2, mist: Int = 5 },\n migrations {\n V1 -> V2 = auto\n V2 -> V3 = version(upgrade = \\(s) -> Save { hero = s.hero, depth = s.depth, fog = s.fog, mist = 7 }, downgrade = auto)\n V1 -> V3 = auto\n }\n}\n\nfn current() : Save = Save { hero = \"Ada\", depth = 12, fog = 30, mist = 99 }\n" + }, + "raw": [ + { + "kind": "import", + "span": [ + 74, + 80 + ] + }, + { + "kind": "uid", + "span": [ + 81, + 85 + ], + "value": "Wire" + }, + { + "kind": "(", + "span": [ + 86, + 87 + ] + }, + { + "kind": "..", + "span": [ + 87, + 89 + ] + }, + { + "kind": ")", + "span": [ + 89, + 90 + ] + }, + { + "kind": "stable", + "span": [ + 92, + 98 + ] + }, + { + "kind": "uid", + "span": [ + 99, + 103 + ], + "value": "Save" + }, + { + "kind": "{", + "span": [ + 104, + 105 + ] + }, + { + "kind": "uid", + "span": [ + 108, + 110 + ], + "value": "V1" + }, + { + "kind": "=", + "span": [ + 111, + 112 + ] + }, + { + "kind": "{", + "span": [ + 113, + 114 + ] + }, + { + "kind": "ident", + "span": [ + 115, + 119 + ], + "value": "hero" + }, + { + "kind": ":", + "span": [ + 119, + 120 + ] + }, + { + "kind": "String", + "span": [ + 121, + 127 + ] + }, + { + "kind": ",", + "span": [ + 127, + 128 + ] + }, + { + "kind": "ident", + "span": [ + 129, + 134 + ], + "value": "depth" + }, + { + "kind": ":", + "span": [ + 134, + 135 + ] + }, + { + "kind": "Int", + "span": [ + 136, + 139 + ] + }, + { + "kind": "}", + "span": [ + 140, + 141 + ] + }, + { + "kind": ",", + "span": [ + 141, + 142 + ] + }, + { + "kind": "uid", + "span": [ + 145, + 147 + ], + "value": "V2" + }, + { + "kind": "=", + "span": [ + 148, + 149 + ] + }, + { + "kind": "{", + "span": [ + 150, + 151 + ] + }, + { + "kind": "..", + "span": [ + 152, + 154 + ] + }, + { + "kind": "uid", + "span": [ + 154, + 156 + ], + "value": "V1" + }, + { + "kind": ",", + "span": [ + 156, + 157 + ] + }, + { + "kind": "ident", + "span": [ + 158, + 161 + ], + "value": "fog" + }, + { + "kind": ":", + "span": [ + 161, + 162 + ] + }, + { + "kind": "Int", + "span": [ + 163, + 166 + ] + }, + { + "kind": "=", + "span": [ + 167, + 168 + ] + }, + { + "kind": "int", + "span": [ + 169, + 171 + ], + "value": "30" + }, + { + "kind": "}", + "span": [ + 172, + 173 + ] + }, + { + "kind": ",", + "span": [ + 173, + 174 + ] + }, + { + "kind": "uid", + "span": [ + 177, + 179 + ], + "value": "V3" + }, + { + "kind": "=", + "span": [ + 180, + 181 + ] + }, + { + "kind": "{", + "span": [ + 182, + 183 + ] + }, + { + "kind": "..", + "span": [ + 184, + 186 + ] + }, + { + "kind": "uid", + "span": [ + 186, + 188 + ], + "value": "V2" + }, + { + "kind": ",", + "span": [ + 188, + 189 + ] + }, + { + "kind": "ident", + "span": [ + 190, + 194 + ], + "value": "mist" + }, + { + "kind": ":", + "span": [ + 194, + 195 + ] + }, + { + "kind": "Int", + "span": [ + 196, + 199 + ] + }, + { + "kind": "=", + "span": [ + 200, + 201 + ] + }, + { + "kind": "int", + "span": [ + 202, + 203 + ], + "value": "5" + }, + { + "kind": "}", + "span": [ + 204, + 205 + ] + }, + { + "kind": ",", + "span": [ + 205, + 206 + ] + }, + { + "kind": "ident", + "span": [ + 209, + 219 + ], + "value": "migrations" + }, + { + "kind": "{", + "span": [ + 220, + 221 + ] + }, + { + "kind": "uid", + "span": [ + 226, + 228 + ], + "value": "V1" + }, + { + "kind": "->", + "span": [ + 229, + 231 + ] + }, + { + "kind": "uid", + "span": [ + 232, + 234 + ], + "value": "V2" + }, + { + "kind": "=", + "span": [ + 235, + 236 + ] + }, + { + "kind": "ident", + "span": [ + 237, + 241 + ], + "value": "auto" + }, + { + "kind": "uid", + "span": [ + 246, + 248 + ], + "value": "V2" + }, + { + "kind": "->", + "span": [ + 249, + 251 + ] + }, + { + "kind": "uid", + "span": [ + 252, + 254 + ], + "value": "V3" + }, + { + "kind": "=", + "span": [ + 255, + 256 + ] + }, + { + "kind": "ident", + "span": [ + 257, + 264 + ], + "value": "version" + }, + { + "kind": "(", + "span": [ + 264, + 265 + ] + }, + { + "kind": "ident", + "span": [ + 265, + 272 + ], + "value": "upgrade" + }, + { + "kind": "=", + "span": [ + 273, + 274 + ] + }, + { + "kind": "\\", + "span": [ + 275, + 276 + ] + }, + { + "kind": "(", + "span": [ + 276, + 277 + ] + }, + { + "kind": "ident", + "span": [ + 277, + 278 + ], + "value": "s" + }, + { + "kind": ")", + "span": [ + 278, + 279 + ] + }, + { + "kind": "->", + "span": [ + 280, + 282 + ] + }, + { + "kind": "uid", + "span": [ + 283, + 287 + ], + "value": "Save" + }, + { + "kind": "{", + "span": [ + 288, + 289 + ] + }, + { + "kind": "ident", + "span": [ + 290, + 294 + ], + "value": "hero" + }, + { + "kind": "=", + "span": [ + 295, + 296 + ] + }, + { + "kind": "ident", + "span": [ + 297, + 298 + ], + "value": "s" + }, + { + "kind": ".", + "span": [ + 298, + 299 + ] + }, + { + "kind": "ident", + "span": [ + 299, + 303 + ], + "value": "hero" + }, + { + "kind": ",", + "span": [ + 303, + 304 + ] + }, + { + "kind": "ident", + "span": [ + 305, + 310 + ], + "value": "depth" + }, + { + "kind": "=", + "span": [ + 311, + 312 + ] + }, + { + "kind": "ident", + "span": [ + 313, + 314 + ], + "value": "s" + }, + { + "kind": ".", + "span": [ + 314, + 315 + ] + }, + { + "kind": "ident", + "span": [ + 315, + 320 + ], + "value": "depth" + }, + { + "kind": ",", + "span": [ + 320, + 321 + ] + }, + { + "kind": "ident", + "span": [ + 322, + 325 + ], + "value": "fog" + }, + { + "kind": "=", + "span": [ + 326, + 327 + ] + }, + { + "kind": "ident", + "span": [ + 328, + 329 + ], + "value": "s" + }, + { + "kind": ".", + "span": [ + 329, + 330 + ] + }, + { + "kind": "ident", + "span": [ + 330, + 333 + ], + "value": "fog" + }, + { + "kind": ",", + "span": [ + 333, + 334 + ] + }, + { + "kind": "ident", + "span": [ + 335, + 339 + ], + "value": "mist" + }, + { + "kind": "=", + "span": [ + 340, + 341 + ] + }, + { + "kind": "int", + "span": [ + 342, + 343 + ], + "value": "7" + }, + { + "kind": "}", + "span": [ + 344, + 345 + ] + }, + { + "kind": ",", + "span": [ + 345, + 346 + ] + }, + { + "kind": "ident", + "span": [ + 347, + 356 + ], + "value": "downgrade" + }, + { + "kind": "=", + "span": [ + 357, + 358 + ] + }, + { + "kind": "ident", + "span": [ + 359, + 363 + ], + "value": "auto" + }, + { + "kind": ")", + "span": [ + 363, + 364 + ] + }, + { + "kind": "uid", + "span": [ + 369, + 371 + ], + "value": "V1" + }, + { + "kind": "->", + "span": [ + 372, + 374 + ] + }, + { + "kind": "uid", + "span": [ + 375, + 377 + ], + "value": "V3" + }, + { + "kind": "=", + "span": [ + 378, + 379 + ] + }, + { + "kind": "ident", + "span": [ + 380, + 384 + ], + "value": "auto" + }, + { + "kind": "}", + "span": [ + 387, + 388 + ] + }, + { + "kind": "}", + "span": [ + 389, + 390 + ] + }, + { + "kind": "fn", + "span": [ + 392, + 394 + ] + }, + { + "kind": "ident", + "span": [ + 395, + 402 + ], + "value": "current" + }, + { + "kind": "(", + "span": [ + 402, + 403 + ] + }, + { + "kind": ")", + "span": [ + 403, + 404 + ] + }, + { + "kind": ":", + "span": [ + 405, + 406 + ] + }, + { + "kind": "uid", + "span": [ + 407, + 411 + ], + "value": "Save" + }, + { + "kind": "=", + "span": [ + 412, + 413 + ] + }, + { + "kind": "uid", + "span": [ + 414, + 418 + ], + "value": "Save" + }, + { + "kind": "{", + "span": [ + 419, + 420 + ] + }, + { + "kind": "ident", + "span": [ + 421, + 425 + ], + "value": "hero" + }, + { + "kind": "=", + "span": [ + 426, + 427 + ] + }, + { + "kind": "str", + "span": [ + 428, + 433 + ], + "value": "Ada" + }, + { + "kind": ",", + "span": [ + 433, + 434 + ] + }, + { + "kind": "ident", + "span": [ + 435, + 440 + ], + "value": "depth" + }, + { + "kind": "=", + "span": [ + 441, + 442 + ] + }, + { + "kind": "int", + "span": [ + 443, + 445 + ], + "value": "12" + }, + { + "kind": ",", + "span": [ + 445, + 446 + ] + }, + { + "kind": "ident", + "span": [ + 447, + 450 + ], + "value": "fog" + }, + { + "kind": "=", + "span": [ + 451, + 452 + ] + }, + { + "kind": "int", + "span": [ + 453, + 455 + ], + "value": "30" + }, + { + "kind": ",", + "span": [ + 455, + 456 + ] + }, + { + "kind": "ident", + "span": [ + 457, + 461 + ], + "value": "mist" + }, + { + "kind": "=", + "span": [ + 462, + 463 + ] + }, + { + "kind": "int", + "span": [ + 464, + 466 + ], + "value": "99" + }, + { + "kind": "}", + "span": [ + 467, + 468 + ] + } + ], + "parse": [ + { + "kind": "v{", + "span": [ + 74, + 74 + ] + }, + { + "kind": "import", + "span": [ + 74, + 80 + ] + }, + { + "kind": "uid", + "span": [ + 81, + 85 + ], + "value": "Wire" + }, + { + "kind": "(", + "span": [ + 86, + 87 + ] + }, + { + "kind": "..", + "span": [ + 87, + 89 + ] + }, + { + "kind": ")", + "span": [ + 89, + 90 + ] + }, + { + "kind": "v;", + "span": [ + 90, + 90 + ] + }, + { + "kind": "stable", + "span": [ + 92, + 98 + ] + }, + { + "kind": "uid", + "span": [ + 99, + 103 + ], + "value": "Save" + }, + { + "kind": "{", + "span": [ + 104, + 105 + ] + }, + { + "kind": "uid", + "span": [ + 108, + 110 + ], + "value": "V1" + }, + { + "kind": "=", + "span": [ + 111, + 112 + ] + }, + { + "kind": "{", + "span": [ + 113, + 114 + ] + }, + { + "kind": "ident", + "span": [ + 115, + 119 + ], + "value": "hero" + }, + { + "kind": ":", + "span": [ + 119, + 120 + ] + }, + { + "kind": "String", + "span": [ + 121, + 127 + ] + }, + { + "kind": ",", + "span": [ + 127, + 128 + ] + }, + { + "kind": "ident", + "span": [ + 129, + 134 + ], + "value": "depth" + }, + { + "kind": ":", + "span": [ + 134, + 135 + ] + }, + { + "kind": "Int", + "span": [ + 136, + 139 + ] + }, + { + "kind": "}", + "span": [ + 140, + 141 + ] + }, + { + "kind": ",", + "span": [ + 141, + 142 + ] + }, + { + "kind": "uid", + "span": [ + 145, + 147 + ], + "value": "V2" + }, + { + "kind": "=", + "span": [ + 148, + 149 + ] + }, + { + "kind": "{", + "span": [ + 150, + 151 + ] + }, + { + "kind": "..", + "span": [ + 152, + 154 + ] + }, + { + "kind": "uid", + "span": [ + 154, + 156 + ], + "value": "V1" + }, + { + "kind": ",", + "span": [ + 156, + 157 + ] + }, + { + "kind": "ident", + "span": [ + 158, + 161 + ], + "value": "fog" + }, + { + "kind": ":", + "span": [ + 161, + 162 + ] + }, + { + "kind": "Int", + "span": [ + 163, + 166 + ] + }, + { + "kind": "=", + "span": [ + 167, + 168 + ] + }, + { + "kind": "int", + "span": [ + 169, + 171 + ], + "value": "30" + }, + { + "kind": "}", + "span": [ + 172, + 173 + ] + }, + { + "kind": ",", + "span": [ + 173, + 174 + ] + }, + { + "kind": "uid", + "span": [ + 177, + 179 + ], + "value": "V3" + }, + { + "kind": "=", + "span": [ + 180, + 181 + ] + }, + { + "kind": "{", + "span": [ + 182, + 183 + ] + }, + { + "kind": "..", + "span": [ + 184, + 186 + ] + }, + { + "kind": "uid", + "span": [ + 186, + 188 + ], + "value": "V2" + }, + { + "kind": ",", + "span": [ + 188, + 189 + ] + }, + { + "kind": "ident", + "span": [ + 190, + 194 + ], + "value": "mist" + }, + { + "kind": ":", + "span": [ + 194, + 195 + ] + }, + { + "kind": "Int", + "span": [ + 196, + 199 + ] + }, + { + "kind": "=", + "span": [ + 200, + 201 + ] + }, + { + "kind": "int", + "span": [ + 202, + 203 + ], + "value": "5" + }, + { + "kind": "}", + "span": [ + 204, + 205 + ] + }, + { + "kind": ",", + "span": [ + 205, + 206 + ] + }, + { + "kind": "ident", + "span": [ + 209, + 219 + ], + "value": "migrations" + }, + { + "kind": "{", + "span": [ + 220, + 221 + ] + }, + { + "kind": "uid", + "span": [ + 226, + 228 + ], + "value": "V1" + }, + { + "kind": "->", + "span": [ + 229, + 231 + ] + }, + { + "kind": "uid", + "span": [ + 232, + 234 + ], + "value": "V2" + }, + { + "kind": "=", + "span": [ + 235, + 236 + ] + }, + { + "kind": "ident", + "span": [ + 237, + 241 + ], + "value": "auto" + }, + { + "kind": "uid", + "span": [ + 246, + 248 + ], + "value": "V2" + }, + { + "kind": "->", + "span": [ + 249, + 251 + ] + }, + { + "kind": "uid", + "span": [ + 252, + 254 + ], + "value": "V3" + }, + { + "kind": "=", + "span": [ + 255, + 256 + ] + }, + { + "kind": "ident", + "span": [ + 257, + 264 + ], + "value": "version" + }, + { + "kind": "(", + "span": [ + 264, + 265 + ] + }, + { + "kind": "ident", + "span": [ + 265, + 272 + ], + "value": "upgrade" + }, + { + "kind": "=", + "span": [ + 273, + 274 + ] + }, + { + "kind": "\\", + "span": [ + 275, + 276 + ] + }, + { + "kind": "(", + "span": [ + 276, + 277 + ] + }, + { + "kind": "ident", + "span": [ + 277, + 278 + ], + "value": "s" + }, + { + "kind": ")", + "span": [ + 278, + 279 + ] + }, + { + "kind": "->", + "span": [ + 280, + 282 + ] + }, + { + "kind": "uid", + "span": [ + 283, + 287 + ], + "value": "Save" + }, + { + "kind": "{", + "span": [ + 288, + 289 + ] + }, + { + "kind": "ident", + "span": [ + 290, + 294 + ], + "value": "hero" + }, + { + "kind": "=", + "span": [ + 295, + 296 + ] + }, + { + "kind": "ident", + "span": [ + 297, + 298 + ], + "value": "s" + }, + { + "kind": ".", + "span": [ + 298, + 299 + ] + }, + { + "kind": "ident", + "span": [ + 299, + 303 + ], + "value": "hero" + }, + { + "kind": ",", + "span": [ + 303, + 304 + ] + }, + { + "kind": "ident", + "span": [ + 305, + 310 + ], + "value": "depth" + }, + { + "kind": "=", + "span": [ + 311, + 312 + ] + }, + { + "kind": "ident", + "span": [ + 313, + 314 + ], + "value": "s" + }, + { + "kind": ".", + "span": [ + 314, + 315 + ] + }, + { + "kind": "ident", + "span": [ + 315, + 320 + ], + "value": "depth" + }, + { + "kind": ",", + "span": [ + 320, + 321 + ] + }, + { + "kind": "ident", + "span": [ + 322, + 325 + ], + "value": "fog" + }, + { + "kind": "=", + "span": [ + 326, + 327 + ] + }, + { + "kind": "ident", + "span": [ + 328, + 329 + ], + "value": "s" + }, + { + "kind": ".", + "span": [ + 329, + 330 + ] + }, + { + "kind": "ident", + "span": [ + 330, + 333 + ], + "value": "fog" + }, + { + "kind": ",", + "span": [ + 333, + 334 + ] + }, + { + "kind": "ident", + "span": [ + 335, + 339 + ], + "value": "mist" + }, + { + "kind": "=", + "span": [ + 340, + 341 + ] + }, + { + "kind": "int", + "span": [ + 342, + 343 + ], + "value": "7" + }, + { + "kind": "}", + "span": [ + 344, + 345 + ] + }, + { + "kind": ",", + "span": [ + 345, + 346 + ] + }, + { + "kind": "ident", + "span": [ + 347, + 356 + ], + "value": "downgrade" + }, + { + "kind": "=", + "span": [ + 357, + 358 + ] + }, + { + "kind": "ident", + "span": [ + 359, + 363 + ], + "value": "auto" + }, + { + "kind": ")", + "span": [ + 363, + 364 + ] + }, + { + "kind": "uid", + "span": [ + 369, + 371 + ], + "value": "V1" + }, + { + "kind": "->", + "span": [ + 372, + 374 + ] + }, + { + "kind": "uid", + "span": [ + 375, + 377 + ], + "value": "V3" + }, + { + "kind": "=", + "span": [ + 378, + 379 + ] + }, + { + "kind": "ident", + "span": [ + 380, + 384 + ], + "value": "auto" + }, + { + "kind": "}", + "span": [ + 387, + 388 + ] + }, + { + "kind": "}", + "span": [ + 389, + 390 + ] + }, + { + "kind": "v;", + "span": [ + 390, + 390 + ] + }, + { + "kind": "fn", + "span": [ + 392, + 394 + ] + }, + { + "kind": "ident", + "span": [ + 395, + 402 + ], + "value": "current" + }, + { + "kind": "(", + "span": [ + 402, + 403 + ] + }, + { + "kind": ")", + "span": [ + 403, + 404 + ] + }, + { + "kind": ":", + "span": [ + 405, + 406 + ] + }, + { + "kind": "uid", + "span": [ + 407, + 411 + ], + "value": "Save" + }, + { + "kind": "=", + "span": [ + 412, + 413 + ] + }, + { + "kind": "uid", + "span": [ + 414, + 418 + ], + "value": "Save" + }, + { + "kind": "{", + "span": [ + 419, + 420 + ] + }, + { + "kind": "ident", + "span": [ + 421, + 425 + ], + "value": "hero" + }, + { + "kind": "=", + "span": [ + 426, + 427 + ] + }, + { + "kind": "str", + "span": [ + 428, + 433 + ], + "value": "Ada" + }, + { + "kind": ",", + "span": [ + 433, + 434 + ] + }, + { + "kind": "ident", + "span": [ + 435, + 440 + ], + "value": "depth" + }, + { + "kind": "=", + "span": [ + 441, + 442 + ] + }, + { + "kind": "int", + "span": [ + 443, + 445 + ], + "value": "12" + }, + { + "kind": ",", + "span": [ + 445, + 446 + ] + }, + { + "kind": "ident", + "span": [ + 447, + 450 + ], + "value": "fog" + }, + { + "kind": "=", + "span": [ + 451, + 452 + ] + }, + { + "kind": "int", + "span": [ + 453, + 455 + ], + "value": "30" + }, + { + "kind": ",", + "span": [ + 455, + 456 + ] + }, + { + "kind": "ident", + "span": [ + 457, + 461 + ], + "value": "mist" + }, + { + "kind": "=", + "span": [ + 462, + 463 + ] + }, + { + "kind": "int", + "span": [ + 464, + 466 + ], + "value": "99" + }, + { + "kind": "}", + "span": [ + 467, + 468 + ] + }, + { + "kind": "v}", + "span": [ + 468, + 468 + ] + } + ], + "trivia": [ + { + "kind": "comment", + "span": [ + 0, + 73 + ] + }, + { + "kind": "blank", + "span": [ + 90, + 92 + ] + }, + { + "kind": "blank", + "span": [ + 390, + 392 + ] + } + ] +} diff --git a/tests/fixtures/syntax/released/0.15.0/types.surface-syntax.json b/tests/fixtures/syntax/released/0.15.0/types.surface-syntax.json new file mode 100644 index 00000000..4d7336ba --- /dev/null +++ b/tests/fixtures/syntax/released/0.15.0/types.surface-syntax.json @@ -0,0 +1,572 @@ +{ + "schema": "prism-surface-syntax-v1", + "compiler": "0.15.0", + "source": { + "digest": "2544973d2dc5f86eed0354cfb43ba57782333229b70adef424b180a08f35198c", + "text": "-- Type syntax: rows, kinds, dimensions, unboxed shapes, open effect tails.\neffect Tick\n tick() : Int\n\ntype Cmd(a, e : Row) = Cmd(() -> a ! {e})\n\ntype Grid(n) = Grid(Vec(Int, n))\n\nfn poly(x : a, f : f(a)) : a = x\n\nfn generic(xs : List(Int), p : (Int, Float)) : Option(Int) = head(xs)\n\nfn effectful() : Int ! {Tick} = tick()\n\nfn forwarding(act : () -> Int ! {e}) : Int ! {Tick | e} = act()\n\nfn boxed_unboxed(t : #(Int, Float), r : #{ w : Int, h : Int }) : Int =\n let s = #{ w = r.#w, h = r.#h }\n let u = #(1, 2.5)\n s.#w + s.#h\n\nfn annotated() : Int =\n let f = (\\(v) -> v : forall a. (a) -> a)\n f((3 : Int))\n" + }, + "items": [ + { + "kind": "effect", + "name": "Tick", + "ops": [ + { + "name": "tick", + "ret": { + "kind": "int" + } + } + ], + "span": [ + 76, + 102 + ] + }, + { + "ctors": [ + { + "args": [ + { + "effects": { + "labels": [ + { + "name": "e" + } + ] + }, + "kind": "fun", + "ret": { + "kind": "var", + "name": "a" + } + } + ], + "name": "Cmd" + } + ], + "kind": "data", + "name": "Cmd", + "param_kinds": [ + "type", + "row" + ], + "params": [ + "a", + "e" + ], + "span": [ + 104, + 145 + ] + }, + { + "ctors": [ + { + "args": [ + { + "args": [ + { + "kind": "int" + }, + { + "kind": "var", + "name": "n" + } + ], + "kind": "con", + "name": "Vec" + } + ], + "name": "Grid" + } + ], + "kind": "data", + "name": "Grid", + "param_kinds": [ + "type" + ], + "params": [ + "n" + ], + "span": [ + 147, + 179 + ] + }, + { + "body": { + "kind": "var", + "name": "x", + "span": [ + 212, + 213 + ] + }, + "kind": "fn", + "name": "poly", + "params": [ + { + "name": "x", + "ty": { + "kind": "var", + "name": "a" + } + }, + { + "name": "f", + "ty": { + "args": [ + { + "kind": "var", + "name": "a" + } + ], + "head": "f", + "kind": "app" + } + } + ], + "ret": { + "kind": "var", + "name": "a" + }, + "span": [ + 181, + 213 + ] + }, + { + "body": { + "args": [ + { + "kind": "var", + "name": "xs", + "span": [ + 281, + 283 + ] + } + ], + "head": { + "kind": "var", + "name": "head", + "span": [ + 276, + 280 + ] + }, + "kind": "call", + "span": [ + 276, + 284 + ] + }, + "kind": "fn", + "name": "generic", + "params": [ + { + "name": "xs", + "ty": { + "args": [ + { + "kind": "int" + } + ], + "kind": "con", + "name": "List" + } + }, + { + "name": "p", + "ty": { + "items": [ + { + "kind": "int" + }, + { + "kind": "float" + } + ], + "kind": "tuple" + } + } + ], + "ret": { + "args": [ + { + "kind": "int" + } + ], + "kind": "con", + "name": "Option" + }, + "span": [ + 215, + 284 + ] + }, + { + "body": { + "head": { + "kind": "var", + "name": "tick", + "span": [ + 318, + 322 + ] + }, + "kind": "call", + "span": [ + 318, + 324 + ] + }, + "effects": { + "labels": [ + { + "name": "Tick" + } + ] + }, + "kind": "fn", + "name": "effectful", + "ret": { + "kind": "int" + }, + "span": [ + 286, + 324 + ] + }, + { + "body": { + "head": { + "kind": "var", + "name": "act", + "span": [ + 384, + 387 + ] + }, + "kind": "call", + "span": [ + 384, + 389 + ] + }, + "effects": { + "labels": [ + { + "name": "Tick" + } + ], + "tail": "e" + }, + "kind": "fn", + "name": "forwarding", + "params": [ + { + "name": "act", + "ty": { + "effects": { + "labels": [ + { + "name": "e" + } + ] + }, + "kind": "fun", + "ret": { + "kind": "int" + } + } + } + ], + "ret": { + "kind": "int" + }, + "span": [ + 326, + 389 + ] + }, + { + "body": { + "body": { + "body": { + "kind": "bin", + "lhs": { + "expr": { + "kind": "var", + "name": "s", + "span": [ + 518, + 519 + ] + }, + "kind": "unboxed-field", + "name": "w", + "span": [ + 518, + 522 + ] + }, + "op": "+", + "rhs": { + "expr": { + "kind": "var", + "name": "s", + "span": [ + 525, + 526 + ] + }, + "kind": "unboxed-field", + "name": "h", + "span": [ + 525, + 529 + ] + }, + "span": [ + 518, + 529 + ] + }, + "kind": "let", + "name": "u", + "span": [ + 498, + 529 + ], + "value": { + "items": [ + { + "kind": "int", + "span": [ + 508, + 509 + ], + "value": "1" + }, + { + "kind": "float", + "span": [ + 511, + 514 + ], + "value": "2.5" + } + ], + "kind": "unboxed-tuple", + "span": [ + 506, + 515 + ] + } + }, + "kind": "let", + "name": "s", + "span": [ + 464, + 529 + ], + "value": { + "fields": [ + { + "name": "w", + "value": { + "expr": { + "kind": "var", + "name": "r", + "span": [ + 479, + 480 + ] + }, + "kind": "unboxed-field", + "name": "w", + "span": [ + 479, + 483 + ] + } + }, + { + "name": "h", + "value": { + "expr": { + "kind": "var", + "name": "r", + "span": [ + 489, + 490 + ] + }, + "kind": "unboxed-field", + "name": "h", + "span": [ + 489, + 493 + ] + } + } + ], + "kind": "unboxed-record", + "span": [ + 472, + 495 + ] + } + }, + "kind": "fn", + "name": "boxed_unboxed", + "params": [ + { + "name": "t", + "ty": { + "items": [ + { + "kind": "int" + }, + { + "kind": "float" + } + ], + "kind": "unboxed-tuple" + } + }, + { + "name": "r", + "ty": { + "fields": [ + { + "name": "w", + "ty": { + "kind": "int" + } + }, + { + "name": "h", + "ty": { + "kind": "int" + } + } + ], + "kind": "unboxed-record" + } + } + ], + "ret": { + "kind": "int" + }, + "span": [ + 391, + 529 + ] + }, + { + "body": { + "body": { + "args": [ + { + "expr": { + "kind": "int", + "span": [ + 602, + 603 + ], + "value": "3" + }, + "kind": "ann", + "span": [ + 601, + 610 + ], + "ty": { + "kind": "int" + } + } + ], + "head": { + "kind": "var", + "name": "f", + "span": [ + 599, + 600 + ] + }, + "kind": "call", + "span": [ + 599, + 611 + ] + }, + "kind": "let", + "name": "f", + "span": [ + 556, + 611 + ], + "value": { + "expr": { + "body": { + "kind": "var", + "name": "v", + "span": [ + 573, + 574 + ] + }, + "kind": "lam", + "params": [ + { + "name": "v" + } + ], + "span": [ + 565, + 574 + ] + }, + "kind": "ann", + "span": [ + 564, + 596 + ], + "ty": { + "kind": "forall", + "ty": { + "effects": { + "labels": [] + }, + "kind": "fun", + "params": [ + { + "kind": "var", + "name": "a" + } + ], + "ret": { + "kind": "var", + "name": "a" + } + }, + "vars": [ + "a" + ] + } + } + }, + "kind": "fn", + "name": "annotated", + "ret": { + "kind": "int" + }, + "span": [ + 531, + 611 + ] + } + ] +} diff --git a/tests/fixtures/syntax/released/0.15.0/types.syntax-diagnostics.json b/tests/fixtures/syntax/released/0.15.0/types.syntax-diagnostics.json new file mode 100644 index 00000000..8f340df4 --- /dev/null +++ b/tests/fixtures/syntax/released/0.15.0/types.syntax-diagnostics.json @@ -0,0 +1,9 @@ +{ + "schema": "prism-syntax-diagnostics-v1", + "compiler": "0.15.0", + "source": { + "digest": "2544973d2dc5f86eed0354cfb43ba57782333229b70adef424b180a08f35198c", + "text": "-- Type syntax: rows, kinds, dimensions, unboxed shapes, open effect tails.\neffect Tick\n tick() : Int\n\ntype Cmd(a, e : Row) = Cmd(() -> a ! {e})\n\ntype Grid(n) = Grid(Vec(Int, n))\n\nfn poly(x : a, f : f(a)) : a = x\n\nfn generic(xs : List(Int), p : (Int, Float)) : Option(Int) = head(xs)\n\nfn effectful() : Int ! {Tick} = tick()\n\nfn forwarding(act : () -> Int ! {e}) : Int ! {Tick | e} = act()\n\nfn boxed_unboxed(t : #(Int, Float), r : #{ w : Int, h : Int }) : Int =\n let s = #{ w = r.#w, h = r.#h }\n let u = #(1, 2.5)\n s.#w + s.#h\n\nfn annotated() : Int =\n let f = (\\(v) -> v : forall a. (a) -> a)\n f((3 : Int))\n" + }, + "diagnostics": [] +} diff --git a/tests/fixtures/syntax/released/0.15.0/types.syntax-tokens.json b/tests/fixtures/syntax/released/0.15.0/types.syntax-tokens.json new file mode 100644 index 00000000..1967a0fb --- /dev/null +++ b/tests/fixtures/syntax/released/0.15.0/types.syntax-tokens.json @@ -0,0 +1,3548 @@ +{ + "schema": "prism-syntax-tokens-v1", + "compiler": "0.15.0", + "source": { + "digest": "2544973d2dc5f86eed0354cfb43ba57782333229b70adef424b180a08f35198c", + "text": "-- Type syntax: rows, kinds, dimensions, unboxed shapes, open effect tails.\neffect Tick\n tick() : Int\n\ntype Cmd(a, e : Row) = Cmd(() -> a ! {e})\n\ntype Grid(n) = Grid(Vec(Int, n))\n\nfn poly(x : a, f : f(a)) : a = x\n\nfn generic(xs : List(Int), p : (Int, Float)) : Option(Int) = head(xs)\n\nfn effectful() : Int ! {Tick} = tick()\n\nfn forwarding(act : () -> Int ! {e}) : Int ! {Tick | e} = act()\n\nfn boxed_unboxed(t : #(Int, Float), r : #{ w : Int, h : Int }) : Int =\n let s = #{ w = r.#w, h = r.#h }\n let u = #(1, 2.5)\n s.#w + s.#h\n\nfn annotated() : Int =\n let f = (\\(v) -> v : forall a. (a) -> a)\n f((3 : Int))\n" + }, + "raw": [ + { + "kind": "effect", + "span": [ + 76, + 82 + ] + }, + { + "kind": "uid", + "span": [ + 83, + 87 + ], + "value": "Tick" + }, + { + "kind": "ident", + "span": [ + 90, + 94 + ], + "value": "tick" + }, + { + "kind": "(", + "span": [ + 94, + 95 + ] + }, + { + "kind": ")", + "span": [ + 95, + 96 + ] + }, + { + "kind": ":", + "span": [ + 97, + 98 + ] + }, + { + "kind": "Int", + "span": [ + 99, + 102 + ] + }, + { + "kind": "type", + "span": [ + 104, + 108 + ] + }, + { + "kind": "uid", + "span": [ + 109, + 112 + ], + "value": "Cmd" + }, + { + "kind": "(", + "span": [ + 112, + 113 + ] + }, + { + "kind": "ident", + "span": [ + 113, + 114 + ], + "value": "a" + }, + { + "kind": ",", + "span": [ + 114, + 115 + ] + }, + { + "kind": "ident", + "span": [ + 116, + 117 + ], + "value": "e" + }, + { + "kind": ":", + "span": [ + 118, + 119 + ] + }, + { + "kind": "uid", + "span": [ + 120, + 123 + ], + "value": "Row" + }, + { + "kind": ")", + "span": [ + 123, + 124 + ] + }, + { + "kind": "=", + "span": [ + 125, + 126 + ] + }, + { + "kind": "uid", + "span": [ + 127, + 130 + ], + "value": "Cmd" + }, + { + "kind": "(", + "span": [ + 130, + 131 + ] + }, + { + "kind": "(", + "span": [ + 131, + 132 + ] + }, + { + "kind": ")", + "span": [ + 132, + 133 + ] + }, + { + "kind": "->", + "span": [ + 134, + 136 + ] + }, + { + "kind": "ident", + "span": [ + 137, + 138 + ], + "value": "a" + }, + { + "kind": "!", + "span": [ + 139, + 140 + ] + }, + { + "kind": "{", + "span": [ + 141, + 142 + ] + }, + { + "kind": "ident", + "span": [ + 142, + 143 + ], + "value": "e" + }, + { + "kind": "}", + "span": [ + 143, + 144 + ] + }, + { + "kind": ")", + "span": [ + 144, + 145 + ] + }, + { + "kind": "type", + "span": [ + 147, + 151 + ] + }, + { + "kind": "uid", + "span": [ + 152, + 156 + ], + "value": "Grid" + }, + { + "kind": "(", + "span": [ + 156, + 157 + ] + }, + { + "kind": "ident", + "span": [ + 157, + 158 + ], + "value": "n" + }, + { + "kind": ")", + "span": [ + 158, + 159 + ] + }, + { + "kind": "=", + "span": [ + 160, + 161 + ] + }, + { + "kind": "uid", + "span": [ + 162, + 166 + ], + "value": "Grid" + }, + { + "kind": "(", + "span": [ + 166, + 167 + ] + }, + { + "kind": "uid", + "span": [ + 167, + 170 + ], + "value": "Vec" + }, + { + "kind": "(", + "span": [ + 170, + 171 + ] + }, + { + "kind": "Int", + "span": [ + 171, + 174 + ] + }, + { + "kind": ",", + "span": [ + 174, + 175 + ] + }, + { + "kind": "ident", + "span": [ + 176, + 177 + ], + "value": "n" + }, + { + "kind": ")", + "span": [ + 177, + 178 + ] + }, + { + "kind": ")", + "span": [ + 178, + 179 + ] + }, + { + "kind": "fn", + "span": [ + 181, + 183 + ] + }, + { + "kind": "ident", + "span": [ + 184, + 188 + ], + "value": "poly" + }, + { + "kind": "(", + "span": [ + 188, + 189 + ] + }, + { + "kind": "ident", + "span": [ + 189, + 190 + ], + "value": "x" + }, + { + "kind": ":", + "span": [ + 191, + 192 + ] + }, + { + "kind": "ident", + "span": [ + 193, + 194 + ], + "value": "a" + }, + { + "kind": ",", + "span": [ + 194, + 195 + ] + }, + { + "kind": "ident", + "span": [ + 196, + 197 + ], + "value": "f" + }, + { + "kind": ":", + "span": [ + 198, + 199 + ] + }, + { + "kind": "ident", + "span": [ + 200, + 201 + ], + "value": "f" + }, + { + "kind": "(", + "span": [ + 201, + 202 + ] + }, + { + "kind": "ident", + "span": [ + 202, + 203 + ], + "value": "a" + }, + { + "kind": ")", + "span": [ + 203, + 204 + ] + }, + { + "kind": ")", + "span": [ + 204, + 205 + ] + }, + { + "kind": ":", + "span": [ + 206, + 207 + ] + }, + { + "kind": "ident", + "span": [ + 208, + 209 + ], + "value": "a" + }, + { + "kind": "=", + "span": [ + 210, + 211 + ] + }, + { + "kind": "ident", + "span": [ + 212, + 213 + ], + "value": "x" + }, + { + "kind": "fn", + "span": [ + 215, + 217 + ] + }, + { + "kind": "ident", + "span": [ + 218, + 225 + ], + "value": "generic" + }, + { + "kind": "(", + "span": [ + 225, + 226 + ] + }, + { + "kind": "ident", + "span": [ + 226, + 228 + ], + "value": "xs" + }, + { + "kind": ":", + "span": [ + 229, + 230 + ] + }, + { + "kind": "uid", + "span": [ + 231, + 235 + ], + "value": "List" + }, + { + "kind": "(", + "span": [ + 235, + 236 + ] + }, + { + "kind": "Int", + "span": [ + 236, + 239 + ] + }, + { + "kind": ")", + "span": [ + 239, + 240 + ] + }, + { + "kind": ",", + "span": [ + 240, + 241 + ] + }, + { + "kind": "ident", + "span": [ + 242, + 243 + ], + "value": "p" + }, + { + "kind": ":", + "span": [ + 244, + 245 + ] + }, + { + "kind": "(", + "span": [ + 246, + 247 + ] + }, + { + "kind": "Int", + "span": [ + 247, + 250 + ] + }, + { + "kind": ",", + "span": [ + 250, + 251 + ] + }, + { + "kind": "Float", + "span": [ + 252, + 257 + ] + }, + { + "kind": ")", + "span": [ + 257, + 258 + ] + }, + { + "kind": ")", + "span": [ + 258, + 259 + ] + }, + { + "kind": ":", + "span": [ + 260, + 261 + ] + }, + { + "kind": "uid", + "span": [ + 262, + 268 + ], + "value": "Option" + }, + { + "kind": "(", + "span": [ + 268, + 269 + ] + }, + { + "kind": "Int", + "span": [ + 269, + 272 + ] + }, + { + "kind": ")", + "span": [ + 272, + 273 + ] + }, + { + "kind": "=", + "span": [ + 274, + 275 + ] + }, + { + "kind": "ident", + "span": [ + 276, + 280 + ], + "value": "head" + }, + { + "kind": "(", + "span": [ + 280, + 281 + ] + }, + { + "kind": "ident", + "span": [ + 281, + 283 + ], + "value": "xs" + }, + { + "kind": ")", + "span": [ + 283, + 284 + ] + }, + { + "kind": "fn", + "span": [ + 286, + 288 + ] + }, + { + "kind": "ident", + "span": [ + 289, + 298 + ], + "value": "effectful" + }, + { + "kind": "(", + "span": [ + 298, + 299 + ] + }, + { + "kind": ")", + "span": [ + 299, + 300 + ] + }, + { + "kind": ":", + "span": [ + 301, + 302 + ] + }, + { + "kind": "Int", + "span": [ + 303, + 306 + ] + }, + { + "kind": "!", + "span": [ + 307, + 308 + ] + }, + { + "kind": "{", + "span": [ + 309, + 310 + ] + }, + { + "kind": "uid", + "span": [ + 310, + 314 + ], + "value": "Tick" + }, + { + "kind": "}", + "span": [ + 314, + 315 + ] + }, + { + "kind": "=", + "span": [ + 316, + 317 + ] + }, + { + "kind": "ident", + "span": [ + 318, + 322 + ], + "value": "tick" + }, + { + "kind": "(", + "span": [ + 322, + 323 + ] + }, + { + "kind": ")", + "span": [ + 323, + 324 + ] + }, + { + "kind": "fn", + "span": [ + 326, + 328 + ] + }, + { + "kind": "ident", + "span": [ + 329, + 339 + ], + "value": "forwarding" + }, + { + "kind": "(", + "span": [ + 339, + 340 + ] + }, + { + "kind": "ident", + "span": [ + 340, + 343 + ], + "value": "act" + }, + { + "kind": ":", + "span": [ + 344, + 345 + ] + }, + { + "kind": "(", + "span": [ + 346, + 347 + ] + }, + { + "kind": ")", + "span": [ + 347, + 348 + ] + }, + { + "kind": "->", + "span": [ + 349, + 351 + ] + }, + { + "kind": "Int", + "span": [ + 352, + 355 + ] + }, + { + "kind": "!", + "span": [ + 356, + 357 + ] + }, + { + "kind": "{", + "span": [ + 358, + 359 + ] + }, + { + "kind": "ident", + "span": [ + 359, + 360 + ], + "value": "e" + }, + { + "kind": "}", + "span": [ + 360, + 361 + ] + }, + { + "kind": ")", + "span": [ + 361, + 362 + ] + }, + { + "kind": ":", + "span": [ + 363, + 364 + ] + }, + { + "kind": "Int", + "span": [ + 365, + 368 + ] + }, + { + "kind": "!", + "span": [ + 369, + 370 + ] + }, + { + "kind": "{", + "span": [ + 371, + 372 + ] + }, + { + "kind": "uid", + "span": [ + 372, + 376 + ], + "value": "Tick" + }, + { + "kind": "|", + "span": [ + 377, + 378 + ] + }, + { + "kind": "ident", + "span": [ + 379, + 380 + ], + "value": "e" + }, + { + "kind": "}", + "span": [ + 380, + 381 + ] + }, + { + "kind": "=", + "span": [ + 382, + 383 + ] + }, + { + "kind": "ident", + "span": [ + 384, + 387 + ], + "value": "act" + }, + { + "kind": "(", + "span": [ + 387, + 388 + ] + }, + { + "kind": ")", + "span": [ + 388, + 389 + ] + }, + { + "kind": "fn", + "span": [ + 391, + 393 + ] + }, + { + "kind": "ident", + "span": [ + 394, + 407 + ], + "value": "boxed_unboxed" + }, + { + "kind": "(", + "span": [ + 407, + 408 + ] + }, + { + "kind": "ident", + "span": [ + 408, + 409 + ], + "value": "t" + }, + { + "kind": ":", + "span": [ + 410, + 411 + ] + }, + { + "kind": "#", + "span": [ + 412, + 413 + ] + }, + { + "kind": "(", + "span": [ + 413, + 414 + ] + }, + { + "kind": "Int", + "span": [ + 414, + 417 + ] + }, + { + "kind": ",", + "span": [ + 417, + 418 + ] + }, + { + "kind": "Float", + "span": [ + 419, + 424 + ] + }, + { + "kind": ")", + "span": [ + 424, + 425 + ] + }, + { + "kind": ",", + "span": [ + 425, + 426 + ] + }, + { + "kind": "ident", + "span": [ + 427, + 428 + ], + "value": "r" + }, + { + "kind": ":", + "span": [ + 429, + 430 + ] + }, + { + "kind": "#", + "span": [ + 431, + 432 + ] + }, + { + "kind": "{", + "span": [ + 432, + 433 + ] + }, + { + "kind": "ident", + "span": [ + 434, + 435 + ], + "value": "w" + }, + { + "kind": ":", + "span": [ + 436, + 437 + ] + }, + { + "kind": "Int", + "span": [ + 438, + 441 + ] + }, + { + "kind": ",", + "span": [ + 441, + 442 + ] + }, + { + "kind": "ident", + "span": [ + 443, + 444 + ], + "value": "h" + }, + { + "kind": ":", + "span": [ + 445, + 446 + ] + }, + { + "kind": "Int", + "span": [ + 447, + 450 + ] + }, + { + "kind": "}", + "span": [ + 451, + 452 + ] + }, + { + "kind": ")", + "span": [ + 452, + 453 + ] + }, + { + "kind": ":", + "span": [ + 454, + 455 + ] + }, + { + "kind": "Int", + "span": [ + 456, + 459 + ] + }, + { + "kind": "=", + "span": [ + 460, + 461 + ] + }, + { + "kind": "let", + "span": [ + 464, + 467 + ] + }, + { + "kind": "ident", + "span": [ + 468, + 469 + ], + "value": "s" + }, + { + "kind": "=", + "span": [ + 470, + 471 + ] + }, + { + "kind": "#", + "span": [ + 472, + 473 + ] + }, + { + "kind": "{", + "span": [ + 473, + 474 + ] + }, + { + "kind": "ident", + "span": [ + 475, + 476 + ], + "value": "w" + }, + { + "kind": "=", + "span": [ + 477, + 478 + ] + }, + { + "kind": "ident", + "span": [ + 479, + 480 + ], + "value": "r" + }, + { + "kind": ".", + "span": [ + 480, + 481 + ] + }, + { + "kind": "#", + "span": [ + 481, + 482 + ] + }, + { + "kind": "ident", + "span": [ + 482, + 483 + ], + "value": "w" + }, + { + "kind": ",", + "span": [ + 483, + 484 + ] + }, + { + "kind": "ident", + "span": [ + 485, + 486 + ], + "value": "h" + }, + { + "kind": "=", + "span": [ + 487, + 488 + ] + }, + { + "kind": "ident", + "span": [ + 489, + 490 + ], + "value": "r" + }, + { + "kind": ".", + "span": [ + 490, + 491 + ] + }, + { + "kind": "#", + "span": [ + 491, + 492 + ] + }, + { + "kind": "ident", + "span": [ + 492, + 493 + ], + "value": "h" + }, + { + "kind": "}", + "span": [ + 494, + 495 + ] + }, + { + "kind": "let", + "span": [ + 498, + 501 + ] + }, + { + "kind": "ident", + "span": [ + 502, + 503 + ], + "value": "u" + }, + { + "kind": "=", + "span": [ + 504, + 505 + ] + }, + { + "kind": "#", + "span": [ + 506, + 507 + ] + }, + { + "kind": "(", + "span": [ + 507, + 508 + ] + }, + { + "kind": "int", + "span": [ + 508, + 509 + ], + "value": "1" + }, + { + "kind": ",", + "span": [ + 509, + 510 + ] + }, + { + "kind": "float", + "span": [ + 511, + 514 + ], + "value": "2.5" + }, + { + "kind": ")", + "span": [ + 514, + 515 + ] + }, + { + "kind": "ident", + "span": [ + 518, + 519 + ], + "value": "s" + }, + { + "kind": ".", + "span": [ + 519, + 520 + ] + }, + { + "kind": "#", + "span": [ + 520, + 521 + ] + }, + { + "kind": "ident", + "span": [ + 521, + 522 + ], + "value": "w" + }, + { + "kind": "+", + "span": [ + 523, + 524 + ] + }, + { + "kind": "ident", + "span": [ + 525, + 526 + ], + "value": "s" + }, + { + "kind": ".", + "span": [ + 526, + 527 + ] + }, + { + "kind": "#", + "span": [ + 527, + 528 + ] + }, + { + "kind": "ident", + "span": [ + 528, + 529 + ], + "value": "h" + }, + { + "kind": "fn", + "span": [ + 531, + 533 + ] + }, + { + "kind": "ident", + "span": [ + 534, + 543 + ], + "value": "annotated" + }, + { + "kind": "(", + "span": [ + 543, + 544 + ] + }, + { + "kind": ")", + "span": [ + 544, + 545 + ] + }, + { + "kind": ":", + "span": [ + 546, + 547 + ] + }, + { + "kind": "Int", + "span": [ + 548, + 551 + ] + }, + { + "kind": "=", + "span": [ + 552, + 553 + ] + }, + { + "kind": "let", + "span": [ + 556, + 559 + ] + }, + { + "kind": "ident", + "span": [ + 560, + 561 + ], + "value": "f" + }, + { + "kind": "=", + "span": [ + 562, + 563 + ] + }, + { + "kind": "(", + "span": [ + 564, + 565 + ] + }, + { + "kind": "\\", + "span": [ + 565, + 566 + ] + }, + { + "kind": "(", + "span": [ + 566, + 567 + ] + }, + { + "kind": "ident", + "span": [ + 567, + 568 + ], + "value": "v" + }, + { + "kind": ")", + "span": [ + 568, + 569 + ] + }, + { + "kind": "->", + "span": [ + 570, + 572 + ] + }, + { + "kind": "ident", + "span": [ + 573, + 574 + ], + "value": "v" + }, + { + "kind": ":", + "span": [ + 575, + 576 + ] + }, + { + "kind": "forall", + "span": [ + 577, + 583 + ] + }, + { + "kind": "ident", + "span": [ + 584, + 585 + ], + "value": "a" + }, + { + "kind": ".", + "span": [ + 585, + 586 + ] + }, + { + "kind": "(", + "span": [ + 587, + 588 + ] + }, + { + "kind": "ident", + "span": [ + 588, + 589 + ], + "value": "a" + }, + { + "kind": ")", + "span": [ + 589, + 590 + ] + }, + { + "kind": "->", + "span": [ + 591, + 593 + ] + }, + { + "kind": "ident", + "span": [ + 594, + 595 + ], + "value": "a" + }, + { + "kind": ")", + "span": [ + 595, + 596 + ] + }, + { + "kind": "ident", + "span": [ + 599, + 600 + ], + "value": "f" + }, + { + "kind": "(", + "span": [ + 600, + 601 + ] + }, + { + "kind": "(", + "span": [ + 601, + 602 + ] + }, + { + "kind": "int", + "span": [ + 602, + 603 + ], + "value": "3" + }, + { + "kind": ":", + "span": [ + 604, + 605 + ] + }, + { + "kind": "Int", + "span": [ + 606, + 609 + ] + }, + { + "kind": ")", + "span": [ + 609, + 610 + ] + }, + { + "kind": ")", + "span": [ + 610, + 611 + ] + } + ], + "parse": [ + { + "kind": "v{", + "span": [ + 76, + 76 + ] + }, + { + "kind": "effect", + "span": [ + 76, + 82 + ] + }, + { + "kind": "uid", + "span": [ + 83, + 87 + ], + "value": "Tick" + }, + { + "kind": "v{", + "span": [ + 90, + 90 + ] + }, + { + "kind": "ident", + "span": [ + 90, + 94 + ], + "value": "tick" + }, + { + "kind": "(", + "span": [ + 94, + 95 + ] + }, + { + "kind": ")", + "span": [ + 95, + 96 + ] + }, + { + "kind": ":", + "span": [ + 97, + 98 + ] + }, + { + "kind": "Int", + "span": [ + 99, + 102 + ] + }, + { + "kind": "v}", + "span": [ + 102, + 102 + ] + }, + { + "kind": "v;", + "span": [ + 102, + 102 + ] + }, + { + "kind": "type", + "span": [ + 104, + 108 + ] + }, + { + "kind": "uid", + "span": [ + 109, + 112 + ], + "value": "Cmd" + }, + { + "kind": "(", + "span": [ + 112, + 113 + ] + }, + { + "kind": "ident", + "span": [ + 113, + 114 + ], + "value": "a" + }, + { + "kind": ",", + "span": [ + 114, + 115 + ] + }, + { + "kind": "ident", + "span": [ + 116, + 117 + ], + "value": "e" + }, + { + "kind": ":", + "span": [ + 118, + 119 + ] + }, + { + "kind": "uid", + "span": [ + 120, + 123 + ], + "value": "Row" + }, + { + "kind": ")", + "span": [ + 123, + 124 + ] + }, + { + "kind": "=", + "span": [ + 125, + 126 + ] + }, + { + "kind": "uid", + "span": [ + 127, + 130 + ], + "value": "Cmd" + }, + { + "kind": "(", + "span": [ + 130, + 131 + ] + }, + { + "kind": "(", + "span": [ + 131, + 132 + ] + }, + { + "kind": ")", + "span": [ + 132, + 133 + ] + }, + { + "kind": "->", + "span": [ + 134, + 136 + ] + }, + { + "kind": "ident", + "span": [ + 137, + 138 + ], + "value": "a" + }, + { + "kind": "!", + "span": [ + 139, + 140 + ] + }, + { + "kind": "{", + "span": [ + 141, + 142 + ] + }, + { + "kind": "ident", + "span": [ + 142, + 143 + ], + "value": "e" + }, + { + "kind": "}", + "span": [ + 143, + 144 + ] + }, + { + "kind": ")", + "span": [ + 144, + 145 + ] + }, + { + "kind": "v;", + "span": [ + 145, + 145 + ] + }, + { + "kind": "type", + "span": [ + 147, + 151 + ] + }, + { + "kind": "uid", + "span": [ + 152, + 156 + ], + "value": "Grid" + }, + { + "kind": "(", + "span": [ + 156, + 157 + ] + }, + { + "kind": "ident", + "span": [ + 157, + 158 + ], + "value": "n" + }, + { + "kind": ")", + "span": [ + 158, + 159 + ] + }, + { + "kind": "=", + "span": [ + 160, + 161 + ] + }, + { + "kind": "uid", + "span": [ + 162, + 166 + ], + "value": "Grid" + }, + { + "kind": "(", + "span": [ + 166, + 167 + ] + }, + { + "kind": "uid", + "span": [ + 167, + 170 + ], + "value": "Vec" + }, + { + "kind": "(", + "span": [ + 170, + 171 + ] + }, + { + "kind": "Int", + "span": [ + 171, + 174 + ] + }, + { + "kind": ",", + "span": [ + 174, + 175 + ] + }, + { + "kind": "ident", + "span": [ + 176, + 177 + ], + "value": "n" + }, + { + "kind": ")", + "span": [ + 177, + 178 + ] + }, + { + "kind": ")", + "span": [ + 178, + 179 + ] + }, + { + "kind": "v;", + "span": [ + 179, + 179 + ] + }, + { + "kind": "fn", + "span": [ + 181, + 183 + ] + }, + { + "kind": "ident", + "span": [ + 184, + 188 + ], + "value": "poly" + }, + { + "kind": "(", + "span": [ + 188, + 189 + ] + }, + { + "kind": "ident", + "span": [ + 189, + 190 + ], + "value": "x" + }, + { + "kind": ":", + "span": [ + 191, + 192 + ] + }, + { + "kind": "ident", + "span": [ + 193, + 194 + ], + "value": "a" + }, + { + "kind": ",", + "span": [ + 194, + 195 + ] + }, + { + "kind": "ident", + "span": [ + 196, + 197 + ], + "value": "f" + }, + { + "kind": ":", + "span": [ + 198, + 199 + ] + }, + { + "kind": "ident", + "span": [ + 200, + 201 + ], + "value": "f" + }, + { + "kind": "(", + "span": [ + 201, + 202 + ] + }, + { + "kind": "ident", + "span": [ + 202, + 203 + ], + "value": "a" + }, + { + "kind": ")", + "span": [ + 203, + 204 + ] + }, + { + "kind": ")", + "span": [ + 204, + 205 + ] + }, + { + "kind": ":", + "span": [ + 206, + 207 + ] + }, + { + "kind": "ident", + "span": [ + 208, + 209 + ], + "value": "a" + }, + { + "kind": "=", + "span": [ + 210, + 211 + ] + }, + { + "kind": "ident", + "span": [ + 212, + 213 + ], + "value": "x" + }, + { + "kind": "v;", + "span": [ + 213, + 213 + ] + }, + { + "kind": "fn", + "span": [ + 215, + 217 + ] + }, + { + "kind": "ident", + "span": [ + 218, + 225 + ], + "value": "generic" + }, + { + "kind": "(", + "span": [ + 225, + 226 + ] + }, + { + "kind": "ident", + "span": [ + 226, + 228 + ], + "value": "xs" + }, + { + "kind": ":", + "span": [ + 229, + 230 + ] + }, + { + "kind": "uid", + "span": [ + 231, + 235 + ], + "value": "List" + }, + { + "kind": "(", + "span": [ + 235, + 236 + ] + }, + { + "kind": "Int", + "span": [ + 236, + 239 + ] + }, + { + "kind": ")", + "span": [ + 239, + 240 + ] + }, + { + "kind": ",", + "span": [ + 240, + 241 + ] + }, + { + "kind": "ident", + "span": [ + 242, + 243 + ], + "value": "p" + }, + { + "kind": ":", + "span": [ + 244, + 245 + ] + }, + { + "kind": "(", + "span": [ + 246, + 247 + ] + }, + { + "kind": "Int", + "span": [ + 247, + 250 + ] + }, + { + "kind": ",", + "span": [ + 250, + 251 + ] + }, + { + "kind": "Float", + "span": [ + 252, + 257 + ] + }, + { + "kind": ")", + "span": [ + 257, + 258 + ] + }, + { + "kind": ")", + "span": [ + 258, + 259 + ] + }, + { + "kind": ":", + "span": [ + 260, + 261 + ] + }, + { + "kind": "uid", + "span": [ + 262, + 268 + ], + "value": "Option" + }, + { + "kind": "(", + "span": [ + 268, + 269 + ] + }, + { + "kind": "Int", + "span": [ + 269, + 272 + ] + }, + { + "kind": ")", + "span": [ + 272, + 273 + ] + }, + { + "kind": "=", + "span": [ + 274, + 275 + ] + }, + { + "kind": "ident", + "span": [ + 276, + 280 + ], + "value": "head" + }, + { + "kind": "(", + "span": [ + 280, + 281 + ] + }, + { + "kind": "ident", + "span": [ + 281, + 283 + ], + "value": "xs" + }, + { + "kind": ")", + "span": [ + 283, + 284 + ] + }, + { + "kind": "v;", + "span": [ + 284, + 284 + ] + }, + { + "kind": "fn", + "span": [ + 286, + 288 + ] + }, + { + "kind": "ident", + "span": [ + 289, + 298 + ], + "value": "effectful" + }, + { + "kind": "(", + "span": [ + 298, + 299 + ] + }, + { + "kind": ")", + "span": [ + 299, + 300 + ] + }, + { + "kind": ":", + "span": [ + 301, + 302 + ] + }, + { + "kind": "Int", + "span": [ + 303, + 306 + ] + }, + { + "kind": "!", + "span": [ + 307, + 308 + ] + }, + { + "kind": "{", + "span": [ + 309, + 310 + ] + }, + { + "kind": "uid", + "span": [ + 310, + 314 + ], + "value": "Tick" + }, + { + "kind": "}", + "span": [ + 314, + 315 + ] + }, + { + "kind": "=", + "span": [ + 316, + 317 + ] + }, + { + "kind": "ident", + "span": [ + 318, + 322 + ], + "value": "tick" + }, + { + "kind": "(", + "span": [ + 322, + 323 + ] + }, + { + "kind": ")", + "span": [ + 323, + 324 + ] + }, + { + "kind": "v;", + "span": [ + 324, + 324 + ] + }, + { + "kind": "fn", + "span": [ + 326, + 328 + ] + }, + { + "kind": "ident", + "span": [ + 329, + 339 + ], + "value": "forwarding" + }, + { + "kind": "(", + "span": [ + 339, + 340 + ] + }, + { + "kind": "ident", + "span": [ + 340, + 343 + ], + "value": "act" + }, + { + "kind": ":", + "span": [ + 344, + 345 + ] + }, + { + "kind": "(", + "span": [ + 346, + 347 + ] + }, + { + "kind": ")", + "span": [ + 347, + 348 + ] + }, + { + "kind": "->", + "span": [ + 349, + 351 + ] + }, + { + "kind": "Int", + "span": [ + 352, + 355 + ] + }, + { + "kind": "!", + "span": [ + 356, + 357 + ] + }, + { + "kind": "{", + "span": [ + 358, + 359 + ] + }, + { + "kind": "ident", + "span": [ + 359, + 360 + ], + "value": "e" + }, + { + "kind": "}", + "span": [ + 360, + 361 + ] + }, + { + "kind": ")", + "span": [ + 361, + 362 + ] + }, + { + "kind": ":", + "span": [ + 363, + 364 + ] + }, + { + "kind": "Int", + "span": [ + 365, + 368 + ] + }, + { + "kind": "!", + "span": [ + 369, + 370 + ] + }, + { + "kind": "{", + "span": [ + 371, + 372 + ] + }, + { + "kind": "uid", + "span": [ + 372, + 376 + ], + "value": "Tick" + }, + { + "kind": "|", + "span": [ + 377, + 378 + ] + }, + { + "kind": "ident", + "span": [ + 379, + 380 + ], + "value": "e" + }, + { + "kind": "}", + "span": [ + 380, + 381 + ] + }, + { + "kind": "=", + "span": [ + 382, + 383 + ] + }, + { + "kind": "ident", + "span": [ + 384, + 387 + ], + "value": "act" + }, + { + "kind": "(", + "span": [ + 387, + 388 + ] + }, + { + "kind": ")", + "span": [ + 388, + 389 + ] + }, + { + "kind": "v;", + "span": [ + 389, + 389 + ] + }, + { + "kind": "fn", + "span": [ + 391, + 393 + ] + }, + { + "kind": "ident", + "span": [ + 394, + 407 + ], + "value": "boxed_unboxed" + }, + { + "kind": "(", + "span": [ + 407, + 408 + ] + }, + { + "kind": "ident", + "span": [ + 408, + 409 + ], + "value": "t" + }, + { + "kind": ":", + "span": [ + 410, + 411 + ] + }, + { + "kind": "#", + "span": [ + 412, + 413 + ] + }, + { + "kind": "(", + "span": [ + 413, + 414 + ] + }, + { + "kind": "Int", + "span": [ + 414, + 417 + ] + }, + { + "kind": ",", + "span": [ + 417, + 418 + ] + }, + { + "kind": "Float", + "span": [ + 419, + 424 + ] + }, + { + "kind": ")", + "span": [ + 424, + 425 + ] + }, + { + "kind": ",", + "span": [ + 425, + 426 + ] + }, + { + "kind": "ident", + "span": [ + 427, + 428 + ], + "value": "r" + }, + { + "kind": ":", + "span": [ + 429, + 430 + ] + }, + { + "kind": "#", + "span": [ + 431, + 432 + ] + }, + { + "kind": "{", + "span": [ + 432, + 433 + ] + }, + { + "kind": "ident", + "span": [ + 434, + 435 + ], + "value": "w" + }, + { + "kind": ":", + "span": [ + 436, + 437 + ] + }, + { + "kind": "Int", + "span": [ + 438, + 441 + ] + }, + { + "kind": ",", + "span": [ + 441, + 442 + ] + }, + { + "kind": "ident", + "span": [ + 443, + 444 + ], + "value": "h" + }, + { + "kind": ":", + "span": [ + 445, + 446 + ] + }, + { + "kind": "Int", + "span": [ + 447, + 450 + ] + }, + { + "kind": "}", + "span": [ + 451, + 452 + ] + }, + { + "kind": ")", + "span": [ + 452, + 453 + ] + }, + { + "kind": ":", + "span": [ + 454, + 455 + ] + }, + { + "kind": "Int", + "span": [ + 456, + 459 + ] + }, + { + "kind": "=", + "span": [ + 460, + 461 + ] + }, + { + "kind": "v{", + "span": [ + 464, + 464 + ] + }, + { + "kind": "let", + "span": [ + 464, + 467 + ] + }, + { + "kind": "ident", + "span": [ + 468, + 469 + ], + "value": "s" + }, + { + "kind": "=", + "span": [ + 470, + 471 + ] + }, + { + "kind": "#", + "span": [ + 472, + 473 + ] + }, + { + "kind": "{", + "span": [ + 473, + 474 + ] + }, + { + "kind": "ident", + "span": [ + 475, + 476 + ], + "value": "w" + }, + { + "kind": "=", + "span": [ + 477, + 478 + ] + }, + { + "kind": "ident", + "span": [ + 479, + 480 + ], + "value": "r" + }, + { + "kind": ".", + "span": [ + 480, + 481 + ] + }, + { + "kind": "#", + "span": [ + 481, + 482 + ] + }, + { + "kind": "ident", + "span": [ + 482, + 483 + ], + "value": "w" + }, + { + "kind": ",", + "span": [ + 483, + 484 + ] + }, + { + "kind": "ident", + "span": [ + 485, + 486 + ], + "value": "h" + }, + { + "kind": "=", + "span": [ + 487, + 488 + ] + }, + { + "kind": "ident", + "span": [ + 489, + 490 + ], + "value": "r" + }, + { + "kind": ".", + "span": [ + 490, + 491 + ] + }, + { + "kind": "#", + "span": [ + 491, + 492 + ] + }, + { + "kind": "ident", + "span": [ + 492, + 493 + ], + "value": "h" + }, + { + "kind": "}", + "span": [ + 494, + 495 + ] + }, + { + "kind": "v;", + "span": [ + 495, + 495 + ] + }, + { + "kind": "let", + "span": [ + 498, + 501 + ] + }, + { + "kind": "ident", + "span": [ + 502, + 503 + ], + "value": "u" + }, + { + "kind": "=", + "span": [ + 504, + 505 + ] + }, + { + "kind": "#", + "span": [ + 506, + 507 + ] + }, + { + "kind": "(", + "span": [ + 507, + 508 + ] + }, + { + "kind": "int", + "span": [ + 508, + 509 + ], + "value": "1" + }, + { + "kind": ",", + "span": [ + 509, + 510 + ] + }, + { + "kind": "float", + "span": [ + 511, + 514 + ], + "value": "2.5" + }, + { + "kind": ")", + "span": [ + 514, + 515 + ] + }, + { + "kind": "v;", + "span": [ + 515, + 515 + ] + }, + { + "kind": "ident", + "span": [ + 518, + 519 + ], + "value": "s" + }, + { + "kind": ".", + "span": [ + 519, + 520 + ] + }, + { + "kind": "#", + "span": [ + 520, + 521 + ] + }, + { + "kind": "ident", + "span": [ + 521, + 522 + ], + "value": "w" + }, + { + "kind": "+", + "span": [ + 523, + 524 + ] + }, + { + "kind": "ident", + "span": [ + 525, + 526 + ], + "value": "s" + }, + { + "kind": ".", + "span": [ + 526, + 527 + ] + }, + { + "kind": "#", + "span": [ + 527, + 528 + ] + }, + { + "kind": "ident", + "span": [ + 528, + 529 + ], + "value": "h" + }, + { + "kind": "v}", + "span": [ + 529, + 529 + ] + }, + { + "kind": "v;", + "span": [ + 529, + 529 + ] + }, + { + "kind": "fn", + "span": [ + 531, + 533 + ] + }, + { + "kind": "ident", + "span": [ + 534, + 543 + ], + "value": "annotated" + }, + { + "kind": "(", + "span": [ + 543, + 544 + ] + }, + { + "kind": ")", + "span": [ + 544, + 545 + ] + }, + { + "kind": ":", + "span": [ + 546, + 547 + ] + }, + { + "kind": "Int", + "span": [ + 548, + 551 + ] + }, + { + "kind": "=", + "span": [ + 552, + 553 + ] + }, + { + "kind": "v{", + "span": [ + 556, + 556 + ] + }, + { + "kind": "let", + "span": [ + 556, + 559 + ] + }, + { + "kind": "ident", + "span": [ + 560, + 561 + ], + "value": "f" + }, + { + "kind": "=", + "span": [ + 562, + 563 + ] + }, + { + "kind": "(", + "span": [ + 564, + 565 + ] + }, + { + "kind": "\\", + "span": [ + 565, + 566 + ] + }, + { + "kind": "(", + "span": [ + 566, + 567 + ] + }, + { + "kind": "ident", + "span": [ + 567, + 568 + ], + "value": "v" + }, + { + "kind": ")", + "span": [ + 568, + 569 + ] + }, + { + "kind": "->", + "span": [ + 570, + 572 + ] + }, + { + "kind": "ident", + "span": [ + 573, + 574 + ], + "value": "v" + }, + { + "kind": ":", + "span": [ + 575, + 576 + ] + }, + { + "kind": "forall", + "span": [ + 577, + 583 + ] + }, + { + "kind": "ident", + "span": [ + 584, + 585 + ], + "value": "a" + }, + { + "kind": ".", + "span": [ + 585, + 586 + ] + }, + { + "kind": "(", + "span": [ + 587, + 588 + ] + }, + { + "kind": "ident", + "span": [ + 588, + 589 + ], + "value": "a" + }, + { + "kind": ")", + "span": [ + 589, + 590 + ] + }, + { + "kind": "->", + "span": [ + 591, + 593 + ] + }, + { + "kind": "ident", + "span": [ + 594, + 595 + ], + "value": "a" + }, + { + "kind": ")", + "span": [ + 595, + 596 + ] + }, + { + "kind": "v;", + "span": [ + 596, + 596 + ] + }, + { + "kind": "ident", + "span": [ + 599, + 600 + ], + "value": "f" + }, + { + "kind": "(", + "span": [ + 600, + 601 + ] + }, + { + "kind": "(", + "span": [ + 601, + 602 + ] + }, + { + "kind": "int", + "span": [ + 602, + 603 + ], + "value": "3" + }, + { + "kind": ":", + "span": [ + 604, + 605 + ] + }, + { + "kind": "Int", + "span": [ + 606, + 609 + ] + }, + { + "kind": ")", + "span": [ + 609, + 610 + ] + }, + { + "kind": ")", + "span": [ + 610, + 611 + ] + }, + { + "kind": "v}", + "span": [ + 611, + 611 + ] + }, + { + "kind": "v}", + "span": [ + 611, + 611 + ] + } + ], + "trivia": [ + { + "kind": "comment", + "span": [ + 0, + 75 + ] + }, + { + "kind": "blank", + "span": [ + 102, + 104 + ] + }, + { + "kind": "blank", + "span": [ + 145, + 147 + ] + }, + { + "kind": "blank", + "span": [ + 179, + 181 + ] + }, + { + "kind": "blank", + "span": [ + 213, + 215 + ] + }, + { + "kind": "blank", + "span": [ + 284, + 286 + ] + }, + { + "kind": "blank", + "span": [ + 324, + 326 + ] + }, + { + "kind": "blank", + "span": [ + 389, + 391 + ] + }, + { + "kind": "blank", + "span": [ + 529, + 531 + ] + } + ] +} diff --git a/tests/fixtures/syntax/roundtrip.surface-syntax.json b/tests/fixtures/syntax/roundtrip.surface-syntax.json index 0dafbb69..f9fcf4b0 100644 --- a/tests/fixtures/syntax/roundtrip.surface-syntax.json +++ b/tests/fixtures/syntax/roundtrip.surface-syntax.json @@ -1,6 +1,6 @@ { "schema": "prism-surface-syntax-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "b2df556543bc484a6e7f6af2ba9ec0c0c2d72a7beb3d80df74821530dd3bc819", "text": "-- The round-trip gate for the versioned syntax artifacts: read one artifact\n-- file, decode it into the typed Syntax vocabulary, re-encode, and print the\n-- exact bytes. arg(0) is the artifact path; arg(1) selects the family\n-- (\"tokens\", \"surface\", \"diagnostics\", or \"resolved\"). The \"resolved-nodes\"\n-- mode instead decodes the resolved document and prints the id of every node\n-- reached by traversing each function body with `rnode_universe`, one per\n-- line: the Prism-side traversal the checker join is diffed against, reading\n-- only the artifact bytes. A refusal prints one structured error line.\nimport Data.Foldable (length)\nimport Data.List (concat_map)\nimport Syntax.Codec (..)\nimport Syntax.Resolved (..)\n\nfn main() =\n let text = read_file(arg(0))\n if arg(1) == \"tokens\" then\n match decode_tokens(text) of\n Err(e) => println(\"decode error: {codec_error_message(e)}\")\n Ok(d) => println(encode_tokens(d))\n elif arg(1) == \"diagnostics\" then\n match decode_diagnostics(text) of\n Err(e) => println(\"decode error: {codec_error_message(e)}\")\n Ok(d) => println(encode_diagnostics(d))\n elif arg(1) == \"resolved\" then\n match decode_resolved(text) of\n Err(e) => println(\"decode error: {codec_error_message(e)}\")\n Ok(d) => println(encode_resolved(d))\n elif arg(1) == \"resolved-nodes\" then\n match decode_resolved(text) of\n Err(e) => println(\"decode error: {codec_error_message(e)}\")\n Ok(d) => print_resolved_ids(d)\n else\n match decode_surface(text) of\n Err(e) => println(\"decode error: {codec_error_message(e)}\")\n Ok(d) => println(encode_surface(d))\n\n-- Every node id reached by walking each function body with the Prism-side\n-- `rnode_universe`, sorted ascending, one per line under a `count` header. A\n-- pure function of the decoded document: no source file or compiler state is\n-- consulted.\nfn print_resolved_ids(d : ResolvedDoc) =\n let ids = sort(\n concat_map(\n \\(f) -> map(\\(n) -> n.id, rnode_universe(f.body)),\n d.functions,\n ),\n )\n println(\"count {length(ids)}\")\n print_ids(ids)\n\nfn print_ids(ids : List(Int)) : Unit ! {IO} =\n match ids of\n Nil => ()\n Cons(i, rest) =>\n println(\"{i}\")\n print_ids(rest)\n" diff --git a/tests/fixtures/syntax/roundtrip.syntax-diagnostics.json b/tests/fixtures/syntax/roundtrip.syntax-diagnostics.json index ad4262f1..968a413a 100644 --- a/tests/fixtures/syntax/roundtrip.syntax-diagnostics.json +++ b/tests/fixtures/syntax/roundtrip.syntax-diagnostics.json @@ -1,6 +1,6 @@ { "schema": "prism-syntax-diagnostics-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "b2df556543bc484a6e7f6af2ba9ec0c0c2d72a7beb3d80df74821530dd3bc819", "text": "-- The round-trip gate for the versioned syntax artifacts: read one artifact\n-- file, decode it into the typed Syntax vocabulary, re-encode, and print the\n-- exact bytes. arg(0) is the artifact path; arg(1) selects the family\n-- (\"tokens\", \"surface\", \"diagnostics\", or \"resolved\"). The \"resolved-nodes\"\n-- mode instead decodes the resolved document and prints the id of every node\n-- reached by traversing each function body with `rnode_universe`, one per\n-- line: the Prism-side traversal the checker join is diffed against, reading\n-- only the artifact bytes. A refusal prints one structured error line.\nimport Data.Foldable (length)\nimport Data.List (concat_map)\nimport Syntax.Codec (..)\nimport Syntax.Resolved (..)\n\nfn main() =\n let text = read_file(arg(0))\n if arg(1) == \"tokens\" then\n match decode_tokens(text) of\n Err(e) => println(\"decode error: {codec_error_message(e)}\")\n Ok(d) => println(encode_tokens(d))\n elif arg(1) == \"diagnostics\" then\n match decode_diagnostics(text) of\n Err(e) => println(\"decode error: {codec_error_message(e)}\")\n Ok(d) => println(encode_diagnostics(d))\n elif arg(1) == \"resolved\" then\n match decode_resolved(text) of\n Err(e) => println(\"decode error: {codec_error_message(e)}\")\n Ok(d) => println(encode_resolved(d))\n elif arg(1) == \"resolved-nodes\" then\n match decode_resolved(text) of\n Err(e) => println(\"decode error: {codec_error_message(e)}\")\n Ok(d) => print_resolved_ids(d)\n else\n match decode_surface(text) of\n Err(e) => println(\"decode error: {codec_error_message(e)}\")\n Ok(d) => println(encode_surface(d))\n\n-- Every node id reached by walking each function body with the Prism-side\n-- `rnode_universe`, sorted ascending, one per line under a `count` header. A\n-- pure function of the decoded document: no source file or compiler state is\n-- consulted.\nfn print_resolved_ids(d : ResolvedDoc) =\n let ids = sort(\n concat_map(\n \\(f) -> map(\\(n) -> n.id, rnode_universe(f.body)),\n d.functions,\n ),\n )\n println(\"count {length(ids)}\")\n print_ids(ids)\n\nfn print_ids(ids : List(Int)) : Unit ! {IO} =\n match ids of\n Nil => ()\n Cons(i, rest) =>\n println(\"{i}\")\n print_ids(rest)\n" diff --git a/tests/fixtures/syntax/roundtrip.syntax-tokens.json b/tests/fixtures/syntax/roundtrip.syntax-tokens.json index 723dfa88..3cbb578a 100644 --- a/tests/fixtures/syntax/roundtrip.syntax-tokens.json +++ b/tests/fixtures/syntax/roundtrip.syntax-tokens.json @@ -1,6 +1,6 @@ { "schema": "prism-syntax-tokens-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "b2df556543bc484a6e7f6af2ba9ec0c0c2d72a7beb3d80df74821530dd3bc819", "text": "-- The round-trip gate for the versioned syntax artifacts: read one artifact\n-- file, decode it into the typed Syntax vocabulary, re-encode, and print the\n-- exact bytes. arg(0) is the artifact path; arg(1) selects the family\n-- (\"tokens\", \"surface\", \"diagnostics\", or \"resolved\"). The \"resolved-nodes\"\n-- mode instead decodes the resolved document and prints the id of every node\n-- reached by traversing each function body with `rnode_universe`, one per\n-- line: the Prism-side traversal the checker join is diffed against, reading\n-- only the artifact bytes. A refusal prints one structured error line.\nimport Data.Foldable (length)\nimport Data.List (concat_map)\nimport Syntax.Codec (..)\nimport Syntax.Resolved (..)\n\nfn main() =\n let text = read_file(arg(0))\n if arg(1) == \"tokens\" then\n match decode_tokens(text) of\n Err(e) => println(\"decode error: {codec_error_message(e)}\")\n Ok(d) => println(encode_tokens(d))\n elif arg(1) == \"diagnostics\" then\n match decode_diagnostics(text) of\n Err(e) => println(\"decode error: {codec_error_message(e)}\")\n Ok(d) => println(encode_diagnostics(d))\n elif arg(1) == \"resolved\" then\n match decode_resolved(text) of\n Err(e) => println(\"decode error: {codec_error_message(e)}\")\n Ok(d) => println(encode_resolved(d))\n elif arg(1) == \"resolved-nodes\" then\n match decode_resolved(text) of\n Err(e) => println(\"decode error: {codec_error_message(e)}\")\n Ok(d) => print_resolved_ids(d)\n else\n match decode_surface(text) of\n Err(e) => println(\"decode error: {codec_error_message(e)}\")\n Ok(d) => println(encode_surface(d))\n\n-- Every node id reached by walking each function body with the Prism-side\n-- `rnode_universe`, sorted ascending, one per line under a `count` header. A\n-- pure function of the decoded document: no source file or compiler state is\n-- consulted.\nfn print_resolved_ids(d : ResolvedDoc) =\n let ids = sort(\n concat_map(\n \\(f) -> map(\\(n) -> n.id, rnode_universe(f.body)),\n d.functions,\n ),\n )\n println(\"count {length(ids)}\")\n print_ids(ids)\n\nfn print_ids(ids : List(Int)) : Unit ! {IO} =\n match ids of\n Nil => ()\n Cons(i, rest) =>\n println(\"{i}\")\n print_ids(rest)\n" diff --git a/tests/fixtures/syntax/stable.resolved-syntax.json b/tests/fixtures/syntax/stable.resolved-syntax.json index 9558af43..374e06cc 100644 --- a/tests/fixtures/syntax/stable.resolved-syntax.json +++ b/tests/fixtures/syntax/stable.resolved-syntax.json @@ -1,6 +1,6 @@ { "schema": "prism-resolved-syntax-v1", - "compiler": "0.18.0", + "compiler": "0.20.0", "source": { "digest": "8f7f59322ffaa712c60fff1c424bd7c02c640d52fa9fe7652f33833222054821", "text": "-- A stable family: rungs, defaults, a migrations table with an override.\nimport Wire (..)\n\nstable Save {\n V1 = { hero: String, depth: Int },\n V2 = { ..V1, fog: Int = 30 },\n V3 = { ..V2, mist: Int = 5 },\n migrations {\n V1 -> V2 = auto\n V2 -> V3 = version(upgrade = \\(s) -> Save { hero = s.hero, depth = s.depth, fog = s.fog, mist = 7 }, downgrade = auto)\n V1 -> V3 = auto\n }\n}\n\nfn current() : Save = Save { hero = \"Ada\", depth = 12, fog = 30, mist = 99 }\n" @@ -61,7 +61,7 @@ } ], "body": { - "id": 4610, + "id": 4744, "kind": "record", "span": [ 145, @@ -69,7 +69,7 @@ ], "children": [ { - "id": 4611, + "id": 4745, "kind": "field", "span": [ 145, @@ -77,7 +77,7 @@ ], "children": [ { - "id": 4612, + "id": 4746, "kind": "var", "span": [ 145, @@ -87,7 +87,7 @@ ] }, { - "id": 4613, + "id": 4747, "kind": "field", "span": [ 145, @@ -95,7 +95,7 @@ ], "children": [ { - "id": 4614, + "id": 4748, "kind": "var", "span": [ 145, @@ -105,7 +105,7 @@ ] }, { - "id": 4615, + "id": 4749, "kind": "int", "span": [ 169, @@ -124,7 +124,7 @@ } ], "body": { - "id": 4616, + "id": 4750, "kind": "tuple", "span": [ 145, @@ -132,7 +132,7 @@ ], "children": [ { - "id": 4617, + "id": 4751, "kind": "record", "span": [ 145, @@ -140,7 +140,7 @@ ], "children": [ { - "id": 4618, + "id": 4752, "kind": "field", "span": [ 145, @@ -148,7 +148,7 @@ ], "children": [ { - "id": 4619, + "id": 4753, "kind": "var", "span": [ 145, @@ -158,7 +158,7 @@ ] }, { - "id": 4620, + "id": 4754, "kind": "field", "span": [ 145, @@ -166,7 +166,7 @@ ], "children": [ { - "id": 4621, + "id": 4755, "kind": "var", "span": [ 145, @@ -178,7 +178,7 @@ ] }, { - "id": 4622, + "id": 4756, "kind": "call", "span": [ 145, @@ -186,7 +186,7 @@ ], "children": [ { - "id": 4623, + "id": 4757, "kind": "var", "span": [ 145, @@ -194,7 +194,7 @@ ] }, { - "id": 4624, + "id": 4758, "kind": "list", "span": [ 145, @@ -202,7 +202,7 @@ ], "children": [ { - "id": 4625, + "id": 4759, "kind": "str", "span": [ 145, @@ -225,7 +225,7 @@ } ], "body": { - "id": 4626, + "id": 4760, "kind": "record", "span": [ 283, @@ -233,7 +233,7 @@ ], "children": [ { - "id": 4627, + "id": 4761, "kind": "field", "span": [ 297, @@ -241,7 +241,7 @@ ], "children": [ { - "id": 4628, + "id": 4762, "kind": "var", "span": [ 297, @@ -251,7 +251,7 @@ ] }, { - "id": 4629, + "id": 4763, "kind": "field", "span": [ 313, @@ -259,7 +259,7 @@ ], "children": [ { - "id": 4630, + "id": 4764, "kind": "var", "span": [ 313, @@ -269,7 +269,7 @@ ] }, { - "id": 4631, + "id": 4765, "kind": "field", "span": [ 328, @@ -277,7 +277,7 @@ ], "children": [ { - "id": 4632, + "id": 4766, "kind": "var", "span": [ 328, @@ -287,7 +287,7 @@ ] }, { - "id": 4633, + "id": 4767, "kind": "int", "span": [ 342, @@ -306,7 +306,7 @@ } ], "body": { - "id": 4634, + "id": 4768, "kind": "tuple", "span": [ 177, @@ -314,7 +314,7 @@ ], "children": [ { - "id": 4635, + "id": 4769, "kind": "record", "span": [ 177, @@ -322,7 +322,7 @@ ], "children": [ { - "id": 4636, + "id": 4770, "kind": "field", "span": [ 177, @@ -330,7 +330,7 @@ ], "children": [ { - "id": 4637, + "id": 4771, "kind": "var", "span": [ 177, @@ -340,7 +340,7 @@ ] }, { - "id": 4638, + "id": 4772, "kind": "field", "span": [ 177, @@ -348,7 +348,7 @@ ], "children": [ { - "id": 4639, + "id": 4773, "kind": "var", "span": [ 177, @@ -358,7 +358,7 @@ ] }, { - "id": 4640, + "id": 4774, "kind": "field", "span": [ 177, @@ -366,7 +366,7 @@ ], "children": [ { - "id": 4641, + "id": 4775, "kind": "var", "span": [ 177, @@ -378,7 +378,7 @@ ] }, { - "id": 4642, + "id": 4776, "kind": "call", "span": [ 177, @@ -386,7 +386,7 @@ ], "children": [ { - "id": 4643, + "id": 4777, "kind": "var", "span": [ 177, @@ -394,7 +394,7 @@ ] }, { - "id": 4644, + "id": 4778, "kind": "list", "span": [ 177, @@ -402,7 +402,7 @@ ], "children": [ { - "id": 4645, + "id": 4779, "kind": "str", "span": [ 177, @@ -425,7 +425,7 @@ } ], "body": { - "id": 4646, + "id": 4780, "kind": "call", "span": [ 108, @@ -433,7 +433,7 @@ ], "children": [ { - "id": 4647, + "id": 4781, "kind": "var", "span": [ 108, @@ -441,7 +441,7 @@ ] }, { - "id": 4648, + "id": 4782, "kind": "call", "span": [ 108, @@ -449,7 +449,7 @@ ], "children": [ { - "id": 4649, + "id": 4783, "kind": "var", "span": [ 108, @@ -457,7 +457,7 @@ ] }, { - "id": 4650, + "id": 4784, "kind": "var", "span": [ 108, @@ -478,7 +478,7 @@ } ], "body": { - "id": 4651, + "id": 4785, "kind": "call", "span": [ 108, @@ -486,7 +486,7 @@ ], "children": [ { - "id": 4652, + "id": 4786, "kind": "call", "span": [ 108, @@ -494,7 +494,7 @@ ], "children": [ { - "id": 4653, + "id": 4787, "kind": "var", "span": [ 108, @@ -502,7 +502,7 @@ ] }, { - "id": 4654, + "id": 4788, "kind": "var", "span": [ 108, @@ -510,7 +510,7 @@ ] }, { - "id": 4655, + "id": 4789, "kind": "var", "span": [ 108, @@ -520,7 +520,7 @@ ] }, { - "id": 4656, + "id": 4790, "kind": "var", "span": [ 108, @@ -539,7 +539,7 @@ } ], "body": { - "id": 4657, + "id": 4791, "kind": "call", "span": [ 145, @@ -547,7 +547,7 @@ ], "children": [ { - "id": 4658, + "id": 4792, "kind": "var", "span": [ 145, @@ -555,7 +555,7 @@ ] }, { - "id": 4659, + "id": 4793, "kind": "var", "span": [ 145, @@ -574,7 +574,7 @@ } ], "body": { - "id": 4660, + "id": 4794, "kind": "call", "span": [ 145, @@ -582,7 +582,7 @@ ], "children": [ { - "id": 4661, + "id": 4795, "kind": "var", "span": [ 145, @@ -590,7 +590,7 @@ ] }, { - "id": 4662, + "id": 4796, "kind": "var", "span": [ 145, @@ -609,7 +609,7 @@ } ], "body": { - "id": 4663, + "id": 4797, "kind": "call", "span": [ 92, @@ -617,7 +617,7 @@ ], "children": [ { - "id": 4664, + "id": 4798, "kind": "var", "span": [ 92, @@ -625,7 +625,7 @@ ] }, { - "id": 4665, + "id": 4799, "kind": "str", "span": [ 92, @@ -633,7 +633,7 @@ ] }, { - "id": 4666, + "id": 4800, "kind": "var", "span": [ 92, @@ -652,7 +652,7 @@ } ], "body": { - "id": 4667, + "id": 4801, "kind": "call", "span": [ 92, @@ -660,7 +660,7 @@ ], "children": [ { - "id": 4668, + "id": 4802, "kind": "var", "span": [ 92, @@ -668,7 +668,7 @@ ] }, { - "id": 4669, + "id": 4803, "kind": "var", "span": [ 92, @@ -676,7 +676,7 @@ ] }, { - "id": 4670, + "id": 4804, "kind": "str", "span": [ 92, @@ -695,7 +695,7 @@ } ], "body": { - "id": 4671, + "id": 4805, "kind": "match", "span": [ 92, @@ -703,7 +703,7 @@ ], "children": [ { - "id": 4672, + "id": 4806, "kind": "call", "span": [ 92, @@ -711,7 +711,7 @@ ], "children": [ { - "id": 4673, + "id": 4807, "kind": "var", "span": [ 92, @@ -719,7 +719,7 @@ ] }, { - "id": 4674, + "id": 4808, "kind": "var", "span": [ 92, @@ -729,7 +729,7 @@ ] }, { - "id": 4678, + "id": 4812, "kind": "if", "span": [ 92, @@ -737,7 +737,7 @@ ], "children": [ { - "id": 4679, + "id": 4813, "kind": "bin", "span": [ 92, @@ -745,7 +745,7 @@ ], "children": [ { - "id": 4680, + "id": 4814, "kind": "var", "span": [ 92, @@ -753,7 +753,7 @@ ] }, { - "id": 4681, + "id": 4815, "kind": "str", "span": [ 92, @@ -763,7 +763,7 @@ ] }, { - "id": 4682, + "id": 4816, "kind": "match", "span": [ 92, @@ -771,7 +771,7 @@ ], "children": [ { - "id": 4683, + "id": 4817, "kind": "call", "span": [ 92, @@ -779,7 +779,7 @@ ], "children": [ { - "id": 4684, + "id": 4818, "kind": "var", "span": [ 92, @@ -787,7 +787,7 @@ ] }, { - "id": 4685, + "id": 4819, "kind": "var", "span": [ 92, @@ -797,7 +797,7 @@ ] }, { - "id": 4689, + "id": 4823, "kind": "if", "span": [ 92, @@ -805,7 +805,7 @@ ], "children": [ { - "id": 4690, + "id": 4824, "kind": "call", "span": [ 92, @@ -813,7 +813,7 @@ ], "children": [ { - "id": 4691, + "id": 4825, "kind": "var", "span": [ 92, @@ -821,7 +821,7 @@ ] }, { - "id": 4692, + "id": 4826, "kind": "var", "span": [ 92, @@ -831,7 +831,7 @@ ] }, { - "id": 4693, + "id": 4827, "kind": "call", "span": [ 92, @@ -839,7 +839,7 @@ ], "children": [ { - "id": 4694, + "id": 4828, "kind": "var", "span": [ 92, @@ -847,7 +847,7 @@ ] }, { - "id": 4695, + "id": 4829, "kind": "call", "span": [ 92, @@ -855,7 +855,7 @@ ], "children": [ { - "id": 4696, + "id": 4830, "kind": "var", "span": [ 92, @@ -863,7 +863,7 @@ ] }, { - "id": 4697, + "id": 4831, "kind": "var", "span": [ 92, @@ -875,7 +875,7 @@ ] }, { - "id": 4698, + "id": 4832, "kind": "call", "span": [ 92, @@ -883,7 +883,7 @@ ], "children": [ { - "id": 4699, + "id": 4833, "kind": "var", "span": [ 92, @@ -897,7 +897,7 @@ ] }, { - "id": 4700, + "id": 4834, "kind": "if", "span": [ 92, @@ -905,7 +905,7 @@ ], "children": [ { - "id": 4701, + "id": 4835, "kind": "bin", "span": [ 92, @@ -913,7 +913,7 @@ ], "children": [ { - "id": 4702, + "id": 4836, "kind": "var", "span": [ 92, @@ -921,7 +921,7 @@ ] }, { - "id": 4703, + "id": 4837, "kind": "str", "span": [ 92, @@ -931,7 +931,7 @@ ] }, { - "id": 4704, + "id": 4838, "kind": "match", "span": [ 92, @@ -939,7 +939,7 @@ ], "children": [ { - "id": 4705, + "id": 4839, "kind": "call", "span": [ 92, @@ -947,7 +947,7 @@ ], "children": [ { - "id": 4706, + "id": 4840, "kind": "var", "span": [ 92, @@ -955,7 +955,7 @@ ] }, { - "id": 4707, + "id": 4841, "kind": "var", "span": [ 92, @@ -965,7 +965,7 @@ ] }, { - "id": 4711, + "id": 4845, "kind": "if", "span": [ 92, @@ -973,7 +973,7 @@ ], "children": [ { - "id": 4712, + "id": 4846, "kind": "call", "span": [ 92, @@ -981,7 +981,7 @@ ], "children": [ { - "id": 4713, + "id": 4847, "kind": "var", "span": [ 92, @@ -989,7 +989,7 @@ ] }, { - "id": 4714, + "id": 4848, "kind": "var", "span": [ 92, @@ -999,7 +999,7 @@ ] }, { - "id": 4715, + "id": 4849, "kind": "call", "span": [ 92, @@ -1007,7 +1007,7 @@ ], "children": [ { - "id": 4716, + "id": 4850, "kind": "var", "span": [ 92, @@ -1015,7 +1015,7 @@ ] }, { - "id": 4717, + "id": 4851, "kind": "var", "span": [ 92, @@ -1025,7 +1025,7 @@ ] }, { - "id": 4718, + "id": 4852, "kind": "call", "span": [ 92, @@ -1033,7 +1033,7 @@ ], "children": [ { - "id": 4719, + "id": 4853, "kind": "var", "span": [ 92, @@ -1047,7 +1047,7 @@ ] }, { - "id": 4720, + "id": 4854, "kind": "if", "span": [ 92, @@ -1055,7 +1055,7 @@ ], "children": [ { - "id": 4721, + "id": 4855, "kind": "bin", "span": [ 92, @@ -1063,7 +1063,7 @@ ], "children": [ { - "id": 4722, + "id": 4856, "kind": "var", "span": [ 92, @@ -1071,7 +1071,7 @@ ] }, { - "id": 4723, + "id": 4857, "kind": "str", "span": [ 92, @@ -1081,7 +1081,7 @@ ] }, { - "id": 4724, + "id": 4858, "kind": "match", "span": [ 92, @@ -1089,7 +1089,7 @@ ], "children": [ { - "id": 4725, + "id": 4859, "kind": "call", "span": [ 92, @@ -1097,7 +1097,7 @@ ], "children": [ { - "id": 4726, + "id": 4860, "kind": "var", "span": [ 92, @@ -1105,7 +1105,7 @@ ] }, { - "id": 4727, + "id": 4861, "kind": "var", "span": [ 92, @@ -1115,7 +1115,7 @@ ] }, { - "id": 4731, + "id": 4865, "kind": "if", "span": [ 92, @@ -1123,7 +1123,7 @@ ], "children": [ { - "id": 4732, + "id": 4866, "kind": "call", "span": [ 92, @@ -1131,7 +1131,7 @@ ], "children": [ { - "id": 4733, + "id": 4867, "kind": "var", "span": [ 92, @@ -1139,7 +1139,7 @@ ] }, { - "id": 4734, + "id": 4868, "kind": "var", "span": [ 92, @@ -1149,7 +1149,7 @@ ] }, { - "id": 4735, + "id": 4869, "kind": "var", "span": [ 92, @@ -1157,7 +1157,7 @@ ] }, { - "id": 4736, + "id": 4870, "kind": "call", "span": [ 92, @@ -1165,7 +1165,7 @@ ], "children": [ { - "id": 4737, + "id": 4871, "kind": "var", "span": [ 92, @@ -1179,7 +1179,7 @@ ] }, { - "id": 4738, + "id": 4872, "kind": "call", "span": [ 92, @@ -1187,7 +1187,7 @@ ], "children": [ { - "id": 4739, + "id": 4873, "kind": "var", "span": [ 92, diff --git a/tests/fixtures/syntax/stable.surface-syntax.json b/tests/fixtures/syntax/stable.surface-syntax.json index 272adfed..12472c66 100644 --- a/tests/fixtures/syntax/stable.surface-syntax.json +++ b/tests/fixtures/syntax/stable.surface-syntax.json @@ -1,6 +1,6 @@ { "schema": "prism-surface-syntax-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "8f7f59322ffaa712c60fff1c424bd7c02c640d52fa9fe7652f33833222054821", "text": "-- A stable family: rungs, defaults, a migrations table with an override.\nimport Wire (..)\n\nstable Save {\n V1 = { hero: String, depth: Int },\n V2 = { ..V1, fog: Int = 30 },\n V3 = { ..V2, mist: Int = 5 },\n migrations {\n V1 -> V2 = auto\n V2 -> V3 = version(upgrade = \\(s) -> Save { hero = s.hero, depth = s.depth, fog = s.fog, mist = 7 }, downgrade = auto)\n V1 -> V3 = auto\n }\n}\n\nfn current() : Save = Save { hero = \"Ada\", depth = 12, fog = 30, mist = 99 }\n" diff --git a/tests/fixtures/syntax/stable.syntax-diagnostics.json b/tests/fixtures/syntax/stable.syntax-diagnostics.json index 2d6ec78e..771c5727 100644 --- a/tests/fixtures/syntax/stable.syntax-diagnostics.json +++ b/tests/fixtures/syntax/stable.syntax-diagnostics.json @@ -1,6 +1,6 @@ { "schema": "prism-syntax-diagnostics-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "8f7f59322ffaa712c60fff1c424bd7c02c640d52fa9fe7652f33833222054821", "text": "-- A stable family: rungs, defaults, a migrations table with an override.\nimport Wire (..)\n\nstable Save {\n V1 = { hero: String, depth: Int },\n V2 = { ..V1, fog: Int = 30 },\n V3 = { ..V2, mist: Int = 5 },\n migrations {\n V1 -> V2 = auto\n V2 -> V3 = version(upgrade = \\(s) -> Save { hero = s.hero, depth = s.depth, fog = s.fog, mist = 7 }, downgrade = auto)\n V1 -> V3 = auto\n }\n}\n\nfn current() : Save = Save { hero = \"Ada\", depth = 12, fog = 30, mist = 99 }\n" diff --git a/tests/fixtures/syntax/stable.syntax-tokens.json b/tests/fixtures/syntax/stable.syntax-tokens.json index 58d6e4bd..2348d312 100644 --- a/tests/fixtures/syntax/stable.syntax-tokens.json +++ b/tests/fixtures/syntax/stable.syntax-tokens.json @@ -1,6 +1,6 @@ { "schema": "prism-syntax-tokens-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "8f7f59322ffaa712c60fff1c424bd7c02c640d52fa9fe7652f33833222054821", "text": "-- A stable family: rungs, defaults, a migrations table with an override.\nimport Wire (..)\n\nstable Save {\n V1 = { hero: String, depth: Int },\n V2 = { ..V1, fog: Int = 30 },\n V3 = { ..V2, mist: Int = 5 },\n migrations {\n V1 -> V2 = auto\n V2 -> V3 = version(upgrade = \\(s) -> Save { hero = s.hero, depth = s.depth, fog = s.fog, mist = 7 }, downgrade = auto)\n V1 -> V3 = auto\n }\n}\n\nfn current() : Save = Save { hero = \"Ada\", depth = 12, fog = 30, mist = 99 }\n" diff --git a/tests/fixtures/syntax/types.surface-syntax.json b/tests/fixtures/syntax/types.surface-syntax.json index 5c8bcbb4..47aee7a9 100644 --- a/tests/fixtures/syntax/types.surface-syntax.json +++ b/tests/fixtures/syntax/types.surface-syntax.json @@ -1,6 +1,6 @@ { "schema": "prism-surface-syntax-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "2544973d2dc5f86eed0354cfb43ba57782333229b70adef424b180a08f35198c", "text": "-- Type syntax: rows, kinds, dimensions, unboxed shapes, open effect tails.\neffect Tick\n tick() : Int\n\ntype Cmd(a, e : Row) = Cmd(() -> a ! {e})\n\ntype Grid(n) = Grid(Vec(Int, n))\n\nfn poly(x : a, f : f(a)) : a = x\n\nfn generic(xs : List(Int), p : (Int, Float)) : Option(Int) = head(xs)\n\nfn effectful() : Int ! {Tick} = tick()\n\nfn forwarding(act : () -> Int ! {e}) : Int ! {Tick | e} = act()\n\nfn boxed_unboxed(t : #(Int, Float), r : #{ w : Int, h : Int }) : Int =\n let s = #{ w = r.#w, h = r.#h }\n let u = #(1, 2.5)\n s.#w + s.#h\n\nfn annotated() : Int =\n let f = (\\(v) -> v : forall a. (a) -> a)\n f((3 : Int))\n" diff --git a/tests/fixtures/syntax/types.syntax-diagnostics.json b/tests/fixtures/syntax/types.syntax-diagnostics.json index d283c974..b8146507 100644 --- a/tests/fixtures/syntax/types.syntax-diagnostics.json +++ b/tests/fixtures/syntax/types.syntax-diagnostics.json @@ -1,6 +1,6 @@ { "schema": "prism-syntax-diagnostics-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "2544973d2dc5f86eed0354cfb43ba57782333229b70adef424b180a08f35198c", "text": "-- Type syntax: rows, kinds, dimensions, unboxed shapes, open effect tails.\neffect Tick\n tick() : Int\n\ntype Cmd(a, e : Row) = Cmd(() -> a ! {e})\n\ntype Grid(n) = Grid(Vec(Int, n))\n\nfn poly(x : a, f : f(a)) : a = x\n\nfn generic(xs : List(Int), p : (Int, Float)) : Option(Int) = head(xs)\n\nfn effectful() : Int ! {Tick} = tick()\n\nfn forwarding(act : () -> Int ! {e}) : Int ! {Tick | e} = act()\n\nfn boxed_unboxed(t : #(Int, Float), r : #{ w : Int, h : Int }) : Int =\n let s = #{ w = r.#w, h = r.#h }\n let u = #(1, 2.5)\n s.#w + s.#h\n\nfn annotated() : Int =\n let f = (\\(v) -> v : forall a. (a) -> a)\n f((3 : Int))\n" diff --git a/tests/fixtures/syntax/types.syntax-tokens.json b/tests/fixtures/syntax/types.syntax-tokens.json index f1284358..e0a0ed00 100644 --- a/tests/fixtures/syntax/types.syntax-tokens.json +++ b/tests/fixtures/syntax/types.syntax-tokens.json @@ -1,6 +1,6 @@ { "schema": "prism-syntax-tokens-v1", - "compiler": "0.19.0", + "compiler": "0.20.0", "source": { "digest": "2544973d2dc5f86eed0354cfb43ba57782333229b70adef424b180a08f35198c", "text": "-- Type syntax: rows, kinds, dimensions, unboxed shapes, open effect tails.\neffect Tick\n tick() : Int\n\ntype Cmd(a, e : Row) = Cmd(() -> a ! {e})\n\ntype Grid(n) = Grid(Vec(Int, n))\n\nfn poly(x : a, f : f(a)) : a = x\n\nfn generic(xs : List(Int), p : (Int, Float)) : Option(Int) = head(xs)\n\nfn effectful() : Int ! {Tick} = tick()\n\nfn forwarding(act : () -> Int ! {e}) : Int ! {Tick | e} = act()\n\nfn boxed_unboxed(t : #(Int, Float), r : #{ w : Int, h : Int }) : Int =\n let s = #{ w = r.#w, h = r.#h }\n let u = #(1, 2.5)\n s.#w + s.#h\n\nfn annotated() : Int =\n let f = (\\(v) -> v : forall a. (a) -> a)\n f((3 : Int))\n" diff --git a/tests/fixtures/tier_cross/convention_split_map.pr b/tests/fixtures/tier_cross/convention_split_map.pr new file mode 100644 index 00000000..4c339eed --- /dev/null +++ b/tests/fixtures/tier_cross/convention_split_map.pr @@ -0,0 +1,25 @@ +-- One row-polymorphic traversal is called at both a direct and an effectful +-- thunk convention. The direct call must not inherit the handled call's +-- runtime representation. + +effect Log + emit(Int) : Unit + +fn bump(x : Int) : Int = x + 1 + +fn shout(x : Int) : Int ! {Log} = + let _u = emit(x) + x * 2 + +fn pure_use(xs : List(Int)) : List(Int) = map(bump, xs) + +fn effect_use(xs : List(Int)) : List(Int) ! {Log} = map(shout, xs) + +fn main() = + let direct = pure_use([1, 2, 3]) + let handled = + handle effect_use([4, 5]) with + emit(_n) resume k => k(()) + return r => r + print(show(direct)) + print(show(handled)) diff --git a/tests/fixtures/tier_cross/convention_split_map_pure.pr b/tests/fixtures/tier_cross/convention_split_map_pure.pr new file mode 100644 index 00000000..4ea7325c --- /dev/null +++ b/tests/fixtures/tier_cross/convention_split_map_pure.pr @@ -0,0 +1,8 @@ +-- Pure control for the convention-split fixture: the shared traversal has only +-- its direct callback convention, so no effect lowering is needed. + +fn bump(x : Int) : Int = x + 1 + +fn pure_use(xs : List(Int)) : List(Int) = map(bump, xs) + +fn main() = print(show(pure_use([1, 2, 3]))) diff --git a/tests/fixtures/tier_cross/convention_split_map_unrolled.pr b/tests/fixtures/tier_cross/convention_split_map_unrolled.pr new file mode 100644 index 00000000..cfe4c86e --- /dev/null +++ b/tests/fixtures/tier_cross/convention_split_map_unrolled.pr @@ -0,0 +1,27 @@ +-- Hand-unrolled control for the convention-split fixture: only the effectful +-- traversal changes shape, while the hot pure use still calls the shared map. + +effect Log + emit(Int) : Unit + +fn bump(x : Int) : Int = x + 1 + +fn shout(x : Int) : Int ! {Log} = + let _u = emit(x) + x * 2 + +fn pure_use(xs : List(Int)) : List(Int) = map(bump, xs) + +fn effect_use(xs : List(Int)) : List(Int) ! {Log} = + match xs of + Nil => Nil + Cons(x, rest) => Cons(shout(x), effect_use(rest)) + +fn main() = + let direct = pure_use([1, 2, 3]) + let handled = + handle effect_use([4, 5]) with + emit(_n) resume k => k(()) + return r => r + print(show(direct)) + print(show(handled)) diff --git a/tests/formatter/fmt_path_lit.rs b/tests/formatter/fmt_path_lit.rs index e4922a53..3718f855 100644 --- a/tests/formatter/fmt_path_lit.rs +++ b/tests/formatter/fmt_path_lit.rs @@ -32,8 +32,8 @@ fn a_literal_survives_an_argument_position() { assert_format(src, src); } -// Extra spacing around the sigil and the steps is the author's, not the -// language's, and normalizing it is the whole reason the form has one spelling. +// The form has one spelling, so formatting removes extra space around the sigil +// and steps. #[test] fn spacing_is_normalized() { assert_format("fn f() =\n let a = #path pos . x\n a\n", MANY_STEPS); diff --git a/tests/formatter/fmt_using.rs b/tests/formatter/fmt_using.rs index 32037408..bd7b23c5 100644 --- a/tests/formatter/fmt_using.rs +++ b/tests/formatter/fmt_using.rs @@ -2,8 +2,8 @@ // every layout path. The flat/break printer and the inline printer decode a // call head through one shared classifier; when they drifted, a call wide enough // to break re-emitted `f(a, using I)` as `f(using I)(a)` -- a fixpoint, so it -// slipped past plain idempotence. These cases check AST-level round-trip (meaning -// preserved), not just `format(format(x)) == format(x)`. +// slipped past plain idempotence. These cases check the AST-level round trip as +// well as `format(format(x)) == format(x)`. // The parse AST with span offsets stripped, so it is invariant under the // whitespace reflow a reformat performs. Reflow shifts only spans; a structural diff --git a/tests/frontend/env_knobs.rs b/tests/frontend/env_knobs.rs index 7d564833..84650997 100644 --- a/tests/frontend/env_knobs.rs +++ b/tests/frontend/env_knobs.rs @@ -48,7 +48,9 @@ const ALLOWED: &[(&str, &[&str])] = &[ ("PRISM_CHECK_LEAKS", &["runtime/prism_mem.c"]), ("PRISM_ALLOC_STATS", &["runtime/prism_mem.c"]), ("PRISM_REUSE_STATS", &["runtime/prism_mem.c"]), + ("PRISM_RC_STATS", &["runtime/prism_mem.c"]), ("PRISM_EFFOP_STATS", &["runtime/prism_mem.c"]), + ("PRISM_PROMOTE_STATS", &["runtime/prism_mem.c"]), ("PRISM_DRIVE_STATS", &["runtime/prism_mem.c"]), // Family 3a: the C-toolchain seam, centralized in one module. ( @@ -68,13 +70,25 @@ const ALLOWED: &[(&str, &[&str])] = &[ ), ("PRISM_Z3", &["src/verify/tests.rs"]), ("PRISM_CVC5", &["src/verify/tests.rs"]), + ("PRISM_REQUIRE_SOLVERS", &["src/verify/tests.rs"]), ]; -// An environment *read* is one of these call forms with a string-literal argument. +// An environment *read* is one of these forms with a string-literal argument. +// The first three are the call forms. The fourth is a knob named once by a +// constant instead of at each call site, so a module reading one knob from +// several places still spells it once: a `*_ENV` constant holding a `PRISM_*` +// literal counts as reading that knob in the file that declares it, which is +// exactly the claim this audit makes (one documented home per knob). +// // A bare `"PRISM_..."` in a diagnostic message or a doc comment is not a read and // is ignored; a read via a dynamic (non-literal) name is out of scope by design // (the `getenv` builtin reads an arbitrary program-supplied name). -const READ_MARKERS: &[&str] = &["env::var(\"", "env::var_os(\"", "getenv(\""]; +const READ_MARKERS: &[&str] = &[ + "env::var(\"", + "env::var_os(\"", + "getenv(\"", + "_ENV: &str = \"", +]; /// Every `(var, relative-file)` env read of a `PRISM_*` knob found by scanning the /// literal call sites under `src/` (`.rs`) and `runtime/` (`.c`/`.h`). diff --git a/tests/frontend/type_query.rs b/tests/frontend/type_query.rs index b67f75e2..289ff90b 100644 --- a/tests/frontend/type_query.rs +++ b/tests/frontend/type_query.rs @@ -1,13 +1,15 @@ //! Type search and bounded, verified hole synthesis through the real CLI. use std::fs; +use std::path::Path; use std::process::{Command, Output}; use crate::support::TempDir; use serde_json::Value; -fn run(args: &[&str]) -> Output { +fn run(store: &Path, args: &[&str]) -> Output { Command::new(env!("CARGO_BIN_EXE_prism")) + .env("PRISM_STORE_PATH", store) .args(args) .output() .expect("runs prism") @@ -70,7 +72,7 @@ entry = "src/dep_main.pr" .unwrap(); let manifest = app.join("prism.toml"); - let rows = json(&run(&[ + let args = [ "search", "(Int) -> Int", "--in", @@ -78,19 +80,28 @@ entry = "src/dep_main.pr" "--limit", "500", "--json", - ])); - let rows = rows.as_array().unwrap(); - assert!( - rows.iter() - .any(|row| row["name"] == "Own.own" && row["source"] == "project"), - "{rows:?}" - ); - assert!( - rows.iter() - .any(|row| row["name"] == "Package.package" && row["source"] == "package"), - "{rows:?}" - ); - assert!(rows.iter().any(|row| row["source"] == "stdlib"), "{rows:?}"); + ]; + // Twice against one store: the second run serves module facts from the + // durable cache, where an interface-only hit once dropped unimported + // project modules from the results. + for pass in ["cold", "warm"] { + let rows = json(&run(&dir.store_root(), &args)); + let rows = rows.as_array().unwrap(); + assert!( + rows.iter() + .any(|row| row["name"] == "Own.own" && row["source"] == "project"), + "{pass}: {rows:?}" + ); + assert!( + rows.iter() + .any(|row| row["name"] == "Package.package" && row["source"] == "package"), + "{pass}: {rows:?}" + ); + assert!( + rows.iter().any(|row| row["source"] == "stdlib"), + "{pass}: {rows:?}" + ); + } } #[test] @@ -114,8 +125,8 @@ fn synth_is_depth_bounded_deterministic_and_rechecked() { "10", "--json", ]; - let first = json(&run(&args)); - let second = json(&run(&args)); + let first = json(&run(&dir.store_root(), &args)); + let second = json(&run(&dir.store_root(), &args)); assert_eq!(first, second); let candidates = first[0]["candidates"].as_array().unwrap(); assert!( @@ -125,14 +136,17 @@ fn synth_is_depth_bounded_deterministic_and_rechecked() { "{first:?}" ); - let shallow = json(&run(&[ - "synth", - file.to_str().unwrap(), - "--at-hole", - "answer", - "--depth", - "0", - "--json", - ])); + let shallow = json(&run( + &dir.store_root(), + &[ + "synth", + file.to_str().unwrap(), + "--at-hole", + "answer", + "--depth", + "0", + "--json", + ], + )); assert!(shallow[0]["candidates"].as_array().unwrap().is_empty()); } diff --git a/tests/lane_ledger.txt b/tests/lane_ledger.txt new file mode 100644 index 00000000..2ecfaccf --- /dev/null +++ b/tests/lane_ledger.txt @@ -0,0 +1,90 @@ +# The gauntlet's cost ledger: one row per continuous-integration job, declaring +# when it runs, how many cells it fans out to, the wall clock of its slowest +# cell, and whether that fits the per-change budget. +# +# Columns, tab separated: +# workflow job arm cells slowest_cell_seconds verdict +# +# `arm` is derived from the workflow's triggers, never asserted independently: +# `per-change` runs on every pull request, `path-gated` runs on a pull request +# only when named paths move, `post-merge` runs after a change lands, +# `nightly` runs on a schedule, and `release` runs from a tag. +# +# `verdict` is `within` or `over-budget` for a per-change row and `unbudgeted` +# for every other arm, since the budget bounds the latency a change pays. +# `planned` names a lane that is declared before it is built: its workflow is +# `-`, and no job by that name may exist yet. +# +# The budget is 2700 seconds of wall clock per cell. A change's latency is its +# slowest cell, not the sum, so the cap is per cell. A row over the cap is +# recorded over the cap rather than the cap being raised to cover it, and the +# repair is named beside the row below. +# +# Timings are observed, not modeled, and each names the run it came from, so a +# figure can be audited rather than believed. They are the last green run of +# each workflow, read from the forge: +# +# ci.yml run 31940291956 2026-08-16 68a299af +# identity.yml run 31940291812 2026-08-16 68a299af +# pages.yml run 31940291864 2026-08-16 68a299af +# release.yml run 31934782654 2026-08-16 9971e978 +# install.yml run 32004949257 2026-08-17 68a299af +# +# They are refreshed when the suite is reshaped, and a stale figure is a wrong +# figure: the offline gate checks the ledger against the live workflows and +# against its own budget, but it cannot see that a number aged. Refreshing them +# is a reviewed edit, deliberately, since a number nobody looked at is the one +# way this file can lie while passing. +# +# Declared before they are built, so that the arm is chosen while the choice is +# still cheap rather than after the lane already runs somewhere: +# +# shadow-parser-receipt - the source-written parser run against the corpus, +# its own sources, generated programs, fuzzed inputs, and hostile inputs, with +# one content-addressed comparison receipt. Nightly: it is a whole-corpus +# double compile, which is the shape that does not fit a per-change cap. +# Its receipt takes the built parser artifact's identity as a key input, not +# only the identity of the sources that built it: the same question asked of a +# different parser is a different question, and a key that cannot tell them +# apart answers the second with the first one's verdict. +# +# stage-reproduction - the two further runs that reproduce syntax and Core +# identity from each stage. Nightly, for the same reason and against the same +# key. +# +# Recorded over the cap: +# +# ci.yml test - 3040s. The four shards are partitioned by test-name hash and +# run 1380s, 1755s, 1485s, and 3040s, so one cell carries twice its share and +# alone sets the latency of every change. The repair is a balanced partition, +# not a move to the nightly arm: the whole-workspace suite is what a change is +# gated on, and a gauntlet that runs tomorrow does not gate today. + +ci.yml checks per-change 1 1010 within +ci.yml corpus-oracles per-change 16 2451 within +ci.yml doc-links per-change 1 6 within +ci.yml feature-matrix per-change 4 111 within +ci.yml lean per-change 1 2557 within +ci.yml lint per-change 1 62 within +ci.yml mimalloc per-change 1 275 within +ci.yml mlir per-change 1 1242 within +ci.yml nix per-change 1 45 within +ci.yml parity per-change 4 294 within +ci.yml prismup per-change 1 64 within +ci.yml readme-version per-change 1 5 within +ci.yml sanitizers per-change 1 2387 within +ci.yml scoreboard per-change 1 6 within +ci.yml test per-change 4 3040 over-budget +ci.yml test-complete per-change 1 4 within +ci.yml web per-change 1 171 within +identity.yml compare post-merge 1 3 unbudgeted +identity.yml manifest post-merge 3 339 unbudgeted +install.yml install path-gated 3 43 unbudgeted +pages.yml build post-merge 1 336 unbudgeted +pages.yml deploy post-merge 1 13 unbudgeted +release.yml build release 3 299 unbudgeted +release.yml docker release 1 254 unbudgeted +release.yml gate release 1 8 unbudgeted +release.yml publish release 1 30 unbudgeted +- shadow-parser-receipt nightly 0 0 planned +- stage-reproduction nightly 0 0 planned diff --git a/tests/language/hash_parity.rs b/tests/language/hash_parity.rs index f24763c8..323c41ef 100644 --- a/tests/language/hash_parity.rs +++ b/tests/language/hash_parity.rs @@ -1,38 +1,13 @@ -// The content-hash parity gate: the central invariant behind -// content-addressed Core. A definition's hash names its pre-optimizer elaborated -// term, and everything past that term (the deterministic Core-to-Core optimizer -// and codegen) is a pure function of it under a fixed toolchain. So, holding the -// compiler build and optimizer configuration fixed (the verification fingerprint, -// which is exactly what carries optimizer/flag drift), *equal hash implies a -// byte-identical compiled artifact*, and any change visible to the elaborated -// term or to codegen must move the hash. That is the content-addressed analogue -// of the interpreter/native parity oracle: there the claim is "same Core, same -// output on every backend"; here it is "same hash, same emitted artifact," with -// its dual "different artifact, different hash." Identity is deliberately -// optimizer-independent; the optimizer level rides in the fingerprint, not the -// hash, so this gate fixes it while it runs. +// Content-hash parity under a fixed compiler and optimizer configuration. Equal +// pre-optimizer Core hashes must produce byte-identical compiled artifacts, and +// a codegen-visible edit must move the hash. // -// The artifact compared is the emitted LLVM IR text (`emit_ir`), which is finer -// than stdout: it reflects codegen choices (rc insertion, lowering) that never -// reach the terminal, so it catches a hash that agrees on behavior but disagrees -// on compiled form. Without this coupling the hash is only asserted-correct -// against itself (`core_hash.rs`); here it is checked against the thing it claims -// to name. +// Compare emitted LLVM IR so codegen changes invisible in stdout remain covered. // -// Three properties. Soundness over real programs: emission is deterministic and -// reformatting (a semantics- and name-preserving reprint) moves neither the -// artifact nor the hash. Soundness with teeth: two programs differing only in a -// local binder name hash identically, so their compiled artifact must be -// byte-identical too. Completeness with teeth: a codegen-visible edit must move -// the hash. The metadata inputs fip/borrow are committed at the hash level in -// `core_hash.rs`; on the current backend they are conservatively folded even -// where they do not move the IR, so they are not re-tested here. +// The cases cover deterministic emission, reformatting, local binder renames, +// and codegen-visible edits. `core_hash.rs` covers fip/borrow metadata. // -// Gated on `feature = "native"` because `emit_ir` is; no C compiler is needed -// (the textual emitter produces IR without invoking clang), so this runs wherever -// the native backend is compiled in. A small fixed set of committed examples -// gives breadth without the per-program interpreter filtering the runnable-corpus -// oracle pays; the curated tables carry the teeth and are cheap. +// `emit_ir` requires the `native` feature but does not invoke a C compiler. #![cfg(feature = "native")] use std::collections::BTreeMap; @@ -43,8 +18,7 @@ use prism::{emit_ir, format, with_prelude}; // A spread of committed examples: arithmetic recursion, higher-order functions, // dictionary-passing type classes, algebraic-effect handlers, list -// comprehensions. Enough shape variety that reformatting exercises real codegen, -// not just a toy. +// comprehensions. This gives reformatting enough variety to exercise real codegen. const EXAMPLES: &[&str] = &["collatz", "curry", "classes", "eff_state", "comprehension"]; // The emitted LLVM IR for a full (prelude-included) program: the compiled diff --git a/tests/language/let_else.rs b/tests/language/let_else.rs index 5b8dbbdf..a99d5b1c 100644 --- a/tests/language/let_else.rs +++ b/tests/language/let_else.rs @@ -11,7 +11,7 @@ // the unreachable arm the expansion actually contains rather than silently // accepted. `?` and `else` are two different answers to a failed step, and // combining them in one binding is refused by the existing whole-statement rule -// for `?` instead of quietly picking one. +// for `?`, avoiding an arbitrary choice. use prism::eval::Rv; use prism::{check, dump, interpret, with_prelude, Error}; diff --git a/tests/language/modules.rs b/tests/language/modules.rs index db10657b..c5d456a2 100644 --- a/tests/language/modules.rs +++ b/tests/language/modules.rs @@ -1,5 +1,5 @@ //! Multi-module resolution: qualified, selective, and aliased imports; -//! private-name namespacing; canonical disjoint namespaces; and the scoping +//! private-name namespacing, canonical disjoint namespaces, and the scoping //! rules that let modules share a short name. use std::path::Path; diff --git a/tests/language/num_tower.rs b/tests/language/num_tower.rs index 562dd937..57cf300c 100644 --- a/tests/language/num_tower.rs +++ b/tests/language/num_tower.rs @@ -191,8 +191,8 @@ impl NonNumericCase { } // A non-numeric operand is rejected as a missing instance on the operand's own -// type, a clean lane story rather than an unresolved-dictionary dump. Crucially -// the message names the lane (`Num(String)`), never a raw `_D`-mangled cell. +// type, a clean lane story rather than an unresolved-dictionary dump. The message +// must name the lane (`Num(String)`), never a raw `_D`-mangled cell. #[rstest] fn non_numeric_operand_reads_as_a_lane_not_a_dict_dump( #[values(NonNumericCase::StringAddition, NonNumericCase::UserTypeAddition)] diff --git a/tests/language/skolem_escape.rs b/tests/language/skolem_escape.rs index b7bb4c64..f88118a6 100644 --- a/tests/language/skolem_escape.rs +++ b/tests/language/skolem_escape.rs @@ -76,8 +76,8 @@ fn corpus_files() -> Vec { // A well-formed generalized scheme binds every variable it mentions under its // own `forall`/`forall`-row, so a free `Type::Var`/`EffRow::Var` left over is an -// escaped skolem. Sweeping the whole corpus asserts the fix holds not just on -// the repro but everywhere: no printed top-level scheme carries an unquantified +// escaped skolem. Sweeping the whole corpus checks every program rather than only +// the reproducer: no printed top-level scheme carries an unquantified // skolem. (`check` prepends the prelude, so the prelude's own schemes are swept // too on every file.) #[test] diff --git a/tests/language/soundness.rs b/tests/language/soundness.rs index 9b98e800..7aeb1097 100644 --- a/tests/language/soundness.rs +++ b/tests/language/soundness.rs @@ -8,6 +8,10 @@ // rejection and the exact structured diagnostic code, and one positive control // proves the coverage rule does not over-reject a fully covered handler. +use std::io::Write; +use std::process::{Command, Stdio}; + +use prism::error::{ErrKind, Frame, TypeError}; use prism::Error; // Two arms for the same operation `pick`. The second silently shadows the first @@ -56,6 +60,31 @@ fn handler_arity_mismatch_is_rejected() { assert_eq!(ty.code(), Some("E5010"), "got: {err}"); } +// One named `Cell(a)` instance fixes `a` at its first directed call. A later +// directed call through the same instance cannot silently instantiate another +// `a`, or the handler's operation and label evidence would disagree. +const NAMED_INSTANCE_ARGUMENT_MISMATCH: &str = + include_str!("../fixtures/language/soundness/named_instance_argument_mismatch.pr"); + +#[test] +fn named_instance_reuses_one_effect_argument_vector() { + let src = prism::with_prelude(NAMED_INSTANCE_ARGUMENT_MISMATCH); + let err = prism::check(&src) + .expect_err("one named effect instance cannot be both Cell(Int) and Cell(String)"); + let Error::Type(TypeError::Kind(diag)) = err else { + panic!("expected a structured type mismatch, got: {err}"); + }; + assert_eq!(diag.kind.code(), "E1022", "got: {diag}"); + let ErrKind::TypeMismatch { expected, found } = &diag.kind else { + panic!("expected the structured type-mismatch payload, got: {diag}"); + }; + assert_eq!((expected.as_str(), found.as_str()), ("Int", "String")); + assert!( + matches!(diag.context.as_slice(), [Frame::InFn(name)] if name == "main"), + "the mismatch must retain its declaration context: {diag}" + ); +} + // The mirror direction: `pair` declares two operation parameters, the clause // binds one. Too few is a compile error just as too many is. const HANDLER_ARITY_TOO_FEW: &str = @@ -260,6 +289,21 @@ const OR_NULL_NESTED: &str = r"fn m() : OrNull(OrNull(Int)) = Null fn main() = println(0) "; +// A transparent newtype can erase to the zero word even though its source type +// is nominal, so it needs declaration-aware representation proof. +const OR_NULL_ZERO_NEWTYPE: &str = r"newtype Zero = Zero(Unit) +fn m() : OrNull(Zero) = This(Zero(())) +fn main() = println(0) +"; + +// Constructor shape is not newtype evidence: an ordinary unary datatype keeps +// its allocated, non-zero wrapper and is a sound nullable element. +const OR_NULL_ORDINARY_UNARY: &str = r"type Box(a) = Box(a) +fn annotated(x : Box(Int)) : OrNull(Box(Int)) = This(x) +fn inferred(x : Box(Int)) = This(x) +fn main() = println(0) +"; + // A well-formed nullable over a heap element must still check. const OR_NULL_OK: &str = r#"fn m(b : Bool) : OrNull(String) = match b of @@ -283,6 +327,7 @@ fn or_null_zero_word_element_is_rejected() { or_null_rejected(OR_NULL_UNIT_INFERRED, "inferred OrNull(Unit)"); or_null_rejected(OR_NULL_UNINFERRED, "un-inferred OrNull element"); or_null_rejected(OR_NULL_NESTED, "nested OrNull"); + or_null_rejected(OR_NULL_ZERO_NEWTYPE, "zero-represented newtype element"); } #[test] @@ -291,6 +336,10 @@ fn or_null_over_heap_element_checks() { prism::check(&prism::with_prelude(OR_NULL_OK)).is_ok(), "OrNull(String) with Null/This arms must check" ); + assert!( + prism::check(&prism::with_prelude(OR_NULL_ORDINARY_UNARY)).is_ok(), + "an ordinary unary datatype keeps its boxed wrapper" + ); } // `@ once` on a closure parameter is a sound, type-carried multiplicity contract. @@ -369,8 +418,8 @@ fn once_direct_reuse_is_rejected() { #[test] fn once_delegation_to_many_context_is_rejected() { - // Contravariant subsumption: a `@ once` value cannot fill a `@ many` slot. - // This is a subsumption mismatch (a legacy `TypeFailure`, not a structured + // A `@ once` value in a `@ many` slot produces a contravariant subsumption + // mismatch (a legacy `TypeFailure`, not a structured // catalogue error), so the pinned surface is its message, not a code. let err = prism::check(&prism::with_prelude(ONCE_DELEGATION)) .expect_err("handing a `@ once` closure to a `@ many` context must be rejected"); @@ -541,34 +590,107 @@ fn noescape_uncheckable_argument_is_rejected() { assert_eq!(once_code(&src, "uncheckable noescape argument"), "E6062"); } -// A field name shared across a datatype's constructors with DIFFERING types is -// unsound: a record read `x.field` resolves the field type from whichever -// constructor `find_field` visits first, so `Square { size: String }` read as -// the `Circle { size: Int }` field would reinterpret a string pointer as an -// integer. This must be rejected at the declaration, not fault at runtime. +// Field types belong to constructors, so a match may refine the same label to +// a different type in each arm without making an unrefined projection safe. #[test] -fn conflicting_shared_field_type_is_rejected() { +fn variant_local_shared_field_types_are_accepted() { let src = prism::with_prelude( - "type Shape = Circle { size: Int } | Square { size: String }\n\ - fn main() = println(\"x\")\n", + r"type Shape = Circle { radius: Int } | Square { radius: Float } +fn radius_text(shape : Shape) : String = + match shape of + Circle { radius = radius } => show(radius) + Square { radius = radius } => show(radius) +fn main() = println(radius_text(Circle { radius = 7 })) +", ); - let err = prism::check(&src).expect_err("a conflicting shared field type must be rejected"); + prism::check(&src).expect("pattern refinement must permit constructor-local field types"); +} + +fn projection_error(src: &str, what: &str) { + let err = + prism::check(&prism::with_prelude(src)).expect_err(&format!("{what} must be rejected")); let Error::Type(ty) = &err else { - panic!("expected a type error, got: {err}"); + panic!("expected a type error for {what}, got: {err}"); }; - assert_eq!(ty.code(), Some("E4008"), "got: {err}"); + assert_eq!(ty.code(), Some("E1023"), "{what}: got {err}"); + assert!( + err.to_string().contains("match a constructor first"), + "{what}: diagnostic must name the repair: {err}" + ); } -// A field name shared across constructors with the SAME type is the sound -// common-field case and must keep compiling: `id` is `Int` in both arms. +// A field present on only one constructor is partial on the unrefined nominal. #[test] -fn compatible_shared_field_type_is_accepted() { - let src = prism::with_prelude( +fn constructor_specific_field_projection_is_rejected() { + projection_error( + "type Shape = Circle { radius: Int } | Square { side: Int }\n\ + fn radius(s : Shape) : Int = s.radius\n\ + fn main() = println(0)\n", + "constructor-specific field projection", + ); +} + +// Even a same-typed common field needs one lowering arm per constructor. Until +// projection facts carry that multi-arm evidence, accepting it would typecheck +// a program whose Core projection handles only one constructor. +#[test] +fn common_field_projection_without_multi_arm_evidence_is_rejected() { + projection_error( "type Tagged = A { id: Int, kind: String } | B { id: Int }\n\ fn tag_id(t : Tagged) : Int = t.id\n\ - fn main() = println(show(tag_id(B { id = 7 })))\n", + fn main() = println(0)\n", + "common field projection", + ); +} + +// The same guard applies below a valid outer projection: `outer.inner` records +// one constructor, but `.id` still receives an unrefined sum. +#[test] +fn nested_sum_field_projection_is_rejected() { + projection_error( + "type Tagged = A { id: Int } | B { id: Int }\n\ + type Outer = Outer { inner: Tagged }\n\ + fn tag_id(outer : Outer) : Int = outer.inner.id\n\ + fn main() = println(0)\n", + "nested sum field projection", + ); +} + +// Bare `.name` is syntactically a field projection, never UFCS fallback. A +// same-named top-level function must not turn E1023 into a silent call. +#[test] +fn partial_projection_does_not_fall_back_to_ufcs() { + projection_error( + "type Shape = Circle { radius: Int } | Square { side: Int }\n\ + fn radius(_shape : Shape) : Int = 99\n\ + fn read(shape : Shape) : Int = shape.radius\n\ + fn main() = println(0)\n", + "partial projection with a same-named function", ); - prism::check(&src).expect("a shared field of identical type must be accepted"); +} + +#[test] +fn single_constructor_field_projection_checks() { + let src = prism::with_prelude( + "type Box = Box { value: Int }\n\ + fn value(box : Box) : Int = box.value\n\ + fn main() = println(value(Box { value = 22 }))\n", + ); + prism::check(&src).expect("a single-constructor field projection must check"); +} + +#[test] +fn record_spread_from_unrefined_sum_is_rejected() { + let src = prism::with_prelude( + "type Shape = Circle { radius: Int } | Square { side: Int }\n\ + fn resize(shape : Shape) : Shape = Circle { ..shape, radius = 2 }\n\ + fn main() = println(0)\n", + ); + let err = prism::check(&src).expect_err("constructor spread must prove the base layout"); + let Error::Type(ty) = &err else { + panic!("expected a type error, got: {err}"); + }; + assert_eq!(ty.code(), Some("E1024"), "got: {err}"); } // A record pattern without `..` must bind every field of its constructor: the @@ -598,3 +720,106 @@ fn record_pattern_with_spread_is_accepted() { ); prism::check(&src).expect("a record pattern with `..` must be accepted"); } + +/// One session of the interactive entry point: the lines are fed to the real +/// binary over its own input and the whole transcript comes back, since a +/// refusal is reported on the error stream and an accepted line prints its +/// value on the output stream. +fn repl_transcript(lines: &str) -> String { + let mut child = Command::new(env!("CARGO_BIN_EXE_prism")) + .arg("repl") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("spawn the interactive entry point"); + child + .stdin + .take() + .expect("session input") + .write_all(format!("{lines}:quit\n").as_bytes()) + .expect("drive the session"); + let out = child.wait_with_output().expect("session transcript"); + format!( + "{}{}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ) +} + +/// A sum whose two constructors carry the same field, which is the shape that +/// makes both a projection and an update path ambiguous: nothing in the +/// unrefined type says which constructor a field access is aimed at. +const AMBIGUOUS_SUM: &str = "type Tagged = A { id: Int } | B { id: Int }\n"; + +// The interactive entry point reaches the checker without a file or a pipeline +// around it, so the constructor-ambiguity refusals are certified through it as +// well as through a compiled program. A field on an unrefined sum is refused in +// declaration position and in expression position alike; selecting the first +// constructor that happens to carry the label would make acceptance depend on +// how the program was entered. +#[test] +fn repl_refuses_unrefined_field_projection() { + let out = repl_transcript(&format!( + "{AMBIGUOUS_SUM}\ + fn tag_id(t : Tagged) : Int = t.id\n\ + let a = A {{ id = 1 }}\n\ + a.id\n" + )); + assert_eq!( + out.matches("[E1023]").count(), + 2, + "both the declared and the entered projection must be refused: {out}" + ); + assert!( + out.contains("match a constructor first"), + "the refusal must name the repair: {out}" + ); +} + +// The update path fails closed on the same shape, and it is the wider of the +// two: it refuses on the constructor count before it ever looks the field up, +// so a record update can never be aimed at a constructor the value may not be. +#[test] +fn repl_refuses_multi_constructor_update_path() { + let out = repl_transcript(&format!( + "{AMBIGUOUS_SUM}\ + fn bump(t : Tagged) : Tagged = {{ t | id = 1 }}\n\ + let a = A {{ id = 1 }}\n\ + {{ a | id = 2 }}\n" + )); + assert_eq!( + out.matches("[E1013]").count(), + 2, + "both the declared and the entered update path must be refused: {out}" + ); + assert!( + out.contains("single-constructor record"), + "the refusal must name what the path needs: {out}" + ); +} + +// The control that keeps the two refusals honest: on a single-constructor +// record the very same projection and update are unambiguous, and the session +// evaluates them rather than refusing everything it is handed. +#[test] +fn repl_accepts_single_constructor_field_paths() { + let out = repl_transcript( + "type Box = Box { value: Int }\n\ + let b = Box { value = 7 }\n\ + b.value\n\ + { b | value = 8 }\n", + ); + assert!( + out.contains("7 : Int"), + "the projection must evaluate: {out}" + ); + assert!( + out.contains("Box(8) : Box"), + "the update must evaluate: {out}" + ); + assert!( + !out.contains("Type Error"), + "an unambiguous field path must not be refused: {out}" + ); +} diff --git a/tests/language/typed_holes.rs b/tests/language/typed_holes.rs index ad8b9249..201c9f74 100644 --- a/tests/language/typed_holes.rs +++ b/tests/language/typed_holes.rs @@ -13,7 +13,7 @@ fn batch_frontend_refuses_typed_holes() { error.hole_report().map(|report| report.name.as_str()), Some("todo") ); - insta::assert_snapshot!(error.to_string(), @"typed hole `?todo`: expected Int with effects {}; candidates: none; 161 binding(s) in scope"); + insta::assert_snapshot!(error.to_string(), @"typed hole `?todo`: expected Int with effects {}; candidates: none; 163 binding(s) in scope"); } #[test] diff --git a/tests/lineage_suite/lineage_tiles.rs b/tests/lineage_suite/lineage_tiles.rs index 519cfb44..145d0c4a 100644 --- a/tests/lineage_suite/lineage_tiles.rs +++ b/tests/lineage_suite/lineage_tiles.rs @@ -162,7 +162,7 @@ fn four_verb_loop_over_the_tiles_pipeline() { ); // 4. edit one tile, re-record, and diff: exactly that tile plus its downstream - // move; the other tiles and the config are preserved; nothing is added/removed. + // move. The other tiles and the config are preserved, with no additions or removals. fs::write(p.join("tiles/north.gray"), NORTH_EDITED).unwrap(); let out = prism( p, diff --git a/tests/lineage_suite/run_lineage.rs b/tests/lineage_suite/run_lineage.rs index c0c709bb..584778c3 100644 --- a/tests/lineage_suite/run_lineage.rs +++ b/tests/lineage_suite/run_lineage.rs @@ -399,7 +399,8 @@ fn why_output_top_level_defaults_to_the_primary_output() { ); } -// The `.plineage` arm of `prism diff` acceptance: changing one input file names exactly that input as +// The `.plineage` arm of `prism diff` acceptance: changing one input file names +// exactly that input as // moved along with every downstream digest (trace, stdout), everything else // preserved, and it exits nonzero so it can gate CI. #[test] diff --git a/tests/native/compiler_cache.rs b/tests/native/compiler_cache.rs index 4236dac2..5fa3bfb5 100644 --- a/tests/native/compiler_cache.rs +++ b/tests/native/compiler_cache.rs @@ -1,6 +1,6 @@ use std::collections::BTreeMap; use std::fs; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::Command; use prism::lineage::{ @@ -12,7 +12,7 @@ use prism::{ SessionStats, }; -use crate::support::{require_cc, TempDir}; +use crate::support::{assert_same_binary, require_cc, TempDir}; // The default worker count auto-detects host parallelism, so the sequential // arm of each byte-diff oracle must pin one worker explicitly. @@ -43,15 +43,38 @@ fn drop_linked_queries(root: &Path) { } } +// Query bindings sit one shard level below the kind directory +// (queries//<2hex>/), so every direct reader walks that level. +fn query_files(root: &Path, kind: &str) -> Vec { + let mut files = Vec::new(); + for shard in fs::read_dir(root.join(kind)).unwrap() { + let shard = shard.unwrap(); + if !shard.file_type().unwrap().is_dir() { + continue; + } + for entry in fs::read_dir(shard.path()).unwrap() { + files.push(entry.unwrap().path()); + } + } + files.sort(); + files +} + +fn query_count(root: &Path, kind: &str) -> usize { + query_files(root, kind).len() +} + fn query_bindings(root: &Path, kind: &str) -> BTreeMap { - fs::read_dir(root.join(kind)) - .unwrap() - .map(|entry| { - let entry = entry.unwrap(); - ( - entry.file_name().to_string_lossy().into_owned(), - fs::read_to_string(entry.path()).unwrap(), - ) + query_files(root, kind) + .into_iter() + .map(|path| { + let shard = path.parent().unwrap().file_name().unwrap(); + let key = format!( + "{}{}", + shard.to_string_lossy(), + path.file_name().unwrap().to_string_lossy() + ); + (key, fs::read_to_string(path).unwrap()) }) .collect() } @@ -148,21 +171,11 @@ fn warm_native_build_materializes_byte_identical_binary() { first.cache_explanation(), "linked artifact and LLVM bitcode keys changed" ); - let native_objects = fs::read_dir(tmp.store_root().join(NATIVE_OBJECT_QUERIES)) - .unwrap() - .count(); - let runtime_objects = fs::read_dir(tmp.store_root().join(RUNTIME_OBJECT_QUERIES)) - .unwrap() - .count(); - let optimized_sccs = fs::read_dir(tmp.store_root().join(OPTIMIZED_SCC_QUERIES)) - .unwrap() - .count(); - let llvm_sccs = fs::read_dir(tmp.store_root().join(LLVM_SCC_QUERIES)) - .unwrap() - .count(); - let closure_summaries = fs::read_dir(tmp.store_root().join(CLOSURE_SUMMARY_QUERIES)) - .unwrap() - .count(); + let native_objects = query_count(&tmp.store_root(), NATIVE_OBJECT_QUERIES); + let runtime_objects = query_count(&tmp.store_root(), RUNTIME_OBJECT_QUERIES); + let optimized_sccs = query_count(&tmp.store_root(), OPTIMIZED_SCC_QUERIES); + let llvm_sccs = query_count(&tmp.store_root(), LLVM_SCC_QUERIES); + let closure_summaries = query_count(&tmp.store_root(), CLOSURE_SUMMARY_QUERIES); assert!(native_objects > 1); assert!(llvm_sccs > 1); assert!(closure_summaries > 0); @@ -187,7 +200,11 @@ fn warm_native_build_materializes_byte_identical_binary() { assert_eq!(second.bitcode_cache, NativeCacheStatus::Disabled); assert!(second.definition_hashes.is_none()); assert_eq!(second.cache_explanation(), "linked artifact key matched"); - assert_eq!(fs::read(&bin).unwrap(), cold); + assert_same_binary( + "warm link hit vs cold build", + &cold, + &fs::read(&bin).unwrap(), + ); assert!(!bin.with_extension("bc").exists()); let warm_run = Command::new(&bin).output().unwrap(); let warm_trace = prism::ObservationTrace::from_process( @@ -205,7 +222,11 @@ fn warm_native_build_materializes_byte_identical_binary() { fs::remove_file(&bin).unwrap(); let parallel = build_on_report(&src, &roots, &bin, ¶llel_cfg).unwrap(); assert_eq!(parallel.cache, NativeCacheStatus::Hit); - assert_eq!(fs::read(&bin).unwrap(), cold); + assert_same_binary( + "parallel workers vs cold build", + &cold, + &fs::read(&bin).unwrap(), + ); let parallel_run = Command::new(&bin).output().unwrap(); assert_eq!( prism::ObservationTrace::from_process( @@ -227,23 +248,21 @@ fn warm_native_build_materializes_byte_identical_binary() { relocation.cache_explanation(), "linked artifact key matched" ); - assert_eq!(fs::read(&relocated).unwrap(), cold); + assert_same_binary( + "relocated output vs cold build", + &cold, + &fs::read(&relocated).unwrap(), + ); assert_eq!( - fs::read_dir(tmp.store_root().join(NATIVE_OBJECT_QUERIES)) - .unwrap() - .count(), + query_count(&tmp.store_root(), NATIVE_OBJECT_QUERIES), native_objects ); assert_eq!( - fs::read_dir(tmp.store_root().join(RUNTIME_OBJECT_QUERIES)) - .unwrap() - .count(), + query_count(&tmp.store_root(), RUNTIME_OBJECT_QUERIES), runtime_objects ); assert_eq!( - fs::read_dir(tmp.store_root().join(OPTIMIZED_SCC_QUERIES)) - .unwrap() - .count(), + query_count(&tmp.store_root(), OPTIMIZED_SCC_QUERIES), optimized_sccs ); @@ -252,25 +271,23 @@ fn warm_native_build_materializes_byte_identical_binary() { let semantic = build_on_report(&formatted_only, &roots, &bin, &cfg).unwrap(); assert_eq!(semantic.cache, NativeCacheStatus::Hit); assert!(semantic.definition_hashes.is_some()); - assert_eq!(fs::read(&bin).unwrap(), cold); + assert_same_binary( + "formatting-only edit vs cold build", + &cold, + &fs::read(&bin).unwrap(), + ); assert_eq!( - fs::read_dir(tmp.store_root().join(OPTIMIZED_SCC_QUERIES)) - .unwrap() - .count(), + query_count(&tmp.store_root(), OPTIMIZED_SCC_QUERIES), optimized_sccs, "formatting-only edits must write no semantic SCC artifacts" ); assert_eq!( - fs::read_dir(tmp.store_root().join(LLVM_SCC_QUERIES)) - .unwrap() - .count(), + query_count(&tmp.store_root(), LLVM_SCC_QUERIES), llvm_sccs, "formatting-only edits must write no backend SCC artifacts" ); assert_eq!( - fs::read_dir(tmp.store_root().join(CLOSURE_SUMMARY_QUERIES)) - .unwrap() - .count(), + query_count(&tmp.store_root(), CLOSURE_SUMMARY_QUERIES), closure_summaries, "formatting-only edits must write no closure summaries" ); @@ -280,31 +297,22 @@ fn warm_native_build_materializes_byte_identical_binary() { let changed_report = build_on_report(&changed, &roots, &bin, &cfg).unwrap(); assert_eq!(changed_report.cache, NativeCacheStatus::Write); assert!( - fs::read_dir(tmp.store_root().join(OPTIMIZED_SCC_QUERIES)) - .unwrap() - .count() - > optimized_sccs, + query_count(&tmp.store_root(), OPTIMIZED_SCC_QUERIES) > optimized_sccs, "a semantic edit must write its affected SCC cone" ); - let changed_llvm_sccs = fs::read_dir(tmp.store_root().join(LLVM_SCC_QUERIES)) - .unwrap() - .count(); + let changed_llvm_sccs = query_count(&tmp.store_root(), LLVM_SCC_QUERIES); assert_eq!( changed_llvm_sccs - llvm_sccs, 2, "only the changed backend SCC and the explicit global metadata plan move" ); - let changed_closure_summaries = fs::read_dir(tmp.store_root().join(CLOSURE_SUMMARY_QUERIES)) - .unwrap() - .count(); + let changed_closure_summaries = query_count(&tmp.store_root(), CLOSURE_SUMMARY_QUERIES); assert_eq!( changed_closure_summaries - closure_summaries, 1, "only the changed backend SCC may write a new closure summary" ); - let changed_native_objects = fs::read_dir(tmp.store_root().join(NATIVE_OBJECT_QUERIES)) - .unwrap() - .count(); + let changed_native_objects = query_count(&tmp.store_root(), NATIVE_OBJECT_QUERIES); assert_eq!( changed_native_objects - native_objects, 2, @@ -324,7 +332,7 @@ fn warm_native_build_materializes_byte_identical_binary() { let report = build_on_report(&changed, &roots, &bin, &cfg).unwrap(); assert_eq!(report.cache, NativeCacheStatus::Disabled); let uncached = fs::read(&bin).unwrap(); - assert_eq!(uncached, changed_cached); + assert_same_binary("cache disabled vs cache warm", &changed_cached, &uncached); let uncached_run = Command::new(&bin).output().unwrap(); assert_eq!( prism::ObservationTrace::from_process( @@ -425,7 +433,11 @@ fn typed_route_second_build_preserves_warm_cache_artifacts() { "an unchanged input must reuse the final artifact" ); assert_eq!(warm_report.bitcode_cache, NativeCacheStatus::Disabled); - assert_eq!(fs::read(observed_bin).unwrap(), observed_bytes); + assert_same_binary( + "warm rebuild vs observed build", + &observed_bytes, + &fs::read(observed_bin).unwrap(), + ); } #[test] @@ -491,8 +503,16 @@ fn incremental_store_reaches_the_fresh_final_artifacts() { let incremental_bytes = fs::read(&incremental_bin).unwrap(); let fresh_bytes = fs::read(&fresh_bin).unwrap(); let parallel_bytes = fs::read(¶llel_bin).unwrap(); - assert_eq!(incremental_bytes, fresh_bytes); - assert_eq!(parallel_bytes, fresh_bytes); + assert_same_binary( + "incremental store vs fresh store", + &fresh_bytes, + &incremental_bytes, + ); + assert_same_binary( + "parallel workers vs fresh store", + &fresh_bytes, + ¶llel_bytes, + ); let run = |path: &Path| { let output = Command::new(path).output().unwrap(); @@ -564,9 +584,10 @@ fn sequential_and_parallel_scc_artifacts_are_identical() { query_bindings(¶llel.store_root(), CLOSURE_SUMMARY_QUERIES), "worker count must not alter closure summary identities" ); - assert_eq!( - fs::read(sequential_bin).unwrap(), - fs::read(parallel_bin).unwrap() + assert_same_binary( + "sequential SCC backend vs parallel SCC backend", + &fs::read(sequential_bin).unwrap(), + &fs::read(parallel_bin).unwrap(), ); } @@ -717,7 +738,11 @@ fn effectful_build_publishes_no_legacy_effect_queries_and_retires_stale_facts() assert_eq!(second_report.cache, NativeCacheStatus::Hit); assert_eq!(second_report.bitcode_cache, NativeCacheStatus::Disabled); let second = Command::new(&first_bin).output().unwrap(); - assert_eq!(fs::read(&first_bin).unwrap(), first_bytes); + assert_same_binary( + "session warm rebuild vs first build", + &first_bytes, + &fs::read(&first_bin).unwrap(), + ); assert_eq!(second.stdout, first.stdout); assert_eq!(second.stderr, first.stderr); assert_eq!(second.status.code(), first.status.code()); @@ -762,17 +787,18 @@ fn effectful_build_publishes_no_legacy_effect_queries_and_retires_stale_facts() .decisions() .iter() .all(|decision| decision.kind != QueryKind::Effect)); + // The planted bindings sit flat under their kind directories, the shape a + // real pre-sharding store leaves behind. A build must not touch them: a + // flat relic is invisible to the sharded read path and its removal belongs + // to `store gc` alone. assert_eq!( - query_bindings(&upgrade.store_root(), RETIRED_EFFECT_PLAN_QUERIES), - BTreeMap::from([("legacy-plan".to_string(), "stale plan binding".to_string())]), + fs::read_to_string(stale_plan.join("legacy-plan")).unwrap(), + "stale plan binding", "old plan bindings are inert and remain Store-GC-owned" ); assert_eq!( - query_bindings(&upgrade.store_root(), RETIRED_EFFECT_RESULT_QUERIES), - BTreeMap::from([( - "legacy-result".to_string(), - "stale result binding".to_string() - )]), + fs::read_to_string(stale_result.join("legacy-result")).unwrap(), + "stale result binding", "old result bindings are inert and remain Store-GC-owned" ); let ledger = FactLedger::load(&store, &scope).unwrap(); @@ -816,12 +842,10 @@ fn corrupt_backend_scc_is_rejected() { cfg.flags.store_path = Some(tmp.store_root()); build_on_report(&src, &roots, &tmp.join("first"), &cfg).unwrap(); - let query = fs::read_dir(tmp.store_root().join(LLVM_SCC_QUERIES)) - .unwrap() + let query = query_files(&tmp.store_root(), LLVM_SCC_QUERIES) + .into_iter() .next() - .unwrap() - .unwrap() - .path(); + .unwrap(); let binding = fs::read_to_string(query).unwrap(); let object_hash = binding.lines().nth(1).unwrap(); let object = tmp @@ -852,12 +876,10 @@ fn corrupt_backend_closure_summary_is_rejected() { cfg.flags.store_path = Some(tmp.store_root()); build_on_report(&src, &roots, &tmp.join("first"), &cfg).unwrap(); - let query = fs::read_dir(tmp.store_root().join(CLOSURE_SUMMARY_QUERIES)) - .unwrap() + let query = query_files(&tmp.store_root(), CLOSURE_SUMMARY_QUERIES) + .into_iter() .next() - .unwrap() - .unwrap() - .path(); + .unwrap(); let binding = fs::read_to_string(query).unwrap(); let object_hash = binding.lines().nth(1).unwrap(); let object = tmp @@ -888,12 +910,10 @@ fn corrupt_optimized_scc_is_rejected() { cfg.flags.store_path = Some(tmp.store_root()); build_on_report(&src, &roots, &tmp.join("first"), &cfg).unwrap(); - let query = fs::read_dir(tmp.store_root().join(OPTIMIZED_SCC_QUERIES)) - .unwrap() + let query = query_files(&tmp.store_root(), OPTIMIZED_SCC_QUERIES) + .into_iter() .next() - .unwrap() - .unwrap() - .path(); + .unwrap(); let binding = fs::read_to_string(query).unwrap(); let object_hash = binding.lines().nth(1).unwrap(); let object = tmp @@ -934,7 +954,11 @@ fn session_semantic_hit_matches_cold_native_build() { fs::remove_file(&bin).unwrap(); let second = build_on_report(&formatted, &roots, &bin, &cfg).unwrap(); assert_eq!(second.cache, NativeCacheStatus::Disabled); - assert_eq!(fs::read(&bin).unwrap(), cold); + assert_same_binary( + "session semantic hit vs cold build", + &cold, + &fs::read(&bin).unwrap(), + ); assert_eq!( session.stats(), SessionStats { diff --git a/tests/native/parity.rs b/tests/native/parity.rs index 7d37701c..f1c25f23 100644 --- a/tests/native/parity.rs +++ b/tests/native/parity.rs @@ -943,39 +943,13 @@ fn systemf_dk_witnesses_pinned() { ); } -// Resource counters on the differential run. +// Record interpreter transitions and native heap allocations alongside output +// parity. Each counter is compared only with its own recorded baseline. // -// Byte-identical output says the two sides compute the same thing; it says -// nothing about what either one spent doing it. An optimization that quietly -// stops firing (a lost specialization, a fold that starts rebuilding its -// accumulator each step) keeps every byte of output identical, so the corpus -// stays green while the cost of running it multiplies. Nothing above notices, -// and the gap is widest for programs that are run interpreted rather than -// compiled, where there is no native binary whose timings anyone is watching. +// The allowed band combines `COST_DRIFT_FACTOR` with `COST_DRIFT_SLACK`, avoiding +// churn on small counts while retaining a bound for every program. // -// So the corpus run also records what each program cost on both sides, each in -// its own unit: machine transitions for the interpreter, heap cells materialized -// for the native binary. Both counters are already there (the machine's step -// count and the runtime's `PRISM_ALLOC_STATS` report), both are pure functions -// of the program and the compiler, and both ride the build and run the parity -// check already performs, so measuring them costs nothing. The two units are not -// comparable to each other and nothing here compares them; each is compared -// against its own recorded baseline. -// -// The bound is deliberately loose: a count may move by a factor of -// `COST_DRIFT_FACTOR` plus a flat `COST_DRIFT_SLACK` before it is reported. -// Anything inside passes and leaves the golden alone, which keeps this from -// becoming an exact-count oracle that has to be reblessed for every small -// movement of ordinary compiler work. The flat term is what lets the bound stay -// armed on the small counts: most of the corpus materializes only a handful of -// cells, and a pure ratio there would fail on a jump of three that means nothing, -// so the choice would be between churn and exempting three quarters of the -// programs from the check. Adding slack instead bounds every program, with the -// smallest ones given proportionally the most room. -// -// A move in either direction fails, an increase as a regression and a decrease -// as a stale baseline. A win that goes unrecorded leaves the baseline inflated, -// and the next regression hides inside the slack it left behind. +// Increases are regressions; decreases indicate a stale baseline. const COST_MANIFEST: &str = "tests/cost_manifest.txt"; const COST_MANIFEST_ACCEPT: &str = "PRISM_ACCEPT_COST_MANIFEST"; diff --git a/tests/native/perf_gate.rs b/tests/native/perf_gate.rs index d525f4f7..20c7b885 100644 --- a/tests/native/perf_gate.rs +++ b/tests/native/perf_gate.rs @@ -20,6 +20,8 @@ use std::path::Path; use std::process::Command; use std::{env, fs}; +use crate::support::{ALLOCATED_BYTES_SUFFIX, ALLOC_STATS}; + // Corpus discovery and prelude-prepending source loader, shared with the parity // oracles. The tier manifest below records the same program set those gates diff, so // it reuses the one definition of "the runnable corpus" rather than rediscovering @@ -45,6 +47,9 @@ const PERF_WIRE_ENCODE: &str = include_str!("../cases/perf/wire_encode.pr"); const PERF_WIRE_DECODE: &str = include_str!("../cases/perf/wire_decode.pr"); const PERF_BUF_CHUNKS: &str = include_str!("../cases/perf/buf_chunks.pr"); const PERF_BYTES_CODEC: &str = include_str!("../cases/perf/bytes_codec_slope.pr"); +const PERF_STR_SLICE_WINDOW: &str = include_str!("../cases/perf/str_slice_window.pr"); +const PERF_JSON_ESCAPE_RUNS: &str = include_str!("../cases/perf/json_escape_runs.pr"); +const PERF_BYTES_BODY_DECODE: &str = include_str!("../cases/perf/bytes_body_decode.pr"); const N_PLACEHOLDER: &str = "__N__"; const PIPELINE_PLACEHOLDER: &str = "__PIPELINE__"; @@ -121,6 +126,18 @@ fn stat_build( suffix: &str, build: impl Fn(&str, &Path) -> Result<(), prism::error::Error>, ) -> Result { + stat_build_many(full, tag, stat_env, &[suffix], build).map(|v| v[0]) +} + +// Every counter a stats run reports is on the same stderr, so a family of them +// costs one build and one run, not one per counter. +fn stat_build_many( + full: &str, + tag: &str, + stat_env: &str, + suffixes: &[&str], + build: impl Fn(&str, &Path) -> Result<(), prism::error::Error>, +) -> Result, String> { let bin = env::temp_dir().join(format!( "prism_perf_{}_{}", std::process::id(), @@ -140,14 +157,19 @@ fn stat_build( cleanup(); let out = out.map_err(|e| format!("{tag}: spawn failed: {e}"))?; let stderr = String::from_utf8_lossy(&out.stderr); - let line = stderr - .lines() - .find(|l| l.trim_end().ends_with(suffix)) - .ok_or_else(|| format!("{tag}: no `{suffix}` line in stderr: {stderr:?}"))?; - line.split_whitespace() - .nth(1) - .and_then(|n| n.parse().ok()) - .ok_or_else(|| format!("{tag}: cannot parse count from {line:?}")) + suffixes + .iter() + .map(|suffix| { + let line = stderr + .lines() + .find(|l| l.trim_end().ends_with(suffix)) + .ok_or_else(|| format!("{tag}: no `{suffix}` line in stderr: {stderr:?}"))?; + line.split_whitespace() + .nth(1) + .and_then(|n| n.parse().ok()) + .ok_or_else(|| format!("{tag}: cannot parse count from {line:?}")) + }) + .collect() } // The fusion corpus: each program drives a different path to the zero-allocation @@ -397,6 +419,9 @@ fn or_null_values_allocate_no_cells() { const PERF_ARENA_REGION_FILL: &str = include_str!("../cases/perf/arena_region_fill.pr"); const PERF_ARENA_REGION_LOOP: &str = include_str!("../cases/perf/arena_region_loop.pr"); +const PERF_ARENA_PROMOTE_NONE: &str = include_str!("../cases/perf/arena_promote_none.pr"); +const PERF_ARENA_PROMOTE_LINEAR: &str = include_str!("../cases/perf/arena_promote_linear.pr"); +const PERF_ARENA_PROMOTE_SHARED: &str = include_str!("../cases/perf/arena_promote_shared.pr"); // Growing the list built under one `with_arena` activation must not grow the // runtime allocation count at all. Every `Cons` comes from the region; only the @@ -460,6 +485,167 @@ fn arena_region_loop_allocates_only_activation_constants() { ); } +// --------------------------------------------------------------------------- +// The promotion oracle. Escaping a `with_arena` scope deep-copies every +// arena-owned cell the result reaches, and what that costs is invisible to every +// other gate in the tree: output parity, the leak balance, and the region +// ratchets above all stay green whether the walk copies a shared sub-DAG once or +// once per path that reaches it, because both produce the identical value and +// neither leaks. `PRISM_PROMOTE_STATS` reports the walk's own size and shape, and +// the three fixtures below pin it at nothing escaping, at an unshared spine, and +// at a shared DAG where the two behaviors differ exponentially. +// +// The counters are exact counts, not timings, which is what lets these read as +// equalities against a stated baseline rather than as thresholds. A timing gate +// would owe a distribution over repeated samples to be worth anything, because a +// single favorable run proves nothing; a count of cells copied is the same +// integer on every run of a deterministic program, so one sample is the whole +// distribution and a regression cannot hide in variance. +// +// These are anti-vacuous by construction: with the promotion walk's forwarding +// disabled, `arena_promotion_copies_a_shared_cell_once` reads roughly 2^N rather +// than N (measured 4.3 GB and 4.18 s at depth 26 against 1.4 MB and 0.00 s), +// `arena_promotion_of_an_unshared_list_is_one_copy_per_cell` is unaffected +// because that fixture has no sharing to lose, and +// `arena_promotion_does_nothing_when_nothing_escapes` is unaffected because the +// walk never runs. So the shared fixture is the one carrying the claim and it +// has been shown to fail without the behavior it asserts. + +const PROMOTE_STATS: &str = "PRISM_PROMOTE_STATS"; + +// The four counters from one promotion-instrumented run. +struct PromoteStats { + copied: i64, + shared: i64, + nodes: i64, + edges: i64, +} + +impl PromoteStats { + // Ordered as the fields above; the runtime prints one `prism: ` + // line per counter. + const SUFFIXES: [&'static str; 4] = [ + "cells promoted", + "promotion copies shared", + "promotion nodes visited", + "promotion edges visited", + ]; + + fn measure(template: &str, tag: &str, n: i64) -> Self { + let v = stat_build_many( + &perf_src_n(template, n), + tag, + PROMOTE_STATS, + &Self::SUFFIXES, + |src, bin| { + let mut cfg = prism::Config::from_env(); + cfg.flags.opt_level = prism::OptLevel::O2; + prism::build_on(src, &prism::default_roots(Path::new(".")), bin, &cfg) + }, + ) + .unwrap_or_else(|e| panic!("{e}")); + Self { + copied: v[0], + shared: v[1], + nodes: v[2], + edges: v[3], + } + } +} + +// Nothing reachable from the result is arena-owned, so the walk never runs. A +// promotion that fires here is deep-copying a region the scope was entitled to +// drop wholesale. +#[test] +fn arena_promotion_does_nothing_when_nothing_escapes() { + require_cc(); + let n = 500; + let s = PromoteStats::measure(PERF_ARENA_PROMOTE_NONE, "arena_promote_none", n); + assert_eq!( + (s.copied, s.shared, s.nodes, s.edges), + (0, 0, 0, 0), + "a scope returning a scalar promoted {} cells over {} nodes and {} edges ({} shared) \ + with a {n}-element region; nothing reachable from the result is arena-owned, so the \ + promotion walk should not have run at all", + s.copied, + s.nodes, + s.edges, + s.shared + ); +} + +// An unshared spine: every cell is reached by exactly one path, so each of the +// three walk measures is one per element and no edge ever finds an existing copy +// to reuse. This is what promotion costs with no sharing to preserve, and it is +// what the shared case is compared against. +#[test] +fn arena_promotion_of_an_unshared_list_is_one_copy_per_cell() { + let (small, big) = (500_i64, 5_000_i64); + require_cc(); + let lo = PromoteStats::measure(PERF_ARENA_PROMOTE_LINEAR, "arena_promote_linear_lo", small); + let hi = PromoteStats::measure(PERF_ARENA_PROMOTE_LINEAR, "arena_promote_linear_hi", big); + let span = big - small; + let (copied, nodes, edges) = ( + (hi.copied - lo.copied) / span, + (hi.nodes - lo.nodes) / span, + (hi.edges - lo.edges) / span, + ); + assert_eq!( + (copied, nodes, hi.shared), + (1, 1, 0), + "promoting an unshared {big}-element list copied {copied} cells and entered {nodes} \ + nodes per element with {} shared reuses (baseline 1, 1, and 0: one copy and one visit \ + per cell, and nothing to share); {edges} edges/element", + hi.shared + ); + assert!( + edges <= 2, + "promoting an unshared list examined {edges} edges/element ({} at n={small}, {} at \ + n={big}); a `Cons` has two fields, so the walk is revisiting cells", + lo.edges, + hi.edges + ); +} + +// A shared DAG: each level's two fields point at the same child, so N region +// cells span 2^N root-to-leaf paths. Copying once per cell costs N and copying +// once per path costs 2^N, and the two produce the identical value, so the +// counters are the only thing that can tell them apart. `shared` rising with +// depth is the positive evidence: it counts edges that reused an existing copy, +// which is region sharing surviving into the promoted result. +#[test] +fn arena_promotion_copies_a_shared_cell_once() { + require_cc(); + let (small, big) = (12_i64, 22_i64); + let lo = PromoteStats::measure(PERF_ARENA_PROMOTE_SHARED, "arena_promote_shared_lo", small); + let hi = PromoteStats::measure(PERF_ARENA_PROMOTE_SHARED, "arena_promote_shared_hi", big); + let span = big - small; + let (copied, nodes, shared) = ( + (hi.copied - lo.copied) / span, + (hi.nodes - lo.nodes) / span, + (hi.shared - lo.shared) / span, + ); + assert!( + copied <= 2 && nodes <= 2, + "promoting a shared DAG out of `with_arena` copied {copied} cells and entered {nodes} \ + nodes per sharing level ({} and {} at depth {small}, {} and {} at depth {big}; \ + baseline 1 each, the single new cell each level adds); the promotion walk is \ + descending into a shared sub-DAG once per path reaching it rather than once per cell", + lo.copied, + lo.nodes, + hi.copied, + hi.nodes + ); + assert_eq!( + shared, 1, + "promoting a shared DAG reused an existing copy on {shared} edges per sharing level \ + ({} at depth {small}, {} at depth {big}; baseline 1, the second field of each level \ + pointing at the child the first field already promoted); region sharing is not \ + surviving promotion", + lo.shared, hi.shared + ); +} + // --------------------------------------------------------------------------- // Wire/Bytes allocation ratchets. The serialization codec threads one growable // buffer through a linear builder fold (`buf_push`/`buf_append`) instead of a @@ -567,14 +753,123 @@ fn bytes_codec_allocation_is_flat() { ); } +// Bytes the program allocates at -O2 for input size `n`. Cells and bytes are not +// the same measurement: a string holds its payload inline, so a copy and a window +// onto someone else's bytes are both exactly one cell, and only the byte total +// separates them. +fn alloc_bytes_o2(template: &str, tag: &str, n: i64) -> i64 { + stat_src_o2( + &perf_src_n(template, n), + tag, + ALLOC_STATS, + ALLOCATED_BYTES_SUFFIX, + ) + .unwrap_or_else(|e| panic!("{e}")) +} + +// Slicing a string is a window onto it, not a copy: the result holds a reference +// to the parent and reads through it, so a slice costs the same on three bytes as +// on three megabytes. The probe takes 200 long windows onto one N-byte string, so +// a copying slice would materialize the sum of their lengths (the number the probe +// prints, ~1.8 MB at n=10000 against ~180 KB at n=1000) while a window pays one +// small cell each. What remains proportional to N either way is the parent's own +// construction, one buffer plus one string, so the byte total may grow by a small +// multiple of the size increase and no more. +#[test] +fn string_slice_is_a_window_not_a_copy() { + require_cc(); + let (small, big) = (1000_i64, 10_000_i64); + let (lo, hi) = ( + alloc_bytes_o2(PERF_STR_SLICE_WINDOW, "str slice window", small), + alloc_bytes_o2(PERF_STR_SLICE_WINDOW, "str slice window", big), + ); + let grew = hi - lo; + let scale = big - small; + // Anti-vacuous: the parent is materialized once, so the input really did scale + // with N and a probe that stopped building a large string would fail here + // rather than pass the ceiling for free. + assert!( + grew >= scale, + r"the window probe stopped scaling with its input ({lo} bytes at n={small}, {hi} at n={big}); its parent string is no longer proportional to N and the ceiling below would pass vacuously" + ); + // The parent costs a buffer and a string, so three times the size increase is + // room to spare for the aliasing path and far under the ~200x a copy pays. + assert!( + grew <= 3 * scale + 4096, + r"string slicing allocates with the parent's size ({lo} bytes at n={small}, {hi} at n={big}, growth {grew} for a {scale}-byte larger parent); the slice copies its window instead of aliasing it" + ); +} + +// Escaping a string is one pass over it, not one rebuild per escape. The probe +// encodes a string whose every other byte is a quote, so both the escape count +// and the output length scale with N; appending each clean run and each escape +// into one growable buffer keeps the byte total proportional to N, while an +// accumulator that rebuilds the escaped output at every escape pays the escape +// count times the length escaped so far and grows with N squared. The ceiling +// sits an order of magnitude under that square and well above the linear cost, +// so it separates the two shapes rather than pinning a constant. +#[test] +fn json_escaping_appends_runs_instead_of_rebuilding() { + require_cc(); + let (small, big) = (200_i64, 2000_i64); + let (lo, hi) = ( + alloc_bytes_o2(PERF_JSON_ESCAPE_RUNS, "json escape runs", small), + alloc_bytes_o2(PERF_JSON_ESCAPE_RUNS, "json escape runs", big), + ); + let grew = hi - lo; + let scale = big - small; + // Anti-vacuous: the encoded output really is proportional to N, so a probe + // that stopped scaling would fail here instead of passing the ceiling for + // free. + assert!( + grew >= scale, + r"the escape probe stopped scaling with its input ({lo} bytes at n={small}, {hi} at n={big}); its output is no longer proportional to N and the ceiling below would pass vacuously" + ); + assert!( + grew <= 150 * scale, + r"escape-heavy encoding allocates with the square of its input ({lo} bytes at n={small}, {hi} at n={big}, growth {grew} for {scale} more escapes); the escaper rebuilds the output per escape instead of appending runs to one buffer" + ); +} + +// Decoding a byte payload accumulates into one buffer that is extended in place, +// not rebuilt per byte. A builder is extended in place only while it is uniquely +// owned, and an accumulator threaded through a failure row is shared at every +// step, so the same loop written inside the row copies the whole accumulation on +// each push and costs the square of N. Nothing about the program's output reveals +// which shape ran, so only the allocation slope catches the regression: the +// ceiling sits several times over the linear cost and an order of magnitude under +// the square. +#[test] +fn byte_payload_decoding_extends_one_buffer_in_place() { + require_cc(); + let (small, big) = (1000_i64, 8000_i64); + let (lo, hi) = ( + alloc_bytes_o2(PERF_BYTES_BODY_DECODE, "bytes body decode", small), + alloc_bytes_o2(PERF_BYTES_BODY_DECODE, "bytes body decode", big), + ); + let grew = hi - lo; + let scale = big - small; + // Anti-vacuous: the decoded payload really is proportional to N, so a probe + // that stopped scaling would fail here instead of passing the ceiling for + // free. + assert!( + grew >= scale, + r"the byte-payload probe stopped scaling with its input ({lo} bytes at n={small}, {hi} at n={big}); its payload is no longer proportional to N and the ceiling below would pass vacuously" + ); + assert!( + grew <= 1200 * scale, + r"decoding a byte payload allocates with the square of its length ({lo} bytes at n={small}, {hi} at n={big}, growth {grew} for {scale} more bytes); the reader threads its accumulator through the failure row, so every push copies what it has decoded so far" + ); +} + // The container codec's builder fold, checked statically in the elaborated Core. // A program that encodes a list must reach `buf_append`, the linear accumulation // primitive the element fold threads through; its presence proves the container // encoder builds into one growable buffer rather than nesting immutable `wire_cat` -// concatenations. The runtime slope guards above measure the consequence; this checks -// the mechanism, and needs no native build. (A right-nested revert is linear in +// concatenations. The runtime slope guards above measure the consequence. This checks +// the mechanism and needs no native build. (A right-nested revert is linear in // cell count too, since each buffer is a single cell, so the slope guards alone -// cannot see it; this static check is what does.) +// only this static check catches it.) #[test] fn container_encoder_threads_the_builder_fold() { let src = perf_src_n(PERF_WIRE_ENCODE, 8); @@ -605,6 +900,62 @@ fn each_update_reuses_uniquely_owned() { ); } +const PERF_BORROWED_WALK: &str = include_str!("../cases/perf/borrowed_walk.pr"); +// The probe walks a 1000-cell list 20 times; without borrowing that is one +// retain per level per pass, so half of it is a generous floor. +const BORROWED_WALK_PAIR_FLOOR: i64 = 10_000; +// Cell traffic the borrowed build may still carry: the list teardown plus +// whatever the prelude print path touches. +const BORROWED_WALK_CELL_SLACK: i64 = 8; + +// Borrow inference must remove reference-count pairs, not relocate them: a +// read-only walk over a shared list threads no retains and no releases on +// cells beyond the structure's own teardown. The inference-off build keeps the +// ceiling honest: the identical program pays a pair per level per pass without +// borrowing, so a probe that stops exercising RC pressure fails the floor +// instead of passing the ceiling vacuously. Both builds pin the flag +// explicitly, so the assertion is independent of the configured default. +#[test] +fn borrowed_walk_threads_no_rc_pairs() { + require_cc(); + let full = prism::with_prelude(PERF_BORROWED_WALK); + let suffixes = &["rc increments on cells", "rc decrements on cells"]; + let build = |infer: bool| { + move |src: &str, bin: &Path| { + let mut cfg = prism::Config::from_env(); + cfg.flags.borrow_infer = infer; + prism::build_on(src, &prism::default_roots(Path::new(".")), bin, &cfg) + } + }; + let on = stat_build_many( + &full, + "borrowed_walk_on", + "PRISM_RC_STATS", + suffixes, + build(true), + ) + .unwrap_or_else(|e| panic!("{e}")); + let off = stat_build_many( + &full, + "borrowed_walk_off", + "PRISM_RC_STATS", + suffixes, + build(false), + ) + .unwrap_or_else(|e| panic!("{e}")); + assert!( + off[0] >= BORROWED_WALK_PAIR_FLOOR, + "the walk probe lost its RC pressure ({} cell retains without borrow inference, floor {BORROWED_WALK_PAIR_FLOOR}); the borrowed ceiling would pass vacuously", + off[0] + ); + assert!( + on[0] <= BORROWED_WALK_CELL_SLACK && on[1] <= BORROWED_WALK_CELL_SLACK, + "a borrowed read-only walk still threads cell RC traffic ({} retains, {} releases, bound {BORROWED_WALK_CELL_SLACK}); borrowing is relocating pairs instead of removing them", + on[0], + on[1] + ); +} + #[test] fn fbip_reuse_fires_at_runtime() { require_cc(); @@ -1014,8 +1365,8 @@ fn tier_manifest_holds_on_compiler_stack() { match golden.get(label) { Some(want) if want == tier => {} Some(want) if matches!((rank(want), rank(tier)), (Some(a), Some(b)) if b > a) => { - // Name the functions that lost fusion so the failure points at the - // handler to investigate, not just the program. + // Name the functions that lost fusion so the failure identifies + // the handler to investigate. let culprits = prism::effect_warnings_full(&crate::support::source(&root.join(label)), root) .unwrap_or_default(); diff --git a/tests/native/symbol_namespace.rs b/tests/native/symbol_namespace.rs index 21a749bf..535c0a30 100644 --- a/tests/native/symbol_namespace.rs +++ b/tests/native/symbol_namespace.rs @@ -24,7 +24,7 @@ // codegen dispatcher before the binary is built. Those two guards are what keep // the gate from passing vacuously if inlining later swallows the case. // -// Note that `prismlam_{tag}` needs no case here: tags are content hashes of the +// `prismlam_{tag}` needs no case here: tags are content hashes of the // owning function, never small integers, so no plausible source identifier spells // one. The prefix split makes it impossible rather than merely improbable, but // there is no program to regress against. diff --git a/tests/native/tier_cross.rs b/tests/native/tier_cross.rs index e8656fd6..e450173a 100644 --- a/tests/native/tier_cross.rs +++ b/tests/native/tier_cross.rs @@ -19,9 +19,9 @@ use prism::{build_on, default_roots, Config, EffectTier, ObservationTrace, Root} use super::{effect_plan, forced}; use crate::support::{ - canonical_process_exit, cleanup_bin, corpus_is_sharded, heavy_corpus_delegated, leak_free, - parallel_check, program_stderr, require_cc, sharded_corpus, source, temp_bin, with_gate_cache, - CHECK_LEAKS, + canonical_process_exit, check_native_parity, cleanup_bin, corpus_is_sharded, + heavy_corpus_delegated, leak_free, parallel_check, program_stderr, require_cc, sharded_corpus, + source, temp_bin, with_gate_cache, CHECK_LEAKS, }; /// Gate-cache tag for a cross-tier verdict: one marker covers the whole grid @@ -36,6 +36,18 @@ const FIXTURE_CASES: &[&str] = &[ "tests/fixtures/tier_cross/non_ascii.pr", "tests/fixtures/tier_cross/byte_seams.pr", "tests/fixtures/tier_cross/thunk_param.pr", + "tests/fixtures/tier_cross/convention_split_map.pr", + "tests/fixtures/tier_cross/convention_split_map_unrolled.pr", +]; + +/// The convention-split three-way control. All three receive ordinary native +/// parity and leak checking; the mixed and unrolled cases additionally sit in +/// `FIXTURE_CASES`, because their plans must move under tier forcing. The pure +/// control intentionally has only one plan, pinned by the compiler test. +const CONVENTION_CONTROL_CASES: &[&str] = &[ + "tests/fixtures/tier_cross/convention_split_map_pure.pr", + "tests/fixtures/tier_cross/convention_split_map.pr", + "tests/fixtures/tier_cross/convention_split_map_unrolled.pr", ]; const MIN_DISTINCT_TIER_PLANS: usize = 2; @@ -249,3 +261,29 @@ fn tiers_match_each_other_on_adversarial_fixtures() { fails.join("\n") ); } + +#[test] +fn convention_split_controls_match_the_interpreter_and_do_not_leak() { + require_cc(); + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let roots = default_roots(Path::new(".")); + let mut cfg = Config::from_env(); + cfg.flags.compiler_cache = false; + cfg.flags.quiet = true; + let cases: Vec = CONVENTION_CONTROL_CASES + .iter() + .map(|case| root.join(case)) + .collect(); + let fails = parallel_check(&cases, |case| { + check_native_parity(case, "convention-control", |source, output| { + build_on(source, &roots, output, &cfg) + }) + }); + assert!( + fails.is_empty(), + "{} of {} convention controls failed native parity/leak:\n{}", + fails.len(), + cases.len(), + fails.join("\n") + ); +} diff --git a/tests/package/receipt.rs b/tests/package/receipt.rs new file mode 100644 index 00000000..ae47cc3c --- /dev/null +++ b/tests/package/receipt.rs @@ -0,0 +1,235 @@ +//! The shadow-parser comparison receipt: its codec, its anti-vacuity refusals, +//! and the two store layers it lands in. +//! +//! The property the tests exist for is the one the receipt is built around: the +//! deterministic half is content-addressed, so re-emitting an unchanged +//! comparison is a hit and re-emitting a changed one is an alarm, while the +//! machine readings stay in the mutable layer where a second run is allowed to +//! differ. + +use std::collections::BTreeMap; +use std::time::Duration; + +use prism::core::work::WorkCounts; +use prism::store::cert::{check_cert, CertStatus, CLAIM_SHADOW_PARSE_AGREED_NAME}; +use prism::store::disk::Written; +use prism::store::receipt::{self, Comparison, ReceiptError, ShadowReceipt}; +use prism::store::CodecError; +use prism::PhaseTally; +use rstest::rstest; + +use crate::support::TempDir; + +const AUTHORITY: &str = "parser-rust@6f1d2c"; +const SHADOW: &str = "parser-prism@a19e04"; +const CORPUS: &str = "corpus@3b7c55"; +const SYNTAX_HASH: &str = "syntax@e40012"; +const CORE_HASH: &str = "core@771ab3"; + +fn agreeing() -> Comparison { + Comparison { + authority: AUTHORITY.to_string(), + shadow: SHADOW.to_string(), + corpus: CORPUS.to_string(), + corpus_files: 42, + syntax_hash_authority: SYNTAX_HASH.to_string(), + syntax_hash_shadow: SYNTAX_HASH.to_string(), + core_hash: CORE_HASH.to_string(), + divergences: 0, + } +} + +const fn tally(invocations: usize, ms: u64, visits: u64, rebuilt: u64, depth: u64) -> PhaseTally { + PhaseTally { + invocations, + wall: Duration::from_millis(ms), + work: WorkCounts { + visits, + rebuilt, + max_depth: depth, + }, + } +} + +// A plausible shape: a front-end phase that charges nothing on the Core counters, +// and two Core phases that do. +fn tallies() -> BTreeMap<&'static str, PhaseTally> { + BTreeMap::from([ + ("parse", tally(2, 3, 0, 0, 0)), + ("elaborate", tally(2, 20, 312, 0, 3)), + ("opt.pre", tally(1, 15, 7801, 7645, 115)), + ]) +} + +fn receipt() -> ShadowReceipt { + ShadowReceipt::new(agreeing(), &tallies(), &["opt.pre"]).unwrap() +} + +#[test] +fn round_trips_through_the_envelope() { + let r = receipt(); + assert_eq!(receipt::decode(&receipt::encode(&r)), Ok(r)); +} + +#[test] +fn drops_phases_that_charged_nothing_and_keeps_those_that_did() { + let r = receipt(); + // `parse` works on the AST and never charges the Core counters; a row of zeros + // beside rows that mean something would read as a measurement. + assert!(!r.phases.contains_key("parse")); + let opt = r.phases.get("opt.pre").expect("an exercised phase"); + assert_eq!((opt.invocations, opt.visits, opt.rebuilt), (1, 7801, 7645)); + // Elaboration charges for its handler scans and rebuilds nothing, which is a + // fact about the phase rather than a silent instrument. + assert_eq!(r.phases["elaborate"].rebuilt, 0); + assert_eq!(r.max_depth, 115); +} + +#[rstest] +// A phase the caller named as exercised but whose counter stayed zero: either it +// did not run or the instrument did not see it, and a receipt may claim neither. +#[case(&["parse"], ReceiptError::VacuousPhase("parse".to_string()))] +// A phase that is not in the tallies at all reads the same way. +#[case(&["rc"], ReceiptError::VacuousPhase("rc".to_string()))] +fn refuses_to_claim_a_phase_it_did_not_exercise( + #[case] exercised: &[&str], + #[case] expected: ReceiptError, +) { + assert_eq!( + ShadowReceipt::new(agreeing(), &tallies(), exercised), + Err(expected) + ); +} + +#[test] +fn refuses_an_empty_corpus_and_a_run_that_exercised_nothing() { + let mut empty_corpus = agreeing(); + empty_corpus.corpus_files = 0; + assert_eq!( + ShadowReceipt::new(empty_corpus, &tallies(), &[]), + Err(ReceiptError::EmptyCorpus) + ); + let silent = BTreeMap::from([("parse", tally(1, 3, 0, 0, 0))]); + assert_eq!( + ShadowReceipt::new(agreeing(), &silent, &[]), + Err(ReceiptError::NoPhases) + ); +} + +#[test] +fn identity_covers_the_compared_inputs_and_not_the_counters() { + let base = receipt(); + + // Different work over the same inputs is the same comparison, so it must + // collide rather than land under a subject nobody would look for. + let mut moved = tallies(); + moved.insert("opt.pre", tally(1, 15, 9000, 8800, 115)); + let heavier = ShadowReceipt::new(agreeing(), &moved, &["opt.pre"]).unwrap(); + assert_eq!(heavier.subject, base.subject); + assert_ne!(receipt::encode(&heavier), receipt::encode(&base)); + + // A different corpus is a different comparison. + let mut other = agreeing(); + other.corpus = "corpus@ffffff".to_string(); + let elsewhere = ShadowReceipt::new(other, &tallies(), &["opt.pre"]).unwrap(); + assert_ne!(elsewhere.subject, base.subject); +} + +#[test] +fn an_unchanged_rerun_is_a_hit_and_a_moved_counter_is_an_alarm() { + let dir = TempDir::new("receipt", "emit"); + let store = prism::store::disk::Store::open_or_create(dir.store_root()).unwrap(); + let r = receipt(); + + assert_eq!(receipt::emit(&store, &r).unwrap(), Written::New); + // The reproduction proof: the same parser over the same corpus, re-attested. + assert_eq!(receipt::emit(&store, &r).unwrap(), Written::Hit); + + let mut moved = tallies(); + moved.insert("opt.pre", tally(1, 15, 9000, 8800, 115)); + let heavier = ShadowReceipt::new(agreeing(), &moved, &["opt.pre"]).unwrap(); + // Same inputs, different work. The immutable layer refuses it, which is the + // event the receipt exists to catch and not a bug to route around. + assert!(receipt::emit(&store, &heavier).is_err()); + + assert_eq!(receipt::get(&store, &r.subject).unwrap(), Some(Ok(r))); +} + +#[test] +fn check_reports_agreement_divergence_and_absence_distinctly() { + let dir = TempDir::new("receipt", "check"); + let store = prism::store::disk::Store::open_or_create(dir.store_root()).unwrap(); + + let r = receipt(); + assert!(matches!( + receipt::check(&store, &r.subject), + CertStatus::Absent + )); + receipt::emit(&store, &r).unwrap(); + let CertStatus::Verified(note) = receipt::check(&store, &r.subject) else { + panic!("an agreeing receipt verifies"); + }; + assert!(note.contains(CLAIM_SHADOW_PARSE_AGREED_NAME) && note.contains("42")); + + let mut split = agreeing(); + split.syntax_hash_shadow = "syntax@000bad".to_string(); + split.divergences = 3; + let diverged = ShadowReceipt::new(split, &tallies(), &["opt.pre"]).unwrap(); + receipt::emit(&store, &diverged).unwrap(); + let CertStatus::Unverifiable(note) = receipt::check(&store, &diverged.subject) else { + panic!("a divergent receipt is reported, never dressed up as a pass"); + }; + assert!(note.contains('3')); +} + +#[test] +fn a_foreign_claim_reader_names_it_without_verifying_it() { + let dir = TempDir::new("receipt", "foreign"); + let store = prism::store::disk::Store::open_or_create(dir.store_root()).unwrap(); + let r = receipt(); + receipt::emit(&store, &r).unwrap(); + // The one global claim number space at work: the parity reader decodes the + // envelope and reports the claim as recognized rather than as corruption. + let CertStatus::Unverifiable(note) = check_cert(&store, &r.subject) else { + panic!("a receipt is well-formed to the parity reader, just not its claim"); + }; + assert!(note.contains(CLAIM_SHADOW_PARSE_AGREED_NAME)); +} + +#[test] +fn every_decode_is_total() { + let good = receipt::encode(&receipt()); + // Every strict prefix, the empty slice included, is an error and not a panic. + for cut in 0..good.len() { + assert!(receipt::decode(&good[..cut]).is_err(), "prefix of {cut}"); + } + let mut trailing = good; + trailing.push(0); + assert_eq!(receipt::decode(&trailing), Err(CodecError::TrailingBytes)); +} + +#[test] +fn timing_is_recorded_where_a_second_run_may_differ() { + let dir = TempDir::new("receipt", "timing"); + let store = prism::store::disk::Store::open_or_create(dir.store_root()).unwrap(); + let r = receipt(); + receipt::emit(&store, &r).unwrap(); + + assert!(receipt::get_timing(&store, &r.subject).unwrap().is_empty()); + receipt::put_timing(&store, &r.subject, &tallies()).unwrap(); + let rows = receipt::get_timing(&store, &r.subject).unwrap(); + assert_eq!(rows.len(), 3); + assert_eq!(rows[0].0, "elaborate"); + assert_eq!(rows[0].1, 2); + assert_eq!(rows[0].2, Duration::from_millis(20)); + + // Two correct runs of the same comparison disagree here, and the layer takes + // it. These readings therefore do not belong in the certificate. + let mut slower = tallies(); + slower.insert("elaborate", tally(2, 97, 312, 0, 3)); + receipt::put_timing(&store, &r.subject, &slower).unwrap(); + let rows = receipt::get_timing(&store, &r.subject).unwrap(); + assert_eq!(rows[0].2, Duration::from_millis(97)); + // The attested half is untouched by the reading that moved. + assert_eq!(receipt::get(&store, &r.subject).unwrap(), Some(Ok(r))); +} diff --git a/tests/runtime/kont_suspend.rs b/tests/runtime/kont_suspend.rs index 829a1dca..12669bb7 100644 --- a/tests/runtime/kont_suspend.rs +++ b/tests/runtime/kont_suspend.rs @@ -49,8 +49,7 @@ fn uninterrupted(full: &str) -> String { fn suspend_and_resume_reproduces_output_at_every_cut() { // Compile once and drive the interpreter directly (recompiling per budget is // needless here), but still round-trip every snapshot through the `kont` - // codec's bytes so this exercises encode + decode at every cut, not just the - // in-memory continuation. + // codec's bytes so this exercises encoding and decoding at every cut. let core = core_of(COUNTER).expect("compile"); let want = uninterrupted(&with_prelude(COUNTER)); let mut observed_out = Vec::new(); diff --git a/tests/snapshots.rs b/tests/snapshots.rs index e802d880..1ba0d27e 100644 --- a/tests/snapshots.rs +++ b/tests/snapshots.rs @@ -323,6 +323,38 @@ fn recursive_fip_examples_lower_to_loops() { ); } +// A loop must stay a loop no matter what the reference-count passes decide. A +// borrowed parameter is released by the frame that made the call, after the +// call returns, so borrowing a position that a loop-eligible self-call hands a +// freshly built value would put work after the jump and cost the frame its tail +// position: the loop silently becomes one stack frame per iteration and a deep +// enough input exhausts the stack. `scan` is that shape. Its first parameter is +// only ever scrutinised, which is what makes it a borrow candidate, and its +// self-call passes a cell the frame itself built. +#[cfg(feature = "native")] +#[test] +fn a_borrowable_parameter_never_costs_a_loop_its_tail_call() { + let src = prism::with_prelude( + "fn peek(s) =\n match s of\n Nil => 0\n Cons(a, _b) => a\n\ + fn scan(cur, xs) =\n match xs of\n Nil => peek(cur)\n Cons(h, t) =>\n \ + let step = Cons(h + peek(cur), Nil)\n scan(step, t)\n\ + fn main() = println(scan([0], [1, 2, 3]))", + ); + let ir = prism::emit_ir(&src).expect("scan must compile"); + let scan = prism::codegen::native_symbol("scan"); + let start = ir + .find(&format!("define i64 @{scan}(")) + .expect("scan must be emitted"); + let rest = &ir[start..]; + let block = &rest[..rest.find("\n}").map_or(rest.len(), |e| e + 2)]; + let total = block.matches(&format!("call i64 @{scan}")).count(); + let tail = block.matches(&format!("musttail call i64 @{scan}")).count(); + assert!( + tail >= 1 && total == tail, + "scan must loop, not grow the stack once per element:\n{block}" + ); +} + // Higher-order effect inference: a function's row must account for effects // performed by applying its function-typed arguments. `apply` propagates its // argument's row into its own, and an effect routed through `apply` (an opaque @@ -985,8 +1017,8 @@ fn free_monad_fallback_warns() { // Optimization coverage requirement. Two guarantees the per-program snapshot // cannot give on its own: (1) breadth -- every named fast path keeps at least one -// live witness in the corpus, so silently losing a whole optimization fails here, -// not just shifts a snapshot line; and (2) the basic-loop invariant -- a canonical +// live witness in the corpus, so losing a whole optimization fails here instead +// of shifting a snapshot line. It also checks the basic-loop invariant: a canonical // `var` while-loop must NOT classify as a free-monad strategy, because imperative // loops have to compile to constant-stack, allocation-free loops. (2) is the // requirement whose absence let the var-loop regression ship; it fails until the @@ -1050,8 +1082,8 @@ fn fmt_idempotent_on_compiler_stack() { } } -// Formatting must preserve meaning, not just be a fixpoint: the desugared core -// of the formatted source has to match the original's. This is what catches a +// Formatting must preserve meaning. The desugared core of the formatted source +// has to match the original's. This catches a // sugar marker that round-trips to the wrong tree, which idempotency cannot see. #[test] fn fmt_preserves_core() { diff --git a/tests/snapshots/base_surface__base_export_surface.snap b/tests/snapshots/base_surface__base_export_surface.snap index dba5dde3..bec7de15 100644 --- a/tests/snapshots/base_surface__base_export_surface.snap +++ b/tests/snapshots/base_surface__base_export_surface.snap @@ -192,6 +192,7 @@ value Data.String.starts_with value Data.String.str_join value Data.String.str_of_char value Data.String.str_repeat +value Data.String.str_slice value Data.String.to_lower value Data.String.to_upper value Data.String.trim diff --git a/tests/snapshots/snapshots__effect_strategy_manifest.snap b/tests/snapshots/snapshots__effect_strategy_manifest.snap index 935731d4..0663543c 100644 --- a/tests/snapshots/snapshots__effect_strategy_manifest.snap +++ b/tests/snapshots/snapshots__effect_strategy_manifest.snap @@ -20,7 +20,7 @@ examples/eff_amb.pr: selective-free-monad examples/eff_exn.pr: selective-free-monad examples/eff_forward.pr: selective-free-monad examples/eff_nontail.pr: selective-free-monad -examples/eff_poly.pr: whole-program-free-monad +examples/eff_poly.pr: selective-free-monad examples/eff_reader.pr: evidence examples/eff_rows.pr: selective-free-monad examples/eff_state.pr: state-fusion @@ -59,9 +59,9 @@ examples/queens.pr: selective-free-monad examples/record_replay.pr: whole-program-free-monad examples/replay_concurrent.pr: whole-program-free-monad examples/result_pipeline.pr: selective-free-monad -examples/same_fringe.pr: selective-free-monad +examples/same_fringe.pr: whole-program-free-monad examples/sandbox.pr: evidence -examples/scheduler.pr: selective-free-monad +examples/scheduler.pr: whole-program-free-monad examples/scheduler_policy.pr: whole-program-free-monad examples/stlc.pr: selective-free-monad examples/streams.pr: state-fusion @@ -97,22 +97,26 @@ tests/cases/run/comp_in_annotated_row.pr: selective-free-monad tests/cases/run/comp_map_once.pr: state-fusion tests/cases/run/constrained_mutual.pr: evidence tests/cases/run/control_effects.pr: evidence -tests/cases/run/control_validate.pr: whole-program-free-monad +tests/cases/run/control_validate.pr: selective-free-monad tests/cases/run/curry_effect.pr: evidence tests/cases/run/deep_effect_recursion.pr: state-fusion tests/cases/run/default_or.pr: selective-free-monad tests/cases/run/deriving_json.pr: selective-free-monad -tests/cases/run/deriving_plate.pr: whole-program-free-monad +tests/cases/run/deriving_plate.pr: selective-free-monad tests/cases/run/eff_closure_arg.pr: selective-free-monad tests/cases/run/eff_eta_thunk.pr: selective-free-monad tests/cases/run/eff_fn_list.pr: whole-program-free-monad tests/cases/run/eff_fuse.pr: evidence tests/cases/run/eff_pending_arg.pr: evidence tests/cases/run/eff_poly_fn_arg.pr: whole-program-free-monad +tests/cases/run/eff_poly_handler_install.pr: evidence tests/cases/run/eff_row_forward.pr: evidence +tests/cases/run/eff_row_unwitnessed.pr: whole-program-free-monad tests/cases/run/eff_two_handlers.pr: evidence tests/cases/run/effop_tax.pr: evidence tests/cases/run/errors.pr: selective-free-monad +tests/cases/run/evidence_residual_row_after_handle.pr: evidence +tests/cases/run/evidence_residual_row_clause.pr: evidence tests/cases/run/fail_guard.pr: selective-free-monad tests/cases/run/final_ctl.pr: selective-free-monad tests/cases/run/flat_array.pr: selective-free-monad @@ -126,6 +130,7 @@ tests/cases/run/fs_bytes.pr: evidence tests/cases/run/grade_ops.pr: selective-free-monad tests/cases/run/handler_arms_answer_apart.pr: selective-free-monad tests/cases/run/handler_funval.pr: evidence +tests/cases/run/handler_implicit_return.pr: selective-free-monad tests/cases/run/handler_partial_forward.pr: selective-free-monad tests/cases/run/incr_diff.pr: whole-program-free-monad tests/cases/run/indexing_ops.pr: selective-free-monad @@ -138,7 +143,11 @@ tests/cases/run/law_reader.pr: whole-program-free-monad tests/cases/run/law_state.pr: whole-program-free-monad tests/cases/run/law_ufp.pr: whole-program-free-monad tests/cases/run/local_mono_combined.pr: local-partial +tests/cases/run/local_mono_effectful_helper.pr: local-partial tests/cases/run/local_mono_escape.pr: whole-program-free-monad +tests/cases/run/local_mono_nontail_resume.pr: local-partial +tests/cases/run/local_mono_state_rest.pr: local-partial +tests/cases/run/local_mono_two_entries.pr: local-partial tests/cases/run/mask.pr: whole-program-free-monad tests/cases/run/multishot_thunk_param.pr: whole-program-free-monad tests/cases/run/named_handlers.pr: selective-free-monad @@ -160,6 +169,7 @@ tests/cases/run/reified_under_world.pr: whole-program-free-monad tests/cases/run/replayable_ok.pr: evidence tests/cases/run/rewrite_strategies.pr: whole-program-free-monad tests/cases/run/rollback.pr: selective-free-monad +tests/cases/run/row_widen_named_effect_list.pr: whole-program-free-monad tests/cases/run/row_widen_task_list.pr: whole-program-free-monad tests/cases/run/row_widen_user_effect.pr: whole-program-free-monad tests/cases/run/scc_effect_row.pr: evidence diff --git a/tests/snapshots/snapshots__interpreter@bytes_view.pr.snap b/tests/snapshots/snapshots__interpreter@bytes_view.pr.snap new file mode 100644 index 00000000..a69d4729 --- /dev/null +++ b/tests/snapshots/snapshots__interpreter@bytes_view.pr.snap @@ -0,0 +1,22 @@ +--- +source: tests/snapshots.rs +expression: interp_output(path) +input_file: tests/cases/run/bytes_view.pr +--- +whole: [68656c6c6f207769646520776f726c64] len=16 +middle: [77696465] len=4 +start negative: [68656c6c] len=4 +start past end: [] len=0 +len negative: [] len=0 +len past end: [776f726c64] len=5 +empty span: [] len=0 +nested: [207769646520] len=6 +peeled: [68656c6c6f207769646520776f726c64] len=16 +peeled text: [hello wide world] +eq=true cmp=0 +hash=true +concat: [77696465776f726c64] len=9 +base64 roundtrip: [wide] +raw window: [ff10fe80] len=4 +raw window text: +=> () diff --git a/tests/snapshots/snapshots__interpreter@eff_poly_fn_arg.pr.snap b/tests/snapshots/snapshots__interpreter@eff_poly_fn_arg.pr.snap index 29e22461..1ab5cbde 100644 --- a/tests/snapshots/snapshots__interpreter@eff_poly_fn_arg.pr.snap +++ b/tests/snapshots/snapshots__interpreter@eff_poly_fn_arg.pr.snap @@ -4,4 +4,8 @@ expression: interp_output(path) input_file: tests/cases/run/eff_poly_fn_arg.pr --- [(0, 10), (1, 20), (2, 30)] +cell read +cell read +cell read +[1, 2, 3] => () diff --git a/tests/snapshots/snapshots__interpreter@eff_poly_handler_install.pr.snap b/tests/snapshots/snapshots__interpreter@eff_poly_handler_install.pr.snap new file mode 100644 index 00000000..f14092d1 --- /dev/null +++ b/tests/snapshots/snapshots__interpreter@eff_poly_handler_install.pr.snap @@ -0,0 +1,7 @@ +--- +source: tests/snapshots.rs +expression: interp_output(path) +input_file: tests/cases/run/eff_poly_handler_install.pr +--- +((), 3) +=> () diff --git a/tests/snapshots/snapshots__interpreter@eff_row_unwitnessed.pr.snap b/tests/snapshots/snapshots__interpreter@eff_row_unwitnessed.pr.snap new file mode 100644 index 00000000..8e376f2a --- /dev/null +++ b/tests/snapshots/snapshots__interpreter@eff_row_unwitnessed.pr.snap @@ -0,0 +1,7 @@ +--- +source: tests/snapshots.rs +expression: interp_output(path) +input_file: tests/cases/run/eff_row_unwitnessed.pr +--- +4 +=> () diff --git a/tests/snapshots/snapshots__interpreter@evidence_residual_row_after_handle.pr.snap b/tests/snapshots/snapshots__interpreter@evidence_residual_row_after_handle.pr.snap new file mode 100644 index 00000000..b54c9328 --- /dev/null +++ b/tests/snapshots/snapshots__interpreter@evidence_residual_row_after_handle.pr.snap @@ -0,0 +1,6 @@ +--- +source: tests/snapshots.rs +expression: interp_output(path) +input_file: tests/cases/run/evidence_residual_row_after_handle.pr +--- +((), 3)=> () diff --git a/tests/snapshots/snapshots__interpreter@evidence_residual_row_clause.pr.snap b/tests/snapshots/snapshots__interpreter@evidence_residual_row_clause.pr.snap new file mode 100644 index 00000000..842f9256 --- /dev/null +++ b/tests/snapshots/snapshots__interpreter@evidence_residual_row_clause.pr.snap @@ -0,0 +1,6 @@ +--- +source: tests/snapshots.rs +expression: interp_output(path) +input_file: tests/cases/run/evidence_residual_row_clause.pr +--- +((), 3)=> () diff --git a/tests/snapshots/snapshots__interpreter@field_projection_single.pr.snap b/tests/snapshots/snapshots__interpreter@field_projection_single.pr.snap new file mode 100644 index 00000000..ced281da --- /dev/null +++ b/tests/snapshots/snapshots__interpreter@field_projection_single.pr.snap @@ -0,0 +1,7 @@ +--- +source: tests/snapshots.rs +expression: interp_output(path) +input_file: tests/cases/run/field_projection_single.pr +--- +22 +=> () diff --git a/tests/snapshots/snapshots__interpreter@handler_implicit_return.pr.snap b/tests/snapshots/snapshots__interpreter@handler_implicit_return.pr.snap new file mode 100644 index 00000000..b9f2892b --- /dev/null +++ b/tests/snapshots/snapshots__interpreter@handler_implicit_return.pr.snap @@ -0,0 +1,8 @@ +--- +source: tests/snapshots.rs +expression: interp_output(path) +input_file: tests/cases/run/handler_implicit_return.pr +--- +42 +9 +=> () diff --git a/tests/snapshots/snapshots__interpreter@local_mono_effectful_helper.pr.snap b/tests/snapshots/snapshots__interpreter@local_mono_effectful_helper.pr.snap new file mode 100644 index 00000000..c4a4a73f --- /dev/null +++ b/tests/snapshots/snapshots__interpreter@local_mono_effectful_helper.pr.snap @@ -0,0 +1,8 @@ +--- +source: tests/snapshots.rs +expression: interp_output(path) +input_file: tests/cases/run/local_mono_effectful_helper.pr +--- +3045 +34 +=> () diff --git a/tests/snapshots/snapshots__interpreter@local_mono_nontail_resume.pr.snap b/tests/snapshots/snapshots__interpreter@local_mono_nontail_resume.pr.snap new file mode 100644 index 00000000..0d0b6410 --- /dev/null +++ b/tests/snapshots/snapshots__interpreter@local_mono_nontail_resume.pr.snap @@ -0,0 +1,8 @@ +--- +source: tests/snapshots.rs +expression: interp_output(path) +input_file: tests/cases/run/local_mono_nontail_resume.pr +--- +2450 +40 +=> () diff --git a/tests/snapshots/snapshots__interpreter@local_mono_state_rest.pr.snap b/tests/snapshots/snapshots__interpreter@local_mono_state_rest.pr.snap new file mode 100644 index 00000000..c914f1fe --- /dev/null +++ b/tests/snapshots/snapshots__interpreter@local_mono_state_rest.pr.snap @@ -0,0 +1,8 @@ +--- +source: tests/snapshots.rs +expression: interp_output(path) +input_file: tests/cases/run/local_mono_state_rest.pr +--- +3 +12 +=> () diff --git a/tests/snapshots/snapshots__interpreter@local_mono_two_entries.pr.snap b/tests/snapshots/snapshots__interpreter@local_mono_two_entries.pr.snap new file mode 100644 index 00000000..ada606c1 --- /dev/null +++ b/tests/snapshots/snapshots__interpreter@local_mono_two_entries.pr.snap @@ -0,0 +1,10 @@ +--- +source: tests/snapshots.rs +expression: interp_output(path) +input_file: tests/cases/run/local_mono_two_entries.pr +--- +2340 +32 +34 +66 +=> () diff --git a/tests/snapshots/snapshots__interpreter@record_pattern_rest.pr.snap b/tests/snapshots/snapshots__interpreter@record_pattern_rest.pr.snap new file mode 100644 index 00000000..def02c3f --- /dev/null +++ b/tests/snapshots/snapshots__interpreter@record_pattern_rest.pr.snap @@ -0,0 +1,12 @@ +--- +source: tests/snapshots.rs +expression: interp_output(path) +input_file: tests/cases/run/record_pattern_rest.pr +--- +1 +2 +7 +2.5 +9 +3.5 +=> () diff --git a/tests/snapshots/snapshots__interpreter@row_widen_named_effect_list.pr.snap b/tests/snapshots/snapshots__interpreter@row_widen_named_effect_list.pr.snap new file mode 100644 index 00000000..72099ed9 --- /dev/null +++ b/tests/snapshots/snapshots__interpreter@row_widen_named_effect_list.pr.snap @@ -0,0 +1,7 @@ +--- +source: tests/snapshots.rs +expression: interp_output(path) +input_file: tests/cases/run/row_widen_named_effect_list.pr +--- +(2, 4) +=> () diff --git a/tests/snapshots/snapshots__interpreter@shadowed_field_binder.pr.snap b/tests/snapshots/snapshots__interpreter@shadowed_field_binder.pr.snap new file mode 100644 index 00000000..5245db38 --- /dev/null +++ b/tests/snapshots/snapshots__interpreter@shadowed_field_binder.pr.snap @@ -0,0 +1,8 @@ +--- +source: tests/snapshots.rs +expression: interp_output(path) +input_file: tests/cases/run/shadowed_field_binder.pr +--- +10 +10 +=> () diff --git a/tests/snapshots/snapshots__interpreter@str_view.pr.snap b/tests/snapshots/snapshots__interpreter@str_view.pr.snap new file mode 100644 index 00000000..f43b8edc --- /dev/null +++ b/tests/snapshots/snapshots__interpreter@str_view.pr.snap @@ -0,0 +1,30 @@ +--- +source: tests/snapshots.rs +expression: interp_output(path) +input_file: tests/cases/run/str_view.pr +--- +whole: [the quick brown fox jumps over the lazy dog] len=43 +mid: [quick] len=5 +empty: [] len=0 +reversed: [] len=0 +clamped_hi: [azy dog] len=7 +clamped_lo: [the] len=3 +both_out: [] len=0 +nested: [uick] len=4 +eaten: [quick brown] len=11 +uni_aligned: [naive] len=5 +uni_split_hi: [naive cafe: �] len=15 +uni_split_lo: [�nïc] len=7 +view_trim: [quick] len=5 +view_concat: [the/quick] len=9 +view_eq: [true] len=4 +view_cmp: [0] len=1 +view_upper: [QUICK] len=5 +view_index: [6] len=1 +view_char_at: [110] len=3 +view_len: [4] len=1 +view_sub: [nï] len=3 +view_parse_float: [3.14] len=4 +view_parse_cut: [3.1] len=3 +view_parse_int: [Some(123)] len=9 +=> () diff --git a/tests/snapshots/snapshots__interpreter@wire_hostile.pr.snap b/tests/snapshots/snapshots__interpreter@wire_hostile.pr.snap index 9563a894..0a4bcdd0 100644 --- a/tests/snapshots/snapshots__interpreter@wire_hostile.pr.snap +++ b/tests/snapshots/snapshots__interpreter@wire_hostile.pr.snap @@ -11,4 +11,7 @@ wrong digest rejected: OK truncated body rejected: OK hostile length rejected: OK descending map rejected: OK +varint at cap accepted: OK +overlong varint rejected: OK +long continuation run rejected: OK => () diff --git a/tests/snapshots/snapshots__pipeline@annot_effect_unused.pr.snap b/tests/snapshots/snapshots__pipeline@annot_effect_unused.pr.snap index b5d98468..271bca12 100644 --- a/tests/snapshots/snapshots__pipeline@annot_effect_unused.pr.snap +++ b/tests/snapshots/snapshots__pipeline@annot_effect_unused.pr.snap @@ -180,8 +180,8 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@prism_native_kont_table = constant [828 x i8] c"scheme prism-core-hash-v2\0Abundle 00678d80800af0364c5390630e17dd5b2df5697496dcf729248f3862c2e95615\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:00678d80800af0364c5390630e17dd5b2df5697496dcf729248f3862c2e95615\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_h 0ac7c9ad242c723285ef6a5e65f10fbab44504860894322bcc117a433827ccfb h\0Afn prismfn_main e12056bf40db52bb7655ea47b058168fbc901d4af20d7995337a703c9776b7bf main\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [919 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 00678d80800af0364c5390630e17dd5b2df5697496dcf729248f3862c2e95615\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:00678d80800af0364c5390630e17dd5b2df5697496dcf729248f3862c2e95615\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_h 0ac7c9ad242c723285ef6a5e65f10fbab44504860894322bcc117a433827ccfb h arity 0 slots abi-word[]\0Astate prismfn_main e12056bf40db52bb7655ea47b058168fbc901d4af20d7995337a703c9776b7bf main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [853 x i8] c"scheme prism-core-hash-v2\0Abundle 00678d80800af0364c5390630e17dd5b2df5697496dcf729248f3862c2e95615\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:00678d80800af0364c5390630e17dd5b2df5697496dcf729248f3862c2e95615\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_h 0ac7c9ad242c723285ef6a5e65f10fbab44504860894322bcc117a433827ccfb h\0Afn prismfn_main e12056bf40db52bb7655ea47b058168fbc901d4af20d7995337a703c9776b7bf main\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [944 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 00678d80800af0364c5390630e17dd5b2df5697496dcf729248f3862c2e95615\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:00678d80800af0364c5390630e17dd5b2df5697496dcf729248f3862c2e95615\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_h 0ac7c9ad242c723285ef6a5e65f10fbab44504860894322bcc117a433827ccfb h arity 0 slots abi-word[]\0Astate prismfn_main e12056bf40db52bb7655ea47b058168fbc901d4af20d7995337a703c9776b7bf main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 @.kont_symbol1 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash1 = private constant [65 x i8] c"e12056bf40db52bb7655ea47b058168fbc901d4af20d7995337a703c9776b7bf\00" @.kont_name1 = private constant [5 x i8] c"main\00" diff --git a/tests/snapshots/snapshots__pipeline@derive_hash.pr.snap b/tests/snapshots/snapshots__pipeline@derive_hash.pr.snap index 1795c39c..807cbcaf 100644 --- a/tests/snapshots/snapshots__pipeline@derive_hash.pr.snap +++ b/tests/snapshots/snapshots__pipeline@derive_hash.pr.snap @@ -262,10 +262,10 @@ fn hashPair() = ; ModuleID = 'prism' source_filename = "prism" -@.str0 = private constant [12 x i8] c"c6:MkPair/0\00" -@.str1 = private constant [2 x i8] c"i\00" -@prism_native_kont_table = constant [1173 x i8] c"scheme prism-core-hash-v2\0Abundle c6d9b32711aa132e15cc9ce9642e45bede46e848c8853a30ec14ee8f1a4447c9\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:c6d9b32711aa132e15cc9ce9642e45bede46e848c8853a30ec14ee8f1a4447c9\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_hashInt 288615e595b75457deb1197f6dab3fc00b98f963f5488b08ca89de595ea8b730 hashInt\0Afn prismfn_hashPair 88fbb3b674cc4fff879484d86a983747982a3fce130090e31a32d970f35f25bf hashPair\0Afn prismfn_iZahashIntZahash 0eabe45704c4040cd9f346fa1f49da315c6911b067a1d67f0bb14b60a891322a i@hashInt@hash\0Afn prismfn_iZahashPairZahash f8f813f0b1320a758d4109d7298ace29466040c2033c2b25b81933e1dfbcca02 i@hashPair@hash\0Afn prismfn_main 39e60a011a6803322d624c02f068d275f107995912c568fa085b08ae65792311 main\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [1111 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle c6d9b32711aa132e15cc9ce9642e45bede46e848c8853a30ec14ee8f1a4447c9\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:c6d9b32711aa132e15cc9ce9642e45bede46e848c8853a30ec14ee8f1a4447c9\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_iZahashIntZahash 0eabe45704c4040cd9f346fa1f49da315c6911b067a1d67f0bb14b60a891322a i@hashInt@hash arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_iZahashPairZahash f8f813f0b1320a758d4109d7298ace29466040c2033c2b25b81933e1dfbcca02 i@hashPair@hash arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_main 39e60a011a6803322d624c02f068d275f107995912c568fa085b08ae65792311 main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 +@.str0 = private constant { i64, i64, i64, [12 x i8] } { i64 1152921504606846976, i64 1398034944, i64 11, [12 x i8] c"c6:MkPair/0\00" }, align 8 +@.str1 = private constant { i64, i64, i64, [2 x i8] } { i64 1152921504606846976, i64 1398034944, i64 1, [2 x i8] c"i\00" }, align 8 +@prism_native_kont_table = constant [1198 x i8] c"scheme prism-core-hash-v2\0Abundle c6d9b32711aa132e15cc9ce9642e45bede46e848c8853a30ec14ee8f1a4447c9\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:c6d9b32711aa132e15cc9ce9642e45bede46e848c8853a30ec14ee8f1a4447c9\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_hashInt 288615e595b75457deb1197f6dab3fc00b98f963f5488b08ca89de595ea8b730 hashInt\0Afn prismfn_hashPair 88fbb3b674cc4fff879484d86a983747982a3fce130090e31a32d970f35f25bf hashPair\0Afn prismfn_iZahashIntZahash 0eabe45704c4040cd9f346fa1f49da315c6911b067a1d67f0bb14b60a891322a i@hashInt@hash\0Afn prismfn_iZahashPairZahash f8f813f0b1320a758d4109d7298ace29466040c2033c2b25b81933e1dfbcca02 i@hashPair@hash\0Afn prismfn_main 39e60a011a6803322d624c02f068d275f107995912c568fa085b08ae65792311 main\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [1136 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle c6d9b32711aa132e15cc9ce9642e45bede46e848c8853a30ec14ee8f1a4447c9\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:c6d9b32711aa132e15cc9ce9642e45bede46e848c8853a30ec14ee8f1a4447c9\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_iZahashIntZahash 0eabe45704c4040cd9f346fa1f49da315c6911b067a1d67f0bb14b60a891322a i@hashInt@hash arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_iZahashPairZahash f8f813f0b1320a758d4109d7298ace29466040c2033c2b25b81933e1dfbcca02 i@hashPair@hash arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_main 39e60a011a6803322d624c02f068d275f107995912c568fa085b08ae65792311 main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 @.kont_symbol2 = private constant [25 x i8] c"prismfn_iZahashIntZahash\00" @.kont_hash2 = private constant [65 x i8] c"0eabe45704c4040cd9f346fa1f49da315c6911b067a1d67f0bb14b60a891322a\00" @.kont_name2 = private constant [15 x i8] c"i@hashInt@hash\00" @@ -284,9 +284,8 @@ entry: %t4 = call i64 @prism_str_concat(i64 %t1, i64 %t3) call void @prism_rc_dec(i64 %t1) call void @prism_rc_dec(i64 %t3) - %t5 = call i64 @prism_str_lit(ptr @.str0, i64 11) - %t6 = call i64 @prism_str_concat(i64 %t5, i64 %t4) - call void @prism_rc_dec(i64 %t5) + %t6 = call i64 @prism_str_concat(i64 ptrtoint (ptr @.str0 to i64), i64 %t4) + call void @prism_rc_dec(i64 ptrtoint (ptr @.str0 to i64)) call void @prism_rc_dec(i64 %t4) %t7 = call i64 @prism_blake3(i64 %t6) call void @prism_rc_dec(i64 %t6) @@ -298,9 +297,8 @@ define i64 @prismfn_iZahashIntZahash(i64 %a0) #0 { entry: %t0 = call i64 @prism_show_int(i64 %a0) call void @prism_rc_dec(i64 %a0) - %t1 = call i64 @prism_str_lit(ptr @.str1, i64 1) - %t2 = call i64 @prism_str_concat(i64 %t1, i64 %t0) - call void @prism_rc_dec(i64 %t1) + %t2 = call i64 @prism_str_concat(i64 ptrtoint (ptr @.str1 to i64), i64 %t0) + call void @prism_rc_dec(i64 ptrtoint (ptr @.str1 to i64)) call void @prism_rc_dec(i64 %t0) %t3 = call i64 @prism_blake3(i64 %t2) call void @prism_rc_dec(i64 %t2) @@ -313,9 +311,6 @@ declare i64 @prism_str_concat(i64, i64) #0 ; Function Attrs: nounwind declare void @prism_rc_dec(i64) #0 -; Function Attrs: nounwind -declare i64 @prism_str_lit(ptr, i64) #0 - ; Function Attrs: nounwind declare i64 @prism_blake3(i64) #0 diff --git a/tests/snapshots/snapshots__pipeline@div_zero.pr.snap b/tests/snapshots/snapshots__pipeline@div_zero.pr.snap index d47e4dc0..228f7b4c 100644 --- a/tests/snapshots/snapshots__pipeline@div_zero.pr.snap +++ b/tests/snapshots/snapshots__pipeline@div_zero.pr.snap @@ -70,8 +70,8 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@prism_native_kont_table = constant [741 x i8] c"scheme prism-core-hash-v2\0Abundle 3d215a232fb26212ee6c435f077308a3ed5bfa3f6228d6c8b2a7415e5c163de6\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:3d215a232fb26212ee6c435f077308a3ed5bfa3f6228d6c8b2a7415e5c163de6\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_main 0cc196d553d7248a3658382832399f32f6a97ad69a026c3cc8996a7c62f26c94 main\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [811 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 3d215a232fb26212ee6c435f077308a3ed5bfa3f6228d6c8b2a7415e5c163de6\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:3d215a232fb26212ee6c435f077308a3ed5bfa3f6228d6c8b2a7415e5c163de6\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main 0cc196d553d7248a3658382832399f32f6a97ad69a026c3cc8996a7c62f26c94 main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [766 x i8] c"scheme prism-core-hash-v2\0Abundle 3d215a232fb26212ee6c435f077308a3ed5bfa3f6228d6c8b2a7415e5c163de6\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:3d215a232fb26212ee6c435f077308a3ed5bfa3f6228d6c8b2a7415e5c163de6\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_main 0cc196d553d7248a3658382832399f32f6a97ad69a026c3cc8996a7c62f26c94 main\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [836 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 3d215a232fb26212ee6c435f077308a3ed5bfa3f6228d6c8b2a7415e5c163de6\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:3d215a232fb26212ee6c435f077308a3ed5bfa3f6228d6c8b2a7415e5c163de6\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main 0cc196d553d7248a3658382832399f32f6a97ad69a026c3cc8996a7c62f26c94 main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 @.kont_symbol0 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash0 = private constant [65 x i8] c"0cc196d553d7248a3658382832399f32f6a97ad69a026c3cc8996a7c62f26c94\00" @.kont_name0 = private constant [5 x i8] c"main\00" diff --git a/tests/snapshots/snapshots__pipeline@effect_rows.pr.snap b/tests/snapshots/snapshots__pipeline@effect_rows.pr.snap index 49ce508e..c2714bfd 100644 --- a/tests/snapshots/snapshots__pipeline@effect_rows.pr.snap +++ b/tests/snapshots/snapshots__pipeline@effect_rows.pr.snap @@ -330,10 +330,10 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@.str0 = private constant [54 x i8] c"ICE: effect op escaped a closed native handler clause\00" -@.str1 = private constant [50 x i8] c"ICE: unhandled effect op in closed native handler\00" -@prism_native_kont_table = constant [929 x i8] c"scheme prism-core-hash-v2\0Abundle 75678a6ea47b892e12efb298826b462e9e66906aca8873f648e8734551574fae\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:75678a6ea47b892e12efb298826b462e9e66906aca8873f648e8734551574fae\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_main 356a812811f03ca83c1876512d4c1af42e366919d97df94ee2325b4c55dd3ad0 main\0Afn prismfn_risky 7b94f3c7c791ea22bf9d8987b1b2d8c6a3f38fc66aa1ad66088fc485c84007e4 risky\0Afn prismfn_safe 73d874444de375999280aa5f0f880aa7b3e2a7dbd4a37be6ad0af76b0848789e safe\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [1067 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 75678a6ea47b892e12efb298826b462e9e66906aca8873f648e8734551574fae\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:75678a6ea47b892e12efb298826b462e9e66906aca8873f648e8734551574fae\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main 356a812811f03ca83c1876512d4c1af42e366919d97df94ee2325b4c55dd3ad0 main arity 0 slots abi-word[]\0Astate prismfn_risky 7b94f3c7c791ea22bf9d8987b1b2d8c6a3f38fc66aa1ad66088fc485c84007e4 risky arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_safe 73d874444de375999280aa5f0f880aa7b3e2a7dbd4a37be6ad0af76b0848789e safe arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 +@.str0 = private constant { i64, i64, i64, [54 x i8] } { i64 1152921504606846976, i64 1398034944, i64 53, [54 x i8] c"ICE: effect op escaped a closed native handler clause\00" }, align 8 +@.str1 = private constant { i64, i64, i64, [50 x i8] } { i64 1152921504606846976, i64 1398034944, i64 49, [50 x i8] c"ICE: unhandled effect op in closed native handler\00" }, align 8 +@prism_native_kont_table = constant [954 x i8] c"scheme prism-core-hash-v2\0Abundle 75678a6ea47b892e12efb298826b462e9e66906aca8873f648e8734551574fae\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:75678a6ea47b892e12efb298826b462e9e66906aca8873f648e8734551574fae\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_main 356a812811f03ca83c1876512d4c1af42e366919d97df94ee2325b4c55dd3ad0 main\0Afn prismfn_risky 7b94f3c7c791ea22bf9d8987b1b2d8c6a3f38fc66aa1ad66088fc485c84007e4 risky\0Afn prismfn_safe 73d874444de375999280aa5f0f880aa7b3e2a7dbd4a37be6ad0af76b0848789e safe\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [1092 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 75678a6ea47b892e12efb298826b462e9e66906aca8873f648e8734551574fae\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:75678a6ea47b892e12efb298826b462e9e66906aca8873f648e8734551574fae\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main 356a812811f03ca83c1876512d4c1af42e366919d97df94ee2325b4c55dd3ad0 main arity 0 slots abi-word[]\0Astate prismfn_risky 7b94f3c7c791ea22bf9d8987b1b2d8c6a3f38fc66aa1ad66088fc485c84007e4 risky arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_safe 73d874444de375999280aa5f0f880aa7b3e2a7dbd4a37be6ad0af76b0848789e safe arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 @.kont_symbol0 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash0 = private constant [65 x i8] c"356a812811f03ca83c1876512d4c1af42e366919d97df94ee2325b4c55dd3ad0\00" @.kont_name0 = private constant [5 x i8] c"main\00" @@ -511,8 +511,7 @@ b7: ; preds = %b6 b8: ; preds = %b6 call void @prism_rc_dec(i64 %t35) call void @prism_rc_dec(i64 %t12) - %t82 = call i64 @prism_str_lit(ptr @.str1, i64 49) - call void @prism_fatal(i64 %t82) + call void @prism_fatal(i64 ptrtoint (ptr @.str1 to i64)) ret i64 0 b11: ; preds = %b7 @@ -537,8 +536,7 @@ b12: ; preds = %b7 %t71 = getelementptr inbounds i8, ptr %t53, i64 48 %t72 = load i64, ptr %t71, align 8 call void @prism_rc_dec(i64 %t52) - %t74 = call i64 @prism_str_lit(ptr @.str0, i64 53) - call void @prism_fatal(i64 %t74) + call void @prism_fatal(i64 ptrtoint (ptr @.str0 to i64)) ret i64 0 b13: ; preds = %b7 @@ -705,9 +703,6 @@ b5: ; preds = %b3 unreachable } -; Function Attrs: nounwind -declare i64 @prism_str_lit(ptr, i64) #0 - ; Function Attrs: nounwind declare void @prism_fatal(i64) #0 diff --git a/tests/snapshots/snapshots__pipeline@effects.pr.snap b/tests/snapshots/snapshots__pipeline@effects.pr.snap index 22eee0be..5422518e 100644 --- a/tests/snapshots/snapshots__pipeline@effects.pr.snap +++ b/tests/snapshots/snapshots__pipeline@effects.pr.snap @@ -562,14 +562,10 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@.str0 = private constant [50 x i8] c"ICE: unhandled effect op in closed native handler\00" -@.str1 = private constant [54 x i8] c"ICE: effect op escaped a closed native handler clause\00" -@.str2 = private constant [50 x i8] c"ICE: unhandled effect op in closed native handler\00" -@.str3 = private constant [54 x i8] c"ICE: effect op escaped a closed native handler clause\00" -@.str4 = private constant [54 x i8] c"ICE: effect op escaped a closed native handler clause\00" -@.str5 = private constant [50 x i8] c"ICE: unhandled effect op in closed native handler\00" -@prism_native_kont_table = constant [1062 x i8] c"scheme prism-core-hash-v2\0Abundle 0ced77e7cb7388111111b275ff82571221d6b57d97799598a32f42d067439f92\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:0ced77e7cb7388111111b275ff82571221d6b57d97799598a32f42d067439f92\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_ask_example 001d03e305e98c6216ed709e88c1799d4c8ae00a05b093188cd5be932670a687 ask_example\0Afn prismfn_catch_exn 2b71f1f8ab1846516adebb916ae73f101e13347751eb9f9e9af393cf8a9b6be7 catch_exn\0Afn prismfn_main 907413324c3a908d6ec37a2dd7363c604fb02f30c0f172d0445aed5098cb56e1 main\0Afn prismfn_state_example c455ec3d1436bfaa1ce35d55558d93c2a35df0aa04f4093b54352b5ff24a1c6d state_example\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [1195 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 0ced77e7cb7388111111b275ff82571221d6b57d97799598a32f42d067439f92\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:0ced77e7cb7388111111b275ff82571221d6b57d97799598a32f42d067439f92\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_ask_example 001d03e305e98c6216ed709e88c1799d4c8ae00a05b093188cd5be932670a687 ask_example arity 0 slots abi-word[]\0Astate prismfn_catch_exn 2b71f1f8ab1846516adebb916ae73f101e13347751eb9f9e9af393cf8a9b6be7 catch_exn arity 0 slots abi-word[]\0Astate prismfn_main 907413324c3a908d6ec37a2dd7363c604fb02f30c0f172d0445aed5098cb56e1 main arity 0 slots abi-word[]\0Astate prismfn_state_example c455ec3d1436bfaa1ce35d55558d93c2a35df0aa04f4093b54352b5ff24a1c6d state_example arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 +@.str0 = private constant { i64, i64, i64, [50 x i8] } { i64 1152921504606846976, i64 1398034944, i64 49, [50 x i8] c"ICE: unhandled effect op in closed native handler\00" }, align 8 +@.str1 = private constant { i64, i64, i64, [54 x i8] } { i64 1152921504606846976, i64 1398034944, i64 53, [54 x i8] c"ICE: effect op escaped a closed native handler clause\00" }, align 8 +@prism_native_kont_table = constant [1087 x i8] c"scheme prism-core-hash-v2\0Abundle 0ced77e7cb7388111111b275ff82571221d6b57d97799598a32f42d067439f92\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:0ced77e7cb7388111111b275ff82571221d6b57d97799598a32f42d067439f92\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_ask_example 001d03e305e98c6216ed709e88c1799d4c8ae00a05b093188cd5be932670a687 ask_example\0Afn prismfn_catch_exn 2b71f1f8ab1846516adebb916ae73f101e13347751eb9f9e9af393cf8a9b6be7 catch_exn\0Afn prismfn_main 907413324c3a908d6ec37a2dd7363c604fb02f30c0f172d0445aed5098cb56e1 main\0Afn prismfn_state_example c455ec3d1436bfaa1ce35d55558d93c2a35df0aa04f4093b54352b5ff24a1c6d state_example\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [1220 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 0ced77e7cb7388111111b275ff82571221d6b57d97799598a32f42d067439f92\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:0ced77e7cb7388111111b275ff82571221d6b57d97799598a32f42d067439f92\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_ask_example 001d03e305e98c6216ed709e88c1799d4c8ae00a05b093188cd5be932670a687 ask_example arity 0 slots abi-word[]\0Astate prismfn_catch_exn 2b71f1f8ab1846516adebb916ae73f101e13347751eb9f9e9af393cf8a9b6be7 catch_exn arity 0 slots abi-word[]\0Astate prismfn_main 907413324c3a908d6ec37a2dd7363c604fb02f30c0f172d0445aed5098cb56e1 main arity 0 slots abi-word[]\0Astate prismfn_state_example c455ec3d1436bfaa1ce35d55558d93c2a35df0aa04f4093b54352b5ff24a1c6d state_example arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 @.kont_symbol2 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash2 = private constant [65 x i8] c"907413324c3a908d6ec37a2dd7363c604fb02f30c0f172d0445aed5098cb56e1\00" @.kont_name2 = private constant [5 x i8] c"main\00" @@ -813,8 +809,7 @@ b7: ; preds = %b6 b8: ; preds = %b6 call void @prism_rc_dec(i64 %t12) call void @prism_rc_dec(i64 %t35) - %t41 = call i64 @prism_str_lit(ptr @.str0, i64 49) - call void @prism_fatal(i64 %t41) + call void @prism_fatal(i64 ptrtoint (ptr @.str0 to i64)) ret i64 0 } @@ -910,8 +905,7 @@ b7: ; preds = %b6 b8: ; preds = %b6 call void @prism_rc_dec(i64 %t35) call void @prism_rc_dec(i64 %t14) - %t82 = call i64 @prism_str_lit(ptr @.str2, i64 49) - call void @prism_fatal(i64 %t82) + call void @prism_fatal(i64 ptrtoint (ptr @.str0 to i64)) ret i64 0 b11: ; preds = %b7 @@ -936,8 +930,7 @@ b12: ; preds = %b7 %t71 = getelementptr inbounds i8, ptr %t53, i64 48 %t72 = load i64, ptr %t71, align 8 call void @prism_rc_dec(i64 %t52) - %t74 = call i64 @prism_str_lit(ptr @.str1, i64 53) - call void @prism_fatal(i64 %t74) + call void @prism_fatal(i64 ptrtoint (ptr @.str1 to i64)) ret i64 0 b13: ; preds = %b7 @@ -1069,8 +1062,7 @@ b12: ; preds = %b7 %t73 = getelementptr inbounds i8, ptr %t55, i64 48 %t74 = load i64, ptr %t73, align 8 call void @prism_rc_dec(i64 %t54) - %t76 = call i64 @prism_str_lit(ptr @.str3, i64 53) - call void @prism_fatal(i64 %t76) + call void @prism_fatal(i64 ptrtoint (ptr @.str1 to i64)) ret i64 0 b13: ; preds = %b7 @@ -1133,8 +1125,7 @@ b17: ; preds = %b16 b18: ; preds = %b16 call void @prism_rc_dec(i64 %t100) call void @prism_rc_dec(i64 %t14) - %t147 = call i64 @prism_str_lit(ptr @.str5, i64 49) - call void @prism_fatal(i64 %t147) + call void @prism_fatal(i64 ptrtoint (ptr @.str0 to i64)) ret i64 0 b21: ; preds = %b17 @@ -1159,8 +1150,7 @@ b22: ; preds = %b17 %t136 = getelementptr inbounds i8, ptr %t118, i64 48 %t137 = load i64, ptr %t136, align 8 call void @prism_rc_dec(i64 %t117) - %t139 = call i64 @prism_str_lit(ptr @.str4, i64 53) - call void @prism_fatal(i64 %t139) + call void @prism_fatal(i64 ptrtoint (ptr @.str1 to i64)) ret i64 0 b23: ; preds = %b17 @@ -1199,9 +1189,6 @@ declare void @prism_rc_inc(i64) #0 ; Function Attrs: nounwind declare i64 @prism_rt_int_cmp(i64, i64) #0 -; Function Attrs: nounwind -declare i64 @prism_str_lit(ptr, i64) #0 - ; Function Attrs: nounwind declare void @prism_fatal(i64) #0 diff --git a/tests/snapshots/snapshots__pipeline@eq_default.pr.snap b/tests/snapshots/snapshots__pipeline@eq_default.pr.snap index dbad25ad..df7ea90e 100644 --- a/tests/snapshots/snapshots__pipeline@eq_default.pr.snap +++ b/tests/snapshots/snapshots__pipeline@eq_default.pr.snap @@ -368,10 +368,14 @@ fn main() = fn eqp(x, y) = return x to t@0 return y to t@1 + dup t@1 + dup t@0 t@0 == t@1 fn ltp(x, y) = return x to t@2 return y to t@3 + dup t@3 + dup t@2 t@2 < t@3 fn main() = return 1 to t@4 @@ -400,7 +404,10 @@ fn main() = drop _ return 2 to t@19 return 2 to t@20 - eqp(t@19, t@20) to t@21 + eqp(t@19, t@20) to %rc0 + drop t@20 + drop t@19 + return %rc0 to t@21 show_bool(t@21) to t@22 prints t@22 to t@23 drop t@23 @@ -408,7 +415,10 @@ fn main() = drop _ return 1 to t@24 return 2 to t@25 - ltp(t@24, t@25) to t@26 + ltp(t@24, t@25) to %rc1 + drop t@25 + drop t@24 + return %rc1 to t@26 show_bool(t@26) to t@27 prints t@27 to t@28 drop t@28 @@ -419,8 +429,8 @@ fn main() = source_filename = "prism" @.fmts = private constant [3 x i8] c"%s\00" -@prism_native_kont_table = constant [923 x i8] c"scheme prism-core-hash-v2\0Abundle a2e29a57b96431bbdc75dbd75ccc45f34d45e5b8ac5754da0b2bb83a60b2fe0a\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:a2e29a57b96431bbdc75dbd75ccc45f34d45e5b8ac5754da0b2bb83a60b2fe0a\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_eqp 055362bef1c82a335fb4bfbdf8c9bb3dd7d851ee62efb128ef9c247b19e8dd5f eqp\0Afn prismfn_ltp 6e58d3d4a8d58100fd612000a344a785f83ebcee8141e19bc3ee92db2ef75e58 ltp\0Afn prismfn_main 257e5934c92f2afbc435ff1ad94432554913f7e89f510e0ea0091ec2f4a7f359 main\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [1089 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle a2e29a57b96431bbdc75dbd75ccc45f34d45e5b8ac5754da0b2bb83a60b2fe0a\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:a2e29a57b96431bbdc75dbd75ccc45f34d45e5b8ac5754da0b2bb83a60b2fe0a\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_eqp 055362bef1c82a335fb4bfbdf8c9bb3dd7d851ee62efb128ef9c247b19e8dd5f eqp arity 2 slots abi-word[arg0=%a0:word,arg1=%a1:word]\0Astate prismfn_ltp 6e58d3d4a8d58100fd612000a344a785f83ebcee8141e19bc3ee92db2ef75e58 ltp arity 2 slots abi-word[arg0=%a0:word,arg1=%a1:word]\0Astate prismfn_main 257e5934c92f2afbc435ff1ad94432554913f7e89f510e0ea0091ec2f4a7f359 main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [948 x i8] c"scheme prism-core-hash-v2\0Abundle a2e29a57b96431bbdc75dbd75ccc45f34d45e5b8ac5754da0b2bb83a60b2fe0a\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:a2e29a57b96431bbdc75dbd75ccc45f34d45e5b8ac5754da0b2bb83a60b2fe0a\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_eqp 055362bef1c82a335fb4bfbdf8c9bb3dd7d851ee62efb128ef9c247b19e8dd5f eqp\0Afn prismfn_ltp 6e58d3d4a8d58100fd612000a344a785f83ebcee8141e19bc3ee92db2ef75e58 ltp\0Afn prismfn_main 257e5934c92f2afbc435ff1ad94432554913f7e89f510e0ea0091ec2f4a7f359 main\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [1114 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle a2e29a57b96431bbdc75dbd75ccc45f34d45e5b8ac5754da0b2bb83a60b2fe0a\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:a2e29a57b96431bbdc75dbd75ccc45f34d45e5b8ac5754da0b2bb83a60b2fe0a\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_eqp 055362bef1c82a335fb4bfbdf8c9bb3dd7d851ee62efb128ef9c247b19e8dd5f eqp arity 2 slots abi-word[arg0=%a0:word,arg1=%a1:word]\0Astate prismfn_ltp 6e58d3d4a8d58100fd612000a344a785f83ebcee8141e19bc3ee92db2ef75e58 ltp arity 2 slots abi-word[arg0=%a0:word,arg1=%a1:word]\0Astate prismfn_main 257e5934c92f2afbc435ff1ad94432554913f7e89f510e0ea0091ec2f4a7f359 main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 @.kont_symbol2 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash2 = private constant [65 x i8] c"257e5934c92f2afbc435ff1ad94432554913f7e89f510e0ea0091ec2f4a7f359\00" @.kont_name2 = private constant [5 x i8] c"main\00" diff --git a/tests/snapshots/snapshots__pipeline@errors_unhandled.pr.snap b/tests/snapshots/snapshots__pipeline@errors_unhandled.pr.snap index 647219be..91bd0bee 100644 --- a/tests/snapshots/snapshots__pipeline@errors_unhandled.pr.snap +++ b/tests/snapshots/snapshots__pipeline@errors_unhandled.pr.snap @@ -176,12 +176,12 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@.str0 = private constant [2 x i8] c"y\00" -@.str1 = private constant [34 x i8] c"unhandled effect `throw@NotFound`\00" -@.str2 = private constant [17 x i8] c"unhandled effect\00" -@.str3 = private constant [2 x i8] c"x\00" -@prism_native_kont_table = constant [838 x i8] c"scheme prism-core-hash-v2\0Abundle 2eea56faba9c2b09e2883166b8fcf1092e91778a28ada9f1bbee72a08942bfd7\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:2eea56faba9c2b09e2883166b8fcf1092e91778a28ada9f1bbee72a08942bfd7\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_lookup 926a1a811cf6ca259c2be122e4fd5627a811a2bb51ac3daac56a91d3df900b06 lookup\0Afn prismfn_main 62cfc83c8884265cb53048dc6e41b0622154cf3b44f46aeb97fafd26dba8c503 main\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [942 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 2eea56faba9c2b09e2883166b8fcf1092e91778a28ada9f1bbee72a08942bfd7\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:2eea56faba9c2b09e2883166b8fcf1092e91778a28ada9f1bbee72a08942bfd7\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_lookup 926a1a811cf6ca259c2be122e4fd5627a811a2bb51ac3daac56a91d3df900b06 lookup arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_main 62cfc83c8884265cb53048dc6e41b0622154cf3b44f46aeb97fafd26dba8c503 main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 +@.str0 = private constant { i64, i64, i64, [2 x i8] } { i64 1152921504606846976, i64 1398034944, i64 1, [2 x i8] c"y\00" }, align 8 +@.str1 = private constant { i64, i64, i64, [34 x i8] } { i64 1152921504606846976, i64 1398034944, i64 33, [34 x i8] c"unhandled effect `throw@NotFound`\00" }, align 8 +@.str2 = private constant { i64, i64, i64, [17 x i8] } { i64 1152921504606846976, i64 1398034944, i64 16, [17 x i8] c"unhandled effect\00" }, align 8 +@.str3 = private constant { i64, i64, i64, [2 x i8] } { i64 1152921504606846976, i64 1398034944, i64 1, [2 x i8] c"x\00" }, align 8 +@prism_native_kont_table = constant [863 x i8] c"scheme prism-core-hash-v2\0Abundle 2eea56faba9c2b09e2883166b8fcf1092e91778a28ada9f1bbee72a08942bfd7\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:2eea56faba9c2b09e2883166b8fcf1092e91778a28ada9f1bbee72a08942bfd7\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_lookup 926a1a811cf6ca259c2be122e4fd5627a811a2bb51ac3daac56a91d3df900b06 lookup\0Afn prismfn_main 62cfc83c8884265cb53048dc6e41b0622154cf3b44f46aeb97fafd26dba8c503 main\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [967 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 2eea56faba9c2b09e2883166b8fcf1092e91778a28ada9f1bbee72a08942bfd7\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:2eea56faba9c2b09e2883166b8fcf1092e91778a28ada9f1bbee72a08942bfd7\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_lookup 926a1a811cf6ca259c2be122e4fd5627a811a2bb51ac3daac56a91d3df900b06 lookup arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_main 62cfc83c8884265cb53048dc6e41b0622154cf3b44f46aeb97fafd26dba8c503 main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 @.kont_symbol1 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash1 = private constant [65 x i8] c"62cfc83c8884265cb53048dc6e41b0622154cf3b44f46aeb97fafd26dba8c503\00" @.kont_name1 = private constant [5 x i8] c"main\00" @@ -192,12 +192,11 @@ source_filename = "prism" ; Function Attrs: nounwind define i64 @prismfn_main() #0 { entry: - %t0 = call i64 @prism_str_lit(ptr @.str0, i64 1) %t2 = call ptr @prism_alloc(i64 1) %t3 = getelementptr inbounds i8, ptr %t2, i64 8 store i64 0, ptr %t3, align 8 %t5 = getelementptr inbounds i8, ptr %t2, i64 24 - store i64 %t0, ptr %t5, align 8 + store i64 ptrtoint (ptr @.str0 to i64), ptr %t5, align 8 %t6 = ptrtoint ptr %t2 to i64 %t8 = call ptr @prism_alloc(i64 0) %t9 = getelementptr inbounds i8, ptr %t8, i64 8 @@ -268,20 +267,15 @@ b6: ; preds = %b5, %b4 b7: ; preds = %b6 call void @prism_rc_dec(i64 %t53) - %t57 = call i64 @prism_str_lit(ptr @.str1, i64 33) - call void @prism_fatal(i64 %t57) + call void @prism_fatal(i64 ptrtoint (ptr @.str1 to i64)) ret i64 0 b8: ; preds = %b6 call void @prism_rc_dec(i64 %t53) - %t60 = call i64 @prism_str_lit(ptr @.str2, i64 16) - call void @prism_fatal(i64 %t60) + call void @prism_fatal(i64 ptrtoint (ptr @.str2 to i64)) ret i64 0 } -; Function Attrs: nounwind -declare i64 @prism_str_lit(ptr, i64) #0 - ; Function Attrs: nounwind declare ptr @prism_alloc(i64) #0 @@ -476,12 +470,11 @@ declare void @prism_print_int(i64) #0 ; Function Attrs: nounwind define i64 @prismlam_7170891667029511803(i64 %_p0) #0 { entry: - %t0 = call i64 @prism_str_lit(ptr @.str3, i64 1) %t2 = call ptr @prism_alloc(i64 1) %t3 = getelementptr inbounds i8, ptr %t2, i64 8 store i64 0, ptr %t3, align 8 %t5 = getelementptr inbounds i8, ptr %t2, i64 24 - store i64 %t0, ptr %t5, align 8 + store i64 ptrtoint (ptr @.str3 to i64), ptr %t5, align 8 %t6 = ptrtoint ptr %t2 to i64 %t8 = call ptr @prism_alloc(i64 1) %t9 = getelementptr inbounds i8, ptr %t8, i64 8 diff --git a/tests/snapshots/snapshots__pipeline@factorial.pr.snap b/tests/snapshots/snapshots__pipeline@factorial.pr.snap index 5200d051..f530747c 100644 --- a/tests/snapshots/snapshots__pipeline@factorial.pr.snap +++ b/tests/snapshots/snapshots__pipeline@factorial.pr.snap @@ -211,26 +211,30 @@ fn main() = == fbip (rc) == fn fact(n) = - dup n return n to t@0 return 0 to t@1 + dup t@0 t@0 == t@1 to t@7 if t@7 then drop t@7 - drop n return 1 else drop t@7 - dup n return n to t@5 return n to t@2 return 1 to t@3 + dup t@2 t@2 - t@3 to t@4 - fact(t@4) to t@6 + fact(t@4) to %rc0 + drop t@4 + return %rc0 to t@6 + dup t@5 t@5 * t@6 fn main() = return 5 to t@8 - fact(t@8) to r + fact(t@8) to %rc1 + drop t@8 + return %rc1 to r dup r return r to t@9 print t@9 to t@10 @@ -243,8 +247,8 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@prism_native_kont_table = constant [834 x i8] c"scheme prism-core-hash-v2\0Abundle dca17e9efc43090d72149beb3c6ca33eb03722f09eac307b4d95665368d2904b\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:dca17e9efc43090d72149beb3c6ca33eb03722f09eac307b4d95665368d2904b\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_fact 554019e1b3573546277da4d001adb1d53ba02bcc0c8f0bb33c832ecdebd47ab4 fact\0Afn prismfn_main e96725a4857ab040d4e56d8758e1f81ce168fd4223bf86f0b5eb9bb011e59601 main\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [938 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle dca17e9efc43090d72149beb3c6ca33eb03722f09eac307b4d95665368d2904b\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:dca17e9efc43090d72149beb3c6ca33eb03722f09eac307b4d95665368d2904b\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_fact 554019e1b3573546277da4d001adb1d53ba02bcc0c8f0bb33c832ecdebd47ab4 fact arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_main e96725a4857ab040d4e56d8758e1f81ce168fd4223bf86f0b5eb9bb011e59601 main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [859 x i8] c"scheme prism-core-hash-v2\0Abundle dca17e9efc43090d72149beb3c6ca33eb03722f09eac307b4d95665368d2904b\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:dca17e9efc43090d72149beb3c6ca33eb03722f09eac307b4d95665368d2904b\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_fact 554019e1b3573546277da4d001adb1d53ba02bcc0c8f0bb33c832ecdebd47ab4 fact\0Afn prismfn_main e96725a4857ab040d4e56d8758e1f81ce168fd4223bf86f0b5eb9bb011e59601 main\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [963 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle dca17e9efc43090d72149beb3c6ca33eb03722f09eac307b4d95665368d2904b\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:dca17e9efc43090d72149beb3c6ca33eb03722f09eac307b4d95665368d2904b\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_fact 554019e1b3573546277da4d001adb1d53ba02bcc0c8f0bb33c832ecdebd47ab4 fact arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_main e96725a4857ab040d4e56d8758e1f81ce168fd4223bf86f0b5eb9bb011e59601 main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 @.kont_symbol0 = private constant [13 x i8] c"prismfn_fact\00" @.kont_hash0 = private constant [65 x i8] c"554019e1b3573546277da4d001adb1d53ba02bcc0c8f0bb33c832ecdebd47ab4\00" @.kont_name0 = private constant [5 x i8] c"fact\00" @@ -288,42 +292,43 @@ b2: ; preds = %b1, %b0 b3: ; preds = %b2 call void @prism_rc_dec(i64 %t18) - call void @prism_rc_dec(i64 %a0) ret i64 3 b4: ; preds = %b2 call void @prism_rc_dec(i64 %t18) call void @prism_rc_inc(i64 %a0) - %t27 = and i64 %a0, 3 - %t29 = and i64 %t27, 1 - %t30 = icmp eq i64 %t29, 1 - br i1 %t30, label %b5, label %b6 + %t26 = and i64 %a0, 3 + %t28 = and i64 %t26, 1 + %t29 = icmp eq i64 %t28, 1 + br i1 %t29, label %b5, label %b6 b5: ; preds = %b4 - %t32 = ashr i64 %a0, 1 - %t35 = sub i64 %t32, 1 - %t37 = shl i64 %t35, 1 - %t38 = ashr i64 %t37, 1 - %t39 = icmp eq i64 %t38, %t35 - br i1 %t39, label %b8, label %b6 + %t31 = ashr i64 %a0, 1 + %t34 = sub i64 %t31, 1 + %t36 = shl i64 %t34, 1 + %t37 = ashr i64 %t36, 1 + %t38 = icmp eq i64 %t37, %t34 + br i1 %t38, label %b8, label %b6 b6: ; preds = %b5, %b4 - %t41 = call i64 @prism_rt_int_sub(i64 %a0, i64 3) + %t40 = call i64 @prism_rt_int_sub(i64 %a0, i64 3) br label %b7 b8: ; preds = %b5 - %t40 = or i64 %t37, 1 + %t39 = or i64 %t36, 1 br label %b7 b7: ; preds = %b6, %b8 - %t42 = phi i64 [ %t40, %b8 ], [ %t41, %b6 ] + %t41 = phi i64 [ %t39, %b8 ], [ %t40, %b6 ] call void @prism_rc_dec(i64 %a0) call void @prism_rc_dec(i64 3) - %t43 = call i64 @prismfn_fact(i64 %t42) - %t44 = call i64 @prism_rt_int_mul(i64 %a0, i64 %t43) + %t42 = call i64 @prismfn_fact(i64 %t41) + call void @prism_rc_dec(i64 %t41) + call void @prism_rc_inc(i64 %a0) + %t45 = call i64 @prism_rt_int_mul(i64 %a0, i64 %t42) call void @prism_rc_dec(i64 %a0) - call void @prism_rc_dec(i64 %t43) - ret i64 %t44 + call void @prism_rc_dec(i64 %t42) + ret i64 %t45 } ; Function Attrs: nounwind diff --git a/tests/snapshots/snapshots__pipeline@fbip.pr.snap b/tests/snapshots/snapshots__pipeline@fbip.pr.snap index e5dae647..3115cb8a 100644 --- a/tests/snapshots/snapshots__pipeline@fbip.pr.snap +++ b/tests/snapshots/snapshots__pipeline@fbip.pr.snap @@ -293,8 +293,8 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@prism_native_kont_table = constant [741 x i8] c"scheme prism-core-hash-v2\0Abundle 9682f603d593f7ac208b6fba6f54f228ea4563621198c0a17999eac766a23e1d\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:9682f603d593f7ac208b6fba6f54f228ea4563621198c0a17999eac766a23e1d\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_main 094d8f8938f2205433197f79b0bdc81d4ce7dcb2200211ebc93cc31667e17d12 main\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [811 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 9682f603d593f7ac208b6fba6f54f228ea4563621198c0a17999eac766a23e1d\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:9682f603d593f7ac208b6fba6f54f228ea4563621198c0a17999eac766a23e1d\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main 094d8f8938f2205433197f79b0bdc81d4ce7dcb2200211ebc93cc31667e17d12 main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [766 x i8] c"scheme prism-core-hash-v2\0Abundle 9682f603d593f7ac208b6fba6f54f228ea4563621198c0a17999eac766a23e1d\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:9682f603d593f7ac208b6fba6f54f228ea4563621198c0a17999eac766a23e1d\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_main 094d8f8938f2205433197f79b0bdc81d4ce7dcb2200211ebc93cc31667e17d12 main\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [836 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 9682f603d593f7ac208b6fba6f54f228ea4563621198c0a17999eac766a23e1d\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:9682f603d593f7ac208b6fba6f54f228ea4563621198c0a17999eac766a23e1d\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main 094d8f8938f2205433197f79b0bdc81d4ce7dcb2200211ebc93cc31667e17d12 main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 @.kont_symbol0 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash0 = private constant [65 x i8] c"094d8f8938f2205433197f79b0bdc81d4ce7dcb2200211ebc93cc31667e17d12\00" @.kont_name0 = private constant [5 x i8] c"main\00" diff --git a/tests/snapshots/snapshots__pipeline@field_projection_common.pr.snap b/tests/snapshots/snapshots__pipeline@field_projection_common.pr.snap new file mode 100644 index 00000000..d7913c66 --- /dev/null +++ b/tests/snapshots/snapshots__pipeline@field_projection_common.pr.snap @@ -0,0 +1,221 @@ +--- +source: tests/snapshots.rs +expression: "normalize_pipeline_report(&prism::report(&src))" +input_file: tests/cases/field_projection_common.pr +--- +== tokens == +VOpen Type UIdent("Tagged") Eq UIdent("A") LBrace Ident("id") Colon KwInt RBrace Bar UIdent("B") LBrace Ident("id") Colon KwInt RBrace VSemi Fn Ident("tag_id_by_match") LParen Ident("tagged") Colon UIdent("Tagged") RParen Colon KwInt Eq VOpen Match Ident("tagged") Of VOpen UIdent("A") LBrace Ident("id") Eq Ident("id") RBrace FatArrow Ident("id") VSemi UIdent("B") LBrace Ident("id") Eq Ident("id") RBrace FatArrow Ident("id") VClose VClose VSemi Fn Ident("tag_id") LParen Ident("tagged") Colon UIdent("Tagged") RParen Colon KwInt Eq Ident("tagged") Dot Ident("id") VSemi Fn Ident("main") LParen RParen Eq Ident("println") LParen Int(IntLit { value: 0, suffix: None }) RParen VClose + +== ast == +Program { + types: [ + DataDecl { + name: "Tagged", + params: [], + param_kinds: [], + ctors: [ + Ctor { + name: "A", + args: [ + Int, + ], + fields: Some( + [ + ( + "id", + Int, + ), + ], + ), + }, + Ctor { + name: "B", + args: [ + Int, + ], + fields: Some( + [ + ( + "id", + Int, + ), + ], + ), + }, + ], + deriving: [], + newtype: false, + span: 179..222, + }, + ], + effects: [], + errors: [], + aliases: [], + classes: [], + instances: [], + fns: [ + Decl { + name: "tag_id_by_match", + params: [ + Param { + name: "tagged", + ty: Some( + Con( + "Tagged", + [], + ), + ), + borrow: false, + default: None, + }, + ], + ret: Some( + Int, + ), + eff: None, + constraints: [], + body: Spanned { + node: Match( + Spanned { + node: Var( + "tagged", + ), + span: 276..282, + }, + [ + Arm { + pat: Spanned { + node: Record( + "A", + [ + ( + "id", + Spanned { + node: Var( + "id", + ), + span: 299..301, + }, + ), + ], + false, + ), + span: 290..303, + }, + body: Spanned { + node: Var( + "id", + ), + span: 307..309, + }, + }, + Arm { + pat: Spanned { + node: Record( + "B", + [ + ( + "id", + Spanned { + node: Var( + "id", + ), + span: 323..325, + }, + ), + ], + false, + ), + span: 314..327, + }, + body: Spanned { + node: Var( + "id", + ), + span: 331..333, + }, + }, + ], + ), + span: 270..333, + }, + wheres: [], + span: 224..333, + }, + Decl { + name: "tag_id", + params: [ + Param { + name: "tagged", + ty: Some( + Con( + "Tagged", + [], + ), + ), + borrow: false, + default: None, + }, + ], + ret: Some( + Int, + ), + eff: None, + constraints: [], + body: Spanned { + node: FieldAccess( + Spanned { + node: Var( + "tagged", + ), + span: 370..376, + }, + "id", + ), + span: 370..379, + }, + wheres: [], + span: 335..379, + }, + Decl { + name: "main", + params: [], + ret: None, + eff: None, + constraints: [], + body: Spanned { + node: Call( + Spanned { + node: Var( + "println", + ), + span: 393..400, + }, + [ + Spanned { + node: Int( + IntLit { + value: 0, + suffix: None, + }, + ), + span: 401..402, + }, + ], + ), + span: 393..403, + }, + wheres: [], + span: 381..403, + }, + ], +} + +== types == +[E1023] Type Error: in `tag_id`: field `id` cannot be projected from unrefined sum type `Tagged` with 2 constructors; match a constructor first + ╭─[ :11:36 ] + │ + 11 │ fn tag_id(tagged : Tagged) : Int = tagged.id + │ ────┬──── + │ ╰────── in `tag_id`: field `id` cannot be projected from unrefined sum type `Tagged` with 2 constructors; match a constructor first +────╯ diff --git a/tests/snapshots/snapshots__pipeline@field_projection_nested.pr.snap b/tests/snapshots/snapshots__pipeline@field_projection_nested.pr.snap new file mode 100644 index 00000000..a3cba758 --- /dev/null +++ b/tests/snapshots/snapshots__pipeline@field_projection_nested.pr.snap @@ -0,0 +1,169 @@ +--- +source: tests/snapshots.rs +expression: "normalize_pipeline_report(&prism::report(&src))" +input_file: tests/cases/field_projection_nested.pr +--- +== tokens == +VOpen Type UIdent("Tagged") Eq UIdent("A") LBrace Ident("id") Colon KwInt RBrace Bar UIdent("B") LBrace Ident("id") Colon KwInt RBrace VSemi Type UIdent("Outer") Eq UIdent("Outer") LBrace Ident("inner") Colon UIdent("Tagged") RBrace VSemi Fn Ident("tag_id") LParen Ident("outer") Colon UIdent("Outer") RParen Colon KwInt Eq Ident("outer") Dot Ident("inner") Dot Ident("id") VSemi Fn Ident("main") LParen RParen Eq Ident("println") LParen Int(IntLit { value: 0, suffix: None }) RParen VClose + +== ast == +Program { + types: [ + DataDecl { + name: "Tagged", + params: [], + param_kinds: [], + ctors: [ + Ctor { + name: "A", + args: [ + Int, + ], + fields: Some( + [ + ( + "id", + Int, + ), + ], + ), + }, + Ctor { + name: "B", + args: [ + Int, + ], + fields: Some( + [ + ( + "id", + Int, + ), + ], + ), + }, + ], + deriving: [], + newtype: false, + span: 70..113, + }, + DataDecl { + name: "Outer", + params: [], + param_kinds: [], + ctors: [ + Ctor { + name: "Outer", + args: [ + Con( + "Tagged", + [], + ), + ], + fields: Some( + [ + ( + "inner", + Con( + "Tagged", + [], + ), + ), + ], + ), + }, + ], + deriving: [], + newtype: false, + span: 115..151, + }, + ], + effects: [], + errors: [], + aliases: [], + classes: [], + instances: [], + fns: [ + Decl { + name: "tag_id", + params: [ + Param { + name: "outer", + ty: Some( + Con( + "Outer", + [], + ), + ), + borrow: false, + default: None, + }, + ], + ret: Some( + Int, + ), + eff: None, + constraints: [], + body: Spanned { + node: FieldAccess( + Spanned { + node: FieldAccess( + Spanned { + node: Var( + "outer", + ), + span: 186..191, + }, + "inner", + ), + span: 186..197, + }, + "id", + ), + span: 186..200, + }, + wheres: [], + span: 153..200, + }, + Decl { + name: "main", + params: [], + ret: None, + eff: None, + constraints: [], + body: Spanned { + node: Call( + Spanned { + node: Var( + "println", + ), + span: 214..221, + }, + [ + Spanned { + node: Int( + IntLit { + value: 0, + suffix: None, + }, + ), + span: 222..223, + }, + ], + ), + span: 214..224, + }, + wheres: [], + span: 202..224, + }, + ], +} + +== types == +[E1023] Type Error: in `tag_id`: field `id` cannot be projected from unrefined sum type `Tagged` with 2 constructors; match a constructor first + ╭─[ :6:34 ] + │ + 6 │ fn tag_id(outer : Outer) : Int = outer.inner.id + │ ───────┬────── + │ ╰──────── in `tag_id`: field `id` cannot be projected from unrefined sum type `Tagged` with 2 constructors; match a constructor first +───╯ diff --git a/tests/snapshots/snapshots__pipeline@field_projection_partial.pr.snap b/tests/snapshots/snapshots__pipeline@field_projection_partial.pr.snap new file mode 100644 index 00000000..3f75303f --- /dev/null +++ b/tests/snapshots/snapshots__pipeline@field_projection_partial.pr.snap @@ -0,0 +1,214 @@ +--- +source: tests/snapshots.rs +expression: "normalize_pipeline_report(&prism::report(&src))" +input_file: tests/cases/field_projection_partial.pr +--- +== tokens == +VOpen Type UIdent("Shape") Eq UIdent("Circle") LBrace Ident("radius") Colon KwInt RBrace Bar UIdent("Square") LBrace Ident("side") Colon KwInt RBrace VSemi Fn Ident("radius_by_match") LParen Ident("shape") Colon UIdent("Shape") RParen Colon KwInt Eq VOpen Match Ident("shape") Of VOpen UIdent("Circle") LBrace Ident("radius") Eq Ident("radius") RBrace FatArrow Ident("radius") VSemi UIdent("Square") LBrace DotDot RBrace FatArrow Int(IntLit { value: 0, suffix: None }) VClose VClose VSemi Fn Ident("radius") LParen Ident("shape") Colon UIdent("Shape") RParen Colon KwInt Eq Ident("shape") Dot Ident("radius") VSemi Fn Ident("main") LParen RParen Eq Ident("println") LParen Int(IntLit { value: 0, suffix: None }) RParen VClose + +== ast == +Program { + types: [ + DataDecl { + name: "Shape", + params: [], + param_kinds: [], + ctors: [ + Ctor { + name: "Circle", + args: [ + Int, + ], + fields: Some( + [ + ( + "radius", + Int, + ), + ], + ), + }, + Ctor { + name: "Square", + args: [ + Int, + ], + fields: Some( + [ + ( + "side", + Int, + ), + ], + ), + }, + ], + deriving: [], + newtype: false, + span: 72..130, + }, + ], + effects: [], + errors: [], + aliases: [], + classes: [], + instances: [], + fns: [ + Decl { + name: "radius_by_match", + params: [ + Param { + name: "shape", + ty: Some( + Con( + "Shape", + [], + ), + ), + borrow: false, + default: None, + }, + ], + ret: Some( + Int, + ), + eff: None, + constraints: [], + body: Spanned { + node: Match( + Spanned { + node: Var( + "shape", + ), + span: 182..187, + }, + [ + Arm { + pat: Spanned { + node: Record( + "Circle", + [ + ( + "radius", + Spanned { + node: Var( + "radius", + ), + span: 213..219, + }, + ), + ], + false, + ), + span: 195..221, + }, + body: Spanned { + node: Var( + "radius", + ), + span: 225..231, + }, + }, + Arm { + pat: Spanned { + node: Record( + "Square", + [], + true, + ), + span: 236..249, + }, + body: Spanned { + node: Int( + IntLit { + value: 0, + suffix: None, + }, + ), + span: 253..254, + }, + }, + ], + ), + span: 176..254, + }, + wheres: [], + span: 132..254, + }, + Decl { + name: "radius", + params: [ + Param { + name: "shape", + ty: Some( + Con( + "Shape", + [], + ), + ), + borrow: false, + default: None, + }, + ], + ret: Some( + Int, + ), + eff: None, + constraints: [], + body: Spanned { + node: FieldAccess( + Spanned { + node: Var( + "shape", + ), + span: 289..294, + }, + "radius", + ), + span: 289..301, + }, + wheres: [], + span: 256..301, + }, + Decl { + name: "main", + params: [], + ret: None, + eff: None, + constraints: [], + body: Spanned { + node: Call( + Spanned { + node: Var( + "println", + ), + span: 315..322, + }, + [ + Spanned { + node: Int( + IntLit { + value: 0, + suffix: None, + }, + ), + span: 323..324, + }, + ], + ), + span: 315..325, + }, + wheres: [], + span: 303..325, + }, + ], +} + +== types == +[E1023] Type Error: in `radius`: field `radius` cannot be projected from unrefined sum type `Shape` with 2 constructors; match a constructor first + ╭─[ :9:34 ] + │ + 9 │ fn radius(shape : Shape) : Int = shape.radius + │ ──────┬───── + │ ╰─────── in `radius`: field `radius` cannot be projected from unrefined sum type `Shape` with 2 constructors; match a constructor first +───╯ diff --git a/tests/snapshots/snapshots__pipeline@field_projection_shadow.pr.snap b/tests/snapshots/snapshots__pipeline@field_projection_shadow.pr.snap new file mode 100644 index 00000000..9087db16 --- /dev/null +++ b/tests/snapshots/snapshots__pipeline@field_projection_shadow.pr.snap @@ -0,0 +1,165 @@ +--- +source: tests/snapshots.rs +expression: "normalize_pipeline_report(&prism::report(&src))" +input_file: tests/cases/field_projection_shadow.pr +--- +== tokens == +VOpen Type UIdent("Shape") Eq UIdent("Circle") LBrace Ident("radius") Colon KwInt RBrace Bar UIdent("Square") LBrace Ident("side") Colon KwInt RBrace VSemi Fn Ident("radius") LParen Ident("_shape") Colon UIdent("Shape") RParen Colon KwInt Eq Int(IntLit { value: 99, suffix: None }) VSemi Fn Ident("read") LParen Ident("shape") Colon UIdent("Shape") RParen Colon KwInt Eq Ident("shape") Dot Ident("radius") VSemi Fn Ident("main") LParen RParen Eq Ident("println") LParen Int(IntLit { value: 0, suffix: None }) RParen VClose + +== ast == +Program { + types: [ + DataDecl { + name: "Shape", + params: [], + param_kinds: [], + ctors: [ + Ctor { + name: "Circle", + args: [ + Int, + ], + fields: Some( + [ + ( + "radius", + Int, + ), + ], + ), + }, + Ctor { + name: "Square", + args: [ + Int, + ], + fields: Some( + [ + ( + "side", + Int, + ), + ], + ), + }, + ], + deriving: [], + newtype: false, + span: 153..211, + }, + ], + effects: [], + errors: [], + aliases: [], + classes: [], + instances: [], + fns: [ + Decl { + name: "radius", + params: [ + Param { + name: "_shape", + ty: Some( + Con( + "Shape", + [], + ), + ), + borrow: false, + default: None, + }, + ], + ret: Some( + Int, + ), + eff: None, + constraints: [], + body: Spanned { + node: Int( + IntLit { + value: 99, + suffix: None, + }, + ), + span: 247..249, + }, + wheres: [], + span: 213..249, + }, + Decl { + name: "read", + params: [ + Param { + name: "shape", + ty: Some( + Con( + "Shape", + [], + ), + ), + borrow: false, + default: None, + }, + ], + ret: Some( + Int, + ), + eff: None, + constraints: [], + body: Spanned { + node: FieldAccess( + Spanned { + node: Var( + "shape", + ), + span: 282..287, + }, + "radius", + ), + span: 282..294, + }, + wheres: [], + span: 251..294, + }, + Decl { + name: "main", + params: [], + ret: None, + eff: None, + constraints: [], + body: Spanned { + node: Call( + Spanned { + node: Var( + "println", + ), + span: 308..315, + }, + [ + Spanned { + node: Int( + IntLit { + value: 0, + suffix: None, + }, + ), + span: 316..317, + }, + ], + ), + span: 308..318, + }, + wheres: [], + span: 296..318, + }, + ], +} + +== types == +[E1023] Type Error: in `read`: field `radius` cannot be projected from unrefined sum type `Shape` with 2 constructors; match a constructor first + ╭─[ :7:32 ] + │ + 7 │ fn read(shape : Shape) : Int = shape.radius + │ ──────┬───── + │ ╰─────── in `read`: field `radius` cannot be projected from unrefined sum type `Shape` with 2 constructors; match a constructor first +───╯ diff --git a/tests/snapshots/snapshots__pipeline@float_show.pr.snap b/tests/snapshots/snapshots__pipeline@float_show.pr.snap index 00471fa4..738ecd3a 100644 --- a/tests/snapshots/snapshots__pipeline@float_show.pr.snap +++ b/tests/snapshots/snapshots__pipeline@float_show.pr.snap @@ -295,8 +295,8 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@prism_native_kont_table = constant [741 x i8] c"scheme prism-core-hash-v2\0Abundle eb54ebe15adbea0ac0aa25991c8d36fa47ff5a51c77cfcc0940a6f0f5bdff40b\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:eb54ebe15adbea0ac0aa25991c8d36fa47ff5a51c77cfcc0940a6f0f5bdff40b\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_main 6bd78f57b8fc70fba80844780d5e86d9cd7124f79253b9f89c5ac2080bb2bf63 main\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [811 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle eb54ebe15adbea0ac0aa25991c8d36fa47ff5a51c77cfcc0940a6f0f5bdff40b\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:eb54ebe15adbea0ac0aa25991c8d36fa47ff5a51c77cfcc0940a6f0f5bdff40b\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main 6bd78f57b8fc70fba80844780d5e86d9cd7124f79253b9f89c5ac2080bb2bf63 main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [766 x i8] c"scheme prism-core-hash-v2\0Abundle eb54ebe15adbea0ac0aa25991c8d36fa47ff5a51c77cfcc0940a6f0f5bdff40b\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:eb54ebe15adbea0ac0aa25991c8d36fa47ff5a51c77cfcc0940a6f0f5bdff40b\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_main 6bd78f57b8fc70fba80844780d5e86d9cd7124f79253b9f89c5ac2080bb2bf63 main\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [836 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle eb54ebe15adbea0ac0aa25991c8d36fa47ff5a51c77cfcc0940a6f0f5bdff40b\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:eb54ebe15adbea0ac0aa25991c8d36fa47ff5a51c77cfcc0940a6f0f5bdff40b\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main 6bd78f57b8fc70fba80844780d5e86d9cd7124f79253b9f89c5ac2080bb2bf63 main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 @.kont_symbol0 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash0 = private constant [65 x i8] c"6bd78f57b8fc70fba80844780d5e86d9cd7124f79253b9f89c5ac2080bb2bf63\00" @.kont_name0 = private constant [5 x i8] c"main\00" diff --git a/tests/snapshots/snapshots__pipeline@forall_multi.pr.snap b/tests/snapshots/snapshots__pipeline@forall_multi.pr.snap index 094d8cd7..6d74df64 100644 --- a/tests/snapshots/snapshots__pipeline@forall_multi.pr.snap +++ b/tests/snapshots/snapshots__pipeline@forall_multi.pr.snap @@ -282,9 +282,9 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@.str0 = private constant [2 x i8] c"x\00" -@prism_native_kont_table = constant [834 x i8] c"scheme prism-core-hash-v2\0Abundle 110f1a11dc07448cc54911f677ec934ecabf6f074fc77c6c2d6e1b8014bb2c15\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:110f1a11dc07448cc54911f677ec934ecabf6f074fc77c6c2d6e1b8014bb2c15\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_main e755f47048bc2c6476bf3ce89a93b446e4cea41c4f420aab1c4a71334e4837d4 main\0Afn prismfn_pick 0f64e0e3878117ce4be36ecb5caeb2a99791d600936680da469db893ce4523e3 pick\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [938 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 110f1a11dc07448cc54911f677ec934ecabf6f074fc77c6c2d6e1b8014bb2c15\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:110f1a11dc07448cc54911f677ec934ecabf6f074fc77c6c2d6e1b8014bb2c15\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main e755f47048bc2c6476bf3ce89a93b446e4cea41c4f420aab1c4a71334e4837d4 main arity 0 slots abi-word[]\0Astate prismfn_pick 0f64e0e3878117ce4be36ecb5caeb2a99791d600936680da469db893ce4523e3 pick arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 +@.str0 = private constant { i64, i64, i64, [2 x i8] } { i64 1152921504606846976, i64 1398034944, i64 1, [2 x i8] c"x\00" }, align 8 +@prism_native_kont_table = constant [859 x i8] c"scheme prism-core-hash-v2\0Abundle 110f1a11dc07448cc54911f677ec934ecabf6f074fc77c6c2d6e1b8014bb2c15\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:110f1a11dc07448cc54911f677ec934ecabf6f074fc77c6c2d6e1b8014bb2c15\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_main e755f47048bc2c6476bf3ce89a93b446e4cea41c4f420aab1c4a71334e4837d4 main\0Afn prismfn_pick 0f64e0e3878117ce4be36ecb5caeb2a99791d600936680da469db893ce4523e3 pick\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [963 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 110f1a11dc07448cc54911f677ec934ecabf6f074fc77c6c2d6e1b8014bb2c15\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:110f1a11dc07448cc54911f677ec934ecabf6f074fc77c6c2d6e1b8014bb2c15\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main e755f47048bc2c6476bf3ce89a93b446e4cea41c4f420aab1c4a71334e4837d4 main arity 0 slots abi-word[]\0Astate prismfn_pick 0f64e0e3878117ce4be36ecb5caeb2a99791d600936680da469db893ce4523e3 pick arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 @.kont_symbol0 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash0 = private constant [65 x i8] c"e755f47048bc2c6476bf3ce89a93b446e4cea41c4f420aab1c4a71334e4837d4\00" @.kont_name0 = private constant [5 x i8] c"main\00" @@ -302,8 +302,7 @@ entry: call void @prism_rc_inc(i64 %t4) %t8 = call i64 @prismap_2(i64 %t4, i64 3, i64 3) call void @prism_rc_dec(i64 %t4) - %t9 = call i64 @prism_str_lit(ptr @.str0, i64 1) - %t10 = call i64 @prismap_2(i64 %t4, i64 %t8, i64 %t9) + %t10 = call i64 @prismap_2(i64 %t4, i64 %t8, i64 ptrtoint (ptr @.str0 to i64)) call void @prism_rc_dec(i64 %t4) %t12 = call ptr @prism_alloc(i64 0) %t13 = getelementptr inbounds i8, ptr %t12, i64 8 @@ -355,9 +354,6 @@ _merge: ; preds = %_lam621769673347206 ; Function Attrs: nounwind declare void @prism_rc_dec(i64) #0 -; Function Attrs: nounwind -declare i64 @prism_str_lit(ptr, i64) #0 - ; Function Attrs: nounwind declare void @prism_print_int(i64) #0 diff --git a/tests/snapshots/snapshots__pipeline@guard_true_exhaustive.pr.snap b/tests/snapshots/snapshots__pipeline@guard_true_exhaustive.pr.snap index a0bc421a..48c2e21e 100644 --- a/tests/snapshots/snapshots__pipeline@guard_true_exhaustive.pr.snap +++ b/tests/snapshots/snapshots__pipeline@guard_true_exhaustive.pr.snap @@ -159,40 +159,34 @@ fn classify(n) = t@0 == 0 to t@3 if t@3 then drop t@3 - drop t@0 return 0 else drop t@3 - dup t@0 return t@0 to t@1 - dup t@1 return t@1 to x - drop x return true to t@2 if t@2 then drop t@2 - drop t@0 return t@1 to x - drop x return 1 else case t@0 of _ => drop t@2 - drop t@1 - drop t@0 error "ICE: guarded match fell through" fn main() = return 5 to t@4 - classify(t@4) + classify(t@4) to %rc0 + drop t@4 + return %rc0 == llvm == ; ModuleID = 'prism' source_filename = "prism" -@.str0 = private constant [32 x i8] c"ICE: guarded match fell through\00" -@prism_native_kont_table = constant [842 x i8] c"scheme prism-core-hash-v2\0Abundle f28620b4a76b98610d263343b0ee903189d118dd81c46f3b4b1b454c0e3074f4\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:f28620b4a76b98610d263343b0ee903189d118dd81c46f3b4b1b454c0e3074f4\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_classify 967607b890fe4449f555e2636452dac9e152286cf90b5c26d7a80b49294fe1d6 classify\0Afn prismfn_main 6b9181ac407a2cfd6dd9baba542864657fa1cbd85668ec12c9291d2b547b9540 main\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [946 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle f28620b4a76b98610d263343b0ee903189d118dd81c46f3b4b1b454c0e3074f4\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:f28620b4a76b98610d263343b0ee903189d118dd81c46f3b4b1b454c0e3074f4\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_classify 967607b890fe4449f555e2636452dac9e152286cf90b5c26d7a80b49294fe1d6 classify arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_main 6b9181ac407a2cfd6dd9baba542864657fa1cbd85668ec12c9291d2b547b9540 main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 +@.str0 = private constant { i64, i64, i64, [32 x i8] } { i64 1152921504606846976, i64 1398034944, i64 31, [32 x i8] c"ICE: guarded match fell through\00" }, align 8 +@prism_native_kont_table = constant [867 x i8] c"scheme prism-core-hash-v2\0Abundle f28620b4a76b98610d263343b0ee903189d118dd81c46f3b4b1b454c0e3074f4\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:f28620b4a76b98610d263343b0ee903189d118dd81c46f3b4b1b454c0e3074f4\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_classify 967607b890fe4449f555e2636452dac9e152286cf90b5c26d7a80b49294fe1d6 classify\0Afn prismfn_main 6b9181ac407a2cfd6dd9baba542864657fa1cbd85668ec12c9291d2b547b9540 main\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [971 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle f28620b4a76b98610d263343b0ee903189d118dd81c46f3b4b1b454c0e3074f4\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:f28620b4a76b98610d263343b0ee903189d118dd81c46f3b4b1b454c0e3074f4\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_classify 967607b890fe4449f555e2636452dac9e152286cf90b5c26d7a80b49294fe1d6 classify arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_main 6b9181ac407a2cfd6dd9baba542864657fa1cbd85668ec12c9291d2b547b9540 main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 @.kont_symbol1 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash1 = private constant [65 x i8] c"6b9181ac407a2cfd6dd9baba542864657fa1cbd85668ec12c9291d2b547b9540\00" @.kont_name1 = private constant [5 x i8] c"main\00" @@ -215,14 +209,10 @@ b2: ; preds = %b1 ret i64 3 b3: ; preds = %b1 - %t8 = call i64 @prism_str_lit(ptr @.str0, i64 31) - call void @prism_fatal(i64 %t8) + call void @prism_fatal(i64 ptrtoint (ptr @.str0 to i64)) ret i64 0 } -; Function Attrs: nounwind -declare i64 @prism_str_lit(ptr, i64) #0 - ; Function Attrs: nounwind declare void @prism_fatal(i64) #0 diff --git a/tests/snapshots/snapshots__pipeline@higher_eff.pr.snap b/tests/snapshots/snapshots__pipeline@higher_eff.pr.snap index 2edafe60..8b56c222 100644 --- a/tests/snapshots/snapshots__pipeline@higher_eff.pr.snap +++ b/tests/snapshots/snapshots__pipeline@higher_eff.pr.snap @@ -321,10 +321,10 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@.str0 = private constant [54 x i8] c"ICE: effect op escaped a closed native handler clause\00" -@.str1 = private constant [50 x i8] c"ICE: unhandled effect op in closed native handler\00" -@prism_native_kont_table = constant [931 x i8] c"scheme prism-core-hash-v2\0Abundle c44d8b74cc4b812c3f52bc098a147724d58ba32945951aa0e3d37c43ba2403ab\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:c44d8b74cc4b812c3f52bc098a147724d58ba32945951aa0e3d37c43ba2403ab\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_apply f968938459a7e3129aca6f89acc029b564561d1aa95260cf6a17c2553cb123d9 apply\0Afn prismfn_main 38d06cfd7c4a92f1e60d7eb87723f3b49568d168d8274a677d2206fd3a6641eb main\0Afn prismfn_risky 96f656488c40c61dfc22eb23b8e8415f044781b4a68391481625e7f31ac56261 risky\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [1083 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle c44d8b74cc4b812c3f52bc098a147724d58ba32945951aa0e3d37c43ba2403ab\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:c44d8b74cc4b812c3f52bc098a147724d58ba32945951aa0e3d37c43ba2403ab\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_apply f968938459a7e3129aca6f89acc029b564561d1aa95260cf6a17c2553cb123d9 apply arity 2 slots abi-word[arg0=%a0:word,arg1=%a1:word]\0Astate prismfn_main 38d06cfd7c4a92f1e60d7eb87723f3b49568d168d8274a677d2206fd3a6641eb main arity 0 slots abi-word[]\0Astate prismfn_risky 96f656488c40c61dfc22eb23b8e8415f044781b4a68391481625e7f31ac56261 risky arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 +@.str0 = private constant { i64, i64, i64, [54 x i8] } { i64 1152921504606846976, i64 1398034944, i64 53, [54 x i8] c"ICE: effect op escaped a closed native handler clause\00" }, align 8 +@.str1 = private constant { i64, i64, i64, [50 x i8] } { i64 1152921504606846976, i64 1398034944, i64 49, [50 x i8] c"ICE: unhandled effect op in closed native handler\00" }, align 8 +@prism_native_kont_table = constant [956 x i8] c"scheme prism-core-hash-v2\0Abundle c44d8b74cc4b812c3f52bc098a147724d58ba32945951aa0e3d37c43ba2403ab\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:c44d8b74cc4b812c3f52bc098a147724d58ba32945951aa0e3d37c43ba2403ab\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_apply f968938459a7e3129aca6f89acc029b564561d1aa95260cf6a17c2553cb123d9 apply\0Afn prismfn_main 38d06cfd7c4a92f1e60d7eb87723f3b49568d168d8274a677d2206fd3a6641eb main\0Afn prismfn_risky 96f656488c40c61dfc22eb23b8e8415f044781b4a68391481625e7f31ac56261 risky\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [1108 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle c44d8b74cc4b812c3f52bc098a147724d58ba32945951aa0e3d37c43ba2403ab\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:c44d8b74cc4b812c3f52bc098a147724d58ba32945951aa0e3d37c43ba2403ab\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_apply f968938459a7e3129aca6f89acc029b564561d1aa95260cf6a17c2553cb123d9 apply arity 2 slots abi-word[arg0=%a0:word,arg1=%a1:word]\0Astate prismfn_main 38d06cfd7c4a92f1e60d7eb87723f3b49568d168d8274a677d2206fd3a6641eb main arity 0 slots abi-word[]\0Astate prismfn_risky 96f656488c40c61dfc22eb23b8e8415f044781b4a68391481625e7f31ac56261 risky arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 @.kont_symbol1 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash1 = private constant [65 x i8] c"38d06cfd7c4a92f1e60d7eb87723f3b49568d168d8274a677d2206fd3a6641eb\00" @.kont_name1 = private constant [5 x i8] c"main\00" @@ -503,8 +503,7 @@ b7: ; preds = %b6 b8: ; preds = %b6 call void @prism_rc_dec(i64 %t35) call void @prism_rc_dec(i64 %t12) - %t82 = call i64 @prism_str_lit(ptr @.str1, i64 49) - call void @prism_fatal(i64 %t82) + call void @prism_fatal(i64 ptrtoint (ptr @.str1 to i64)) ret i64 0 b11: ; preds = %b7 @@ -529,8 +528,7 @@ b12: ; preds = %b7 %t71 = getelementptr inbounds i8, ptr %t53, i64 48 %t72 = load i64, ptr %t71, align 8 call void @prism_rc_dec(i64 %t52) - %t74 = call i64 @prism_str_lit(ptr @.str0, i64 53) - call void @prism_fatal(i64 %t74) + call void @prism_fatal(i64 ptrtoint (ptr @.str0 to i64)) ret i64 0 b13: ; preds = %b7 @@ -650,9 +648,6 @@ b5: ; preds = %b3 unreachable } -; Function Attrs: nounwind -declare i64 @prism_str_lit(ptr, i64) #0 - ; Function Attrs: nounwind declare void @prism_fatal(i64) #0 diff --git a/tests/snapshots/snapshots__pipeline@instance_canonical.pr.snap b/tests/snapshots/snapshots__pipeline@instance_canonical.pr.snap index c692548a..12ca4b42 100644 --- a/tests/snapshots/snapshots__pipeline@instance_canonical.pr.snap +++ b/tests/snapshots/snapshots__pipeline@instance_canonical.pr.snap @@ -310,8 +310,8 @@ fn ordRev() = ; ModuleID = 'prism' source_filename = "prism" -@prism_native_kont_table = constant [1157 x i8] c"scheme prism-core-hash-v2\0Abundle e2b9d440297b39a25388429cc9f69efafeeb2ee86d9f695e9c61f6b313d2dbed\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:e2b9d440297b39a25388429cc9f69efafeeb2ee86d9f695e9c61f6b313d2dbed\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_iZaordIntZacmp 08eace05b71e4956caa3a8b2aa58770c0868212b4ffb0efa256c345e881d123f i@ordInt@cmp\0Afn prismfn_iZaordRevZacmp a55aaea16e55f530509d4217818614a9d0e14e791bf58d6f94bbe2e458551220 i@ordRev@cmp\0Afn prismfn_main ed0609e5d77f0f5f6ddbad114d0233b709bd2eff84c61b14ff0997af334528fd main\0Afn prismfn_ordInt 935765be9b0a38f5be50cc3b2c0969e4219c788fc93b8126e9a7e17ae3bc9306 ordInt\0Afn prismfn_ordRev d4d1cfc48a8fd97d9c43afdec88e431a31a0794bf17706d7d3e946076ef0ed4d ordRev\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [970 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle e2b9d440297b39a25388429cc9f69efafeeb2ee86d9f695e9c61f6b313d2dbed\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:e2b9d440297b39a25388429cc9f69efafeeb2ee86d9f695e9c61f6b313d2dbed\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_iZaordIntZacmp 08eace05b71e4956caa3a8b2aa58770c0868212b4ffb0efa256c345e881d123f i@ordInt@cmp arity 2 slots abi-word[arg0=%a0:word,arg1=%a1:word]\0Astate prismfn_main ed0609e5d77f0f5f6ddbad114d0233b709bd2eff84c61b14ff0997af334528fd main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [1182 x i8] c"scheme prism-core-hash-v2\0Abundle e2b9d440297b39a25388429cc9f69efafeeb2ee86d9f695e9c61f6b313d2dbed\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:e2b9d440297b39a25388429cc9f69efafeeb2ee86d9f695e9c61f6b313d2dbed\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_iZaordIntZacmp 08eace05b71e4956caa3a8b2aa58770c0868212b4ffb0efa256c345e881d123f i@ordInt@cmp\0Afn prismfn_iZaordRevZacmp a55aaea16e55f530509d4217818614a9d0e14e791bf58d6f94bbe2e458551220 i@ordRev@cmp\0Afn prismfn_main ed0609e5d77f0f5f6ddbad114d0233b709bd2eff84c61b14ff0997af334528fd main\0Afn prismfn_ordInt 935765be9b0a38f5be50cc3b2c0969e4219c788fc93b8126e9a7e17ae3bc9306 ordInt\0Afn prismfn_ordRev d4d1cfc48a8fd97d9c43afdec88e431a31a0794bf17706d7d3e946076ef0ed4d ordRev\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [995 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle e2b9d440297b39a25388429cc9f69efafeeb2ee86d9f695e9c61f6b313d2dbed\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:e2b9d440297b39a25388429cc9f69efafeeb2ee86d9f695e9c61f6b313d2dbed\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_iZaordIntZacmp 08eace05b71e4956caa3a8b2aa58770c0868212b4ffb0efa256c345e881d123f i@ordInt@cmp arity 2 slots abi-word[arg0=%a0:word,arg1=%a1:word]\0Astate prismfn_main ed0609e5d77f0f5f6ddbad114d0233b709bd2eff84c61b14ff0997af334528fd main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 @.kont_symbol2 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash2 = private constant [65 x i8] c"ed0609e5d77f0f5f6ddbad114d0233b709bd2eff84c61b14ff0997af334528fd\00" @.kont_name2 = private constant [5 x i8] c"main\00" diff --git a/tests/snapshots/snapshots__pipeline@list.pr.snap b/tests/snapshots/snapshots__pipeline@list.pr.snap index 63a4b1de..0a4513da 100644 --- a/tests/snapshots/snapshots__pipeline@list.pr.snap +++ b/tests/snapshots/snapshots__pipeline@list.pr.snap @@ -297,11 +297,8 @@ fn length(xs) = return xs to t@0 case t@0 of Nil => - drop t@0 return 0 Cons(x, rest) => - dup rest - drop t@0 return 1 to t@2 return rest to t@1 length(t@1) to t@3 @@ -314,7 +311,9 @@ fn main() = return Cons(t@6, t@7) to t@8 return Cons(t@5, t@8) to t@9 return Cons(t@4, t@9) to t@10 - length(t@10) to t@11 + length(t@10) to %rc0 + drop t@10 + return %rc0 to t@11 print t@11 to t@12 drop t@12 print_nl @@ -323,8 +322,8 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@prism_native_kont_table = constant [838 x i8] c"scheme prism-core-hash-v2\0Abundle 55b3a6c1fe8f9d6dd117bc6f249475a40830201c19bb39fe6d58ed9ec88019e8\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:55b3a6c1fe8f9d6dd117bc6f249475a40830201c19bb39fe6d58ed9ec88019e8\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_length fc90c78988febcf91456aff540045b2b2beb2cabc5163e5eeb2b7b0bb7a151d9 length\0Afn prismfn_main f6058102113fd0a4b6ca30ae501dbcefa5a11efc7089d99a9e5c021ae7406e21 main\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [942 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 55b3a6c1fe8f9d6dd117bc6f249475a40830201c19bb39fe6d58ed9ec88019e8\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:55b3a6c1fe8f9d6dd117bc6f249475a40830201c19bb39fe6d58ed9ec88019e8\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_length fc90c78988febcf91456aff540045b2b2beb2cabc5163e5eeb2b7b0bb7a151d9 length arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_main f6058102113fd0a4b6ca30ae501dbcefa5a11efc7089d99a9e5c021ae7406e21 main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [863 x i8] c"scheme prism-core-hash-v2\0Abundle 55b3a6c1fe8f9d6dd117bc6f249475a40830201c19bb39fe6d58ed9ec88019e8\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:55b3a6c1fe8f9d6dd117bc6f249475a40830201c19bb39fe6d58ed9ec88019e8\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_length fc90c78988febcf91456aff540045b2b2beb2cabc5163e5eeb2b7b0bb7a151d9 length\0Afn prismfn_main f6058102113fd0a4b6ca30ae501dbcefa5a11efc7089d99a9e5c021ae7406e21 main\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [967 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 55b3a6c1fe8f9d6dd117bc6f249475a40830201c19bb39fe6d58ed9ec88019e8\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:55b3a6c1fe8f9d6dd117bc6f249475a40830201c19bb39fe6d58ed9ec88019e8\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_length fc90c78988febcf91456aff540045b2b2beb2cabc5163e5eeb2b7b0bb7a151d9 length arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_main f6058102113fd0a4b6ca30ae501dbcefa5a11efc7089d99a9e5c021ae7406e21 main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 @.kont_symbol0 = private constant [15 x i8] c"prismfn_length\00" @.kont_hash0 = private constant [65 x i8] c"fc90c78988febcf91456aff540045b2b2beb2cabc5163e5eeb2b7b0bb7a151d9\00" @.kont_name0 = private constant [7 x i8] c"length\00" @@ -347,85 +346,79 @@ entry: ] b2: ; preds = %entry - call void @prism_rc_dec(i64 %a0) - %t5 = and i64 1, %a1 - %t7 = and i64 %t5, 1 - %t8 = icmp eq i64 %t7, 1 - br i1 %t8, label %b4, label %b5 + %t4 = and i64 1, %a1 + %t6 = and i64 %t4, 1 + %t7 = icmp eq i64 %t6, 1 + br i1 %t7, label %b4, label %b5 b3: ; preds = %entry - %t21 = getelementptr inbounds i8, ptr %t0, i64 24 - %t22 = load i64, ptr %t21, align 8 - %t23 = getelementptr inbounds i8, ptr %t0, i64 32 - %t24 = load i64, ptr %t23, align 8 - call void @prism_rc_inc(i64 %t24) - call void @prism_rc_dec(i64 %a0) - %t28 = and i64 %a1, 3 - %t30 = and i64 %t28, 1 - %t31 = icmp eq i64 %t30, 1 - br i1 %t31, label %b8, label %b9 + %t20 = getelementptr inbounds i8, ptr %t0, i64 24 + %t21 = load i64, ptr %t20, align 8 + %t22 = getelementptr inbounds i8, ptr %t0, i64 32 + %t23 = load i64, ptr %t22, align 8 + %t25 = and i64 %a1, 3 + %t27 = and i64 %t25, 1 + %t28 = icmp eq i64 %t27, 1 + br i1 %t28, label %b8, label %b9 b1: ; preds = %entry call void @prism_match_error() unreachable b4: ; preds = %b2 - %t12 = ashr i64 %a1, 1 - %t13 = add i64 0, %t12 - %t15 = shl i64 %t13, 1 - %t16 = ashr i64 %t15, 1 - %t17 = icmp eq i64 %t16, %t13 - br i1 %t17, label %b7, label %b5 + %t11 = ashr i64 %a1, 1 + %t12 = add i64 0, %t11 + %t14 = shl i64 %t12, 1 + %t15 = ashr i64 %t14, 1 + %t16 = icmp eq i64 %t15, %t12 + br i1 %t16, label %b7, label %b5 b5: ; preds = %b4, %b2 - %t19 = call i64 @prism_rt_int_add(i64 1, i64 %a1) + %t18 = call i64 @prism_rt_int_add(i64 1, i64 %a1) br label %b6 b7: ; preds = %b4 - %t18 = or i64 %t15, 1 + %t17 = or i64 %t14, 1 br label %b6 b6: ; preds = %b5, %b7 - %t20 = phi i64 [ %t18, %b7 ], [ %t19, %b5 ] + %t19 = phi i64 [ %t17, %b7 ], [ %t18, %b5 ] call void @prism_rc_dec(i64 1) call void @prism_rc_dec(i64 %a1) - ret i64 %t20 + ret i64 %t19 b8: ; preds = %b3 - %t33 = ashr i64 %a1, 1 - %t36 = add i64 %t33, 1 - %t38 = shl i64 %t36, 1 - %t39 = ashr i64 %t38, 1 - %t40 = icmp eq i64 %t39, %t36 - br i1 %t40, label %b11, label %b9 + %t30 = ashr i64 %a1, 1 + %t33 = add i64 %t30, 1 + %t35 = shl i64 %t33, 1 + %t36 = ashr i64 %t35, 1 + %t37 = icmp eq i64 %t36, %t33 + br i1 %t37, label %b11, label %b9 b9: ; preds = %b8, %b3 - %t42 = call i64 @prism_rt_int_add(i64 %a1, i64 3) + %t39 = call i64 @prism_rt_int_add(i64 %a1, i64 3) br label %b10 b11: ; preds = %b8 - %t41 = or i64 %t38, 1 + %t38 = or i64 %t35, 1 br label %b10 b10: ; preds = %b9, %b11 - %t43 = phi i64 [ %t41, %b11 ], [ %t42, %b9 ] + %t40 = phi i64 [ %t38, %b11 ], [ %t39, %b9 ] call void @prism_rc_dec(i64 %a1) call void @prism_rc_dec(i64 3) - %t44 = musttail call i64 @prismtrmc_length(i64 %t24, i64 %t43) - ret i64 %t44 + %t41 = musttail call i64 @prismtrmc_length(i64 %t23, i64 %t40) + ret i64 %t41 } ; Function Attrs: nounwind declare void @prism_match_error() #0 -; Function Attrs: nounwind -declare void @prism_rc_dec(i64) #0 - ; Function Attrs: nounwind declare i64 @prism_rt_int_add(i64, i64) #0 ; Function Attrs: nounwind -declare void @prism_rc_inc(i64) #0 +declare void @prism_rc_dec(i64) #0 ; Function Attrs: nounwind define i64 @prismfn_length(i64 %a0) #0 { @@ -466,6 +459,7 @@ entry: store i64 %t20, ptr %t27, align 8 %t28 = ptrtoint ptr %t23 to i64 %t29 = call i64 @prismfn_length(i64 %t28) + call void @prism_rc_dec(i64 %t28) call void @prism_print_int(i64 %t29) call void @prism_rc_dec(i64 %t29) call void @prism_rc_dec(i64 0) diff --git a/tests/snapshots/snapshots__pipeline@mask_tunnels_to_outer.pr.snap b/tests/snapshots/snapshots__pipeline@mask_tunnels_to_outer.pr.snap index 7163af5c..290e5974 100644 --- a/tests/snapshots/snapshots__pipeline@mask_tunnels_to_outer.pr.snap +++ b/tests/snapshots/snapshots__pipeline@mask_tunnels_to_outer.pr.snap @@ -296,10 +296,10 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@.str0 = private constant [54 x i8] c"ICE: effect op escaped a closed native handler clause\00" -@.str1 = private constant [50 x i8] c"ICE: unhandled effect op in closed native handler\00" -@prism_native_kont_table = constant [828 x i8] c"scheme prism-core-hash-v2\0Abundle 0f8bf956b3481d5e165799d34adc7ae7b675823f9a06b5943685ebd23e1e7b93\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:0f8bf956b3481d5e165799d34adc7ae7b675823f9a06b5943685ebd23e1e7b93\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_f 4fbaf5d325b34ff4e184f1b4fff1f235a25719559ddf774aed56bf6e5ed0625b f\0Afn prismfn_main b45af925c22facc0b2e7c9e6e4fb016e3d035e63c8bf2b404363b14357bccc64 main\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [919 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 0f8bf956b3481d5e165799d34adc7ae7b675823f9a06b5943685ebd23e1e7b93\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:0f8bf956b3481d5e165799d34adc7ae7b675823f9a06b5943685ebd23e1e7b93\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_f 4fbaf5d325b34ff4e184f1b4fff1f235a25719559ddf774aed56bf6e5ed0625b f arity 0 slots abi-word[]\0Astate prismfn_main b45af925c22facc0b2e7c9e6e4fb016e3d035e63c8bf2b404363b14357bccc64 main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 +@.str0 = private constant { i64, i64, i64, [54 x i8] } { i64 1152921504606846976, i64 1398034944, i64 53, [54 x i8] c"ICE: effect op escaped a closed native handler clause\00" }, align 8 +@.str1 = private constant { i64, i64, i64, [50 x i8] } { i64 1152921504606846976, i64 1398034944, i64 49, [50 x i8] c"ICE: unhandled effect op in closed native handler\00" }, align 8 +@prism_native_kont_table = constant [853 x i8] c"scheme prism-core-hash-v2\0Abundle 0f8bf956b3481d5e165799d34adc7ae7b675823f9a06b5943685ebd23e1e7b93\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:0f8bf956b3481d5e165799d34adc7ae7b675823f9a06b5943685ebd23e1e7b93\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_f 4fbaf5d325b34ff4e184f1b4fff1f235a25719559ddf774aed56bf6e5ed0625b f\0Afn prismfn_main b45af925c22facc0b2e7c9e6e4fb016e3d035e63c8bf2b404363b14357bccc64 main\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [944 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 0f8bf956b3481d5e165799d34adc7ae7b675823f9a06b5943685ebd23e1e7b93\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:0f8bf956b3481d5e165799d34adc7ae7b675823f9a06b5943685ebd23e1e7b93\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_f 4fbaf5d325b34ff4e184f1b4fff1f235a25719559ddf774aed56bf6e5ed0625b f arity 0 slots abi-word[]\0Astate prismfn_main b45af925c22facc0b2e7c9e6e4fb016e3d035e63c8bf2b404363b14357bccc64 main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 @.kont_symbol1 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash1 = private constant [65 x i8] c"b45af925c22facc0b2e7c9e6e4fb016e3d035e63c8bf2b404363b14357bccc64\00" @.kont_name1 = private constant [5 x i8] c"main\00" @@ -836,8 +836,7 @@ b8: ; preds = %b6 call void @prism_rc_dec(i64 %t36) call void @prism_rc_dec(i64 %t14) call void @prism_rc_dec(i64 %t12) - %t89 = call i64 @prism_str_lit(ptr @.str1, i64 49) - call void @prism_fatal(i64 %t89) + call void @prism_fatal(i64 ptrtoint (ptr @.str1 to i64)) ret i64 0 b11: ; preds = %b7 @@ -862,8 +861,7 @@ b12: ; preds = %b7 %t77 = getelementptr inbounds i8, ptr %t59, i64 48 %t78 = load i64, ptr %t77, align 8 call void @prism_rc_dec(i64 %t58) - %t80 = call i64 @prism_str_lit(ptr @.str0, i64 53) - call void @prism_fatal(i64 %t80) + call void @prism_fatal(i64 ptrtoint (ptr @.str0 to i64)) ret i64 0 b13: ; preds = %b7 @@ -1004,9 +1002,6 @@ b5: ; preds = %b3 unreachable } -; Function Attrs: nounwind -declare i64 @prism_str_lit(ptr, i64) #0 - ; Function Attrs: nounwind declare void @prism_fatal(i64) #0 diff --git a/tests/snapshots/snapshots__pipeline@mod_zero.pr.snap b/tests/snapshots/snapshots__pipeline@mod_zero.pr.snap index 3319bf1f..810fbd69 100644 --- a/tests/snapshots/snapshots__pipeline@mod_zero.pr.snap +++ b/tests/snapshots/snapshots__pipeline@mod_zero.pr.snap @@ -70,8 +70,8 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@prism_native_kont_table = constant [741 x i8] c"scheme prism-core-hash-v2\0Abundle 86e049c3169ca467e418fe36f79dd8c51c9e8c27ad7b628ecd9ef42dd7325c84\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:86e049c3169ca467e418fe36f79dd8c51c9e8c27ad7b628ecd9ef42dd7325c84\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_main f6dbadc1d664217cfae18d3b5054e1b1a2bfbe888289a16245fad8b68f966cdd main\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [811 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 86e049c3169ca467e418fe36f79dd8c51c9e8c27ad7b628ecd9ef42dd7325c84\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:86e049c3169ca467e418fe36f79dd8c51c9e8c27ad7b628ecd9ef42dd7325c84\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main f6dbadc1d664217cfae18d3b5054e1b1a2bfbe888289a16245fad8b68f966cdd main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [766 x i8] c"scheme prism-core-hash-v2\0Abundle 86e049c3169ca467e418fe36f79dd8c51c9e8c27ad7b628ecd9ef42dd7325c84\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:86e049c3169ca467e418fe36f79dd8c51c9e8c27ad7b628ecd9ef42dd7325c84\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_main f6dbadc1d664217cfae18d3b5054e1b1a2bfbe888289a16245fad8b68f966cdd main\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [836 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 86e049c3169ca467e418fe36f79dd8c51c9e8c27ad7b628ecd9ef42dd7325c84\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:86e049c3169ca467e418fe36f79dd8c51c9e8c27ad7b628ecd9ef42dd7325c84\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main f6dbadc1d664217cfae18d3b5054e1b1a2bfbe888289a16245fad8b68f966cdd main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 @.kont_symbol0 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash0 = private constant [65 x i8] c"f6dbadc1d664217cfae18d3b5054e1b1a2bfbe888289a16245fad8b68f966cdd\00" @.kont_name0 = private constant [5 x i8] c"main\00" diff --git a/tests/snapshots/snapshots__pipeline@multishot.pr.snap b/tests/snapshots/snapshots__pipeline@multishot.pr.snap index 97382a02..0192acbe 100644 --- a/tests/snapshots/snapshots__pipeline@multishot.pr.snap +++ b/tests/snapshots/snapshots__pipeline@multishot.pr.snap @@ -378,8 +378,8 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@prism_native_kont_table = constant [852 x i8] c"scheme prism-core-hash-v2\0Abundle 5fd09514b427a4c2ca91209e8e722ff81c431d8c4179ced05f319678d3496a3e\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:5fd09514b427a4c2ca91209e8e722ff81c431d8c4179ced05f319678d3496a3e\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_collect_bools ff4c58a944aa1a2c2f0c7c08b7048afc01dc161ae6df9a2df3738e04f18f7681 collect_bools\0Afn prismfn_main 3ad795d96fb5ec587443812935103608150fed2f3e086a95309046bbc5d69bd9 main\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [943 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 5fd09514b427a4c2ca91209e8e722ff81c431d8c4179ced05f319678d3496a3e\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:5fd09514b427a4c2ca91209e8e722ff81c431d8c4179ced05f319678d3496a3e\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_collect_bools ff4c58a944aa1a2c2f0c7c08b7048afc01dc161ae6df9a2df3738e04f18f7681 collect_bools arity 0 slots abi-word[]\0Astate prismfn_main 3ad795d96fb5ec587443812935103608150fed2f3e086a95309046bbc5d69bd9 main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [877 x i8] c"scheme prism-core-hash-v2\0Abundle 5fd09514b427a4c2ca91209e8e722ff81c431d8c4179ced05f319678d3496a3e\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:5fd09514b427a4c2ca91209e8e722ff81c431d8c4179ced05f319678d3496a3e\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_collect_bools ff4c58a944aa1a2c2f0c7c08b7048afc01dc161ae6df9a2df3738e04f18f7681 collect_bools\0Afn prismfn_main 3ad795d96fb5ec587443812935103608150fed2f3e086a95309046bbc5d69bd9 main\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [968 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 5fd09514b427a4c2ca91209e8e722ff81c431d8c4179ced05f319678d3496a3e\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:5fd09514b427a4c2ca91209e8e722ff81c431d8c4179ced05f319678d3496a3e\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_collect_bools ff4c58a944aa1a2c2f0c7c08b7048afc01dc161ae6df9a2df3738e04f18f7681 collect_bools arity 0 slots abi-word[]\0Astate prismfn_main 3ad795d96fb5ec587443812935103608150fed2f3e086a95309046bbc5d69bd9 main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 @.kont_symbol1 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash1 = private constant [65 x i8] c"3ad795d96fb5ec587443812935103608150fed2f3e086a95309046bbc5d69bd9\00" @.kont_name1 = private constant [5 x i8] c"main\00" diff --git a/tests/snapshots/snapshots__pipeline@no_alloc_ok.pr.snap b/tests/snapshots/snapshots__pipeline@no_alloc_ok.pr.snap index 3afcd084..eab8bd77 100644 --- a/tests/snapshots/snapshots__pipeline@no_alloc_ok.pr.snap +++ b/tests/snapshots/snapshots__pipeline@no_alloc_ok.pr.snap @@ -181,8 +181,8 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@prism_native_kont_table = constant [927 x i8] c"scheme prism-core-hash-v2\0Abundle c0032d94837a7399fd75a26f1a09a24ba8beb40417f978c9b389feaf916ee4de\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:c0032d94837a7399fd75a26f1a09a24ba8beb40417f978c9b389feaf916ee4de\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_inc 3bc0c9d37b29baabc8c21448af21633e47aa613063258033cf3593dafe0ce507 inc\0Afn prismfn_main 7f4d8aad335420755f5ddf21dc7b7f1b05f9ada64e7c1d7b510dc6ee51139edf main\0Afn prismfn_twice c270ad901364757576b6067c5d058f658e08e1d86e624fd7737aaea7d0e7e086 twice\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [1065 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle c0032d94837a7399fd75a26f1a09a24ba8beb40417f978c9b389feaf916ee4de\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:c0032d94837a7399fd75a26f1a09a24ba8beb40417f978c9b389feaf916ee4de\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_inc 3bc0c9d37b29baabc8c21448af21633e47aa613063258033cf3593dafe0ce507 inc arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_main 7f4d8aad335420755f5ddf21dc7b7f1b05f9ada64e7c1d7b510dc6ee51139edf main arity 0 slots abi-word[]\0Astate prismfn_twice c270ad901364757576b6067c5d058f658e08e1d86e624fd7737aaea7d0e7e086 twice arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [952 x i8] c"scheme prism-core-hash-v2\0Abundle c0032d94837a7399fd75a26f1a09a24ba8beb40417f978c9b389feaf916ee4de\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:c0032d94837a7399fd75a26f1a09a24ba8beb40417f978c9b389feaf916ee4de\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_inc 3bc0c9d37b29baabc8c21448af21633e47aa613063258033cf3593dafe0ce507 inc\0Afn prismfn_main 7f4d8aad335420755f5ddf21dc7b7f1b05f9ada64e7c1d7b510dc6ee51139edf main\0Afn prismfn_twice c270ad901364757576b6067c5d058f658e08e1d86e624fd7737aaea7d0e7e086 twice\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [1090 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle c0032d94837a7399fd75a26f1a09a24ba8beb40417f978c9b389feaf916ee4de\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:c0032d94837a7399fd75a26f1a09a24ba8beb40417f978c9b389feaf916ee4de\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_inc 3bc0c9d37b29baabc8c21448af21633e47aa613063258033cf3593dafe0ce507 inc arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_main 7f4d8aad335420755f5ddf21dc7b7f1b05f9ada64e7c1d7b510dc6ee51139edf main arity 0 slots abi-word[]\0Astate prismfn_twice c270ad901364757576b6067c5d058f658e08e1d86e624fd7737aaea7d0e7e086 twice arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 @.kont_symbol0 = private constant [12 x i8] c"prismfn_inc\00" @.kont_hash0 = private constant [65 x i8] c"3bc0c9d37b29baabc8c21448af21633e47aa613063258033cf3593dafe0ce507\00" @.kont_name0 = private constant [4 x i8] c"inc\00" diff --git a/tests/snapshots/snapshots__pipeline@no_alloc_region.pr.snap b/tests/snapshots/snapshots__pipeline@no_alloc_region.pr.snap index 7558e2d7..a354438b 100644 --- a/tests/snapshots/snapshots__pipeline@no_alloc_region.pr.snap +++ b/tests/snapshots/snapshots__pipeline@no_alloc_region.pr.snap @@ -185,19 +185,22 @@ fn tick_region(base) = fn tick(n) = return n to t@2 return 1 to t@3 + dup t@2 t@2 + t@3 to base return base to t@4 tick_region(t@4) fn main() = return 20 to t@5 - tick(t@5) + tick(t@5) to %rc0 + drop t@5 + return %rc0 == llvm == ; ModuleID = 'prism' source_filename = "prism" -@prism_native_kont_table = constant [941 x i8] c"scheme prism-core-hash-v2\0Abundle 03bd2af20d02ae9464295b50c3cba83eae53b97b162e95841b95de2a56cecc1c\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:03bd2af20d02ae9464295b50c3cba83eae53b97b162e95841b95de2a56cecc1c\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_main f973fd72479423cad9a3ac6a417f8c9b34ff34abb0006891638a345a08135c8a main\0Afn prismfn_tick 4625f54ce6e8bbda0a01d619c77509114d2bc9a7824edb29d4024f5f325e88e9 tick\0Afn prismfn_tick_region edce46a4c96a5041fb2171cafb9bd30366e29d2d5f63b78a8cfc0648c6e5aff6 tick_region\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [1079 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 03bd2af20d02ae9464295b50c3cba83eae53b97b162e95841b95de2a56cecc1c\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:03bd2af20d02ae9464295b50c3cba83eae53b97b162e95841b95de2a56cecc1c\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main f973fd72479423cad9a3ac6a417f8c9b34ff34abb0006891638a345a08135c8a main arity 0 slots abi-word[]\0Astate prismfn_tick 4625f54ce6e8bbda0a01d619c77509114d2bc9a7824edb29d4024f5f325e88e9 tick arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_tick_region edce46a4c96a5041fb2171cafb9bd30366e29d2d5f63b78a8cfc0648c6e5aff6 tick_region arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [966 x i8] c"scheme prism-core-hash-v2\0Abundle 03bd2af20d02ae9464295b50c3cba83eae53b97b162e95841b95de2a56cecc1c\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:03bd2af20d02ae9464295b50c3cba83eae53b97b162e95841b95de2a56cecc1c\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_main f973fd72479423cad9a3ac6a417f8c9b34ff34abb0006891638a345a08135c8a main\0Afn prismfn_tick 4625f54ce6e8bbda0a01d619c77509114d2bc9a7824edb29d4024f5f325e88e9 tick\0Afn prismfn_tick_region edce46a4c96a5041fb2171cafb9bd30366e29d2d5f63b78a8cfc0648c6e5aff6 tick_region\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [1104 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 03bd2af20d02ae9464295b50c3cba83eae53b97b162e95841b95de2a56cecc1c\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:03bd2af20d02ae9464295b50c3cba83eae53b97b162e95841b95de2a56cecc1c\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main f973fd72479423cad9a3ac6a417f8c9b34ff34abb0006891638a345a08135c8a main arity 0 slots abi-word[]\0Astate prismfn_tick 4625f54ce6e8bbda0a01d619c77509114d2bc9a7824edb29d4024f5f325e88e9 tick arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_tick_region edce46a4c96a5041fb2171cafb9bd30366e29d2d5f63b78a8cfc0648c6e5aff6 tick_region arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 @.kont_symbol0 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash0 = private constant [65 x i8] c"f973fd72479423cad9a3ac6a417f8c9b34ff34abb0006891638a345a08135c8a\00" @.kont_name0 = private constant [5 x i8] c"main\00" diff --git a/tests/snapshots/snapshots__pipeline@option.pr.snap b/tests/snapshots/snapshots__pipeline@option.pr.snap index 60ab48b5..b7fe8b20 100644 --- a/tests/snapshots/snapshots__pipeline@option.pr.snap +++ b/tests/snapshots/snapshots__pipeline@option.pr.snap @@ -210,18 +210,18 @@ fn get(d, o) = return o to t@0 case t@0 of None => - drop t@0 return d Some(x) => - dup x - drop t@0 drop d + dup x return x fn main() = return 0 to t@1 return 42 to t@2 return Some(t@2) to t@3 - get(t@1, t@3) to t@4 + get(t@1, t@3) to %rc0 + drop t@3 + return %rc0 to t@4 print t@4 to t@5 drop t@5 print_nl @@ -230,8 +230,8 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@prism_native_kont_table = constant [832 x i8] c"scheme prism-core-hash-v2\0Abundle c1f5095f12f2aceef575ca27c01e6dc3846c4c29f6d9d61add13b25351c5f093\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:c1f5095f12f2aceef575ca27c01e6dc3846c4c29f6d9d61add13b25351c5f093\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_get b9255037359b98ab790750679691acbf15d6592fcd7b2021c46822b4d541fa8a get\0Afn prismfn_main 3fdee20d319ea5152b2267863ec85070b7e397dc393ab4f11fccb54589a16722 main\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [950 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle c1f5095f12f2aceef575ca27c01e6dc3846c4c29f6d9d61add13b25351c5f093\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:c1f5095f12f2aceef575ca27c01e6dc3846c4c29f6d9d61add13b25351c5f093\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_get b9255037359b98ab790750679691acbf15d6592fcd7b2021c46822b4d541fa8a get arity 2 slots abi-word[arg0=%a0:word,arg1=%a1:word]\0Astate prismfn_main 3fdee20d319ea5152b2267863ec85070b7e397dc393ab4f11fccb54589a16722 main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [857 x i8] c"scheme prism-core-hash-v2\0Abundle c1f5095f12f2aceef575ca27c01e6dc3846c4c29f6d9d61add13b25351c5f093\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:c1f5095f12f2aceef575ca27c01e6dc3846c4c29f6d9d61add13b25351c5f093\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_get b9255037359b98ab790750679691acbf15d6592fcd7b2021c46822b4d541fa8a get\0Afn prismfn_main 3fdee20d319ea5152b2267863ec85070b7e397dc393ab4f11fccb54589a16722 main\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [975 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle c1f5095f12f2aceef575ca27c01e6dc3846c4c29f6d9d61add13b25351c5f093\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:c1f5095f12f2aceef575ca27c01e6dc3846c4c29f6d9d61add13b25351c5f093\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_get b9255037359b98ab790750679691acbf15d6592fcd7b2021c46822b4d541fa8a get arity 2 slots abi-word[arg0=%a0:word,arg1=%a1:word]\0Astate prismfn_main 3fdee20d319ea5152b2267863ec85070b7e397dc393ab4f11fccb54589a16722 main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 @.kont_symbol1 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash1 = private constant [65 x i8] c"3fdee20d319ea5152b2267863ec85070b7e397dc393ab4f11fccb54589a16722\00" @.kont_name1 = private constant [5 x i8] c"main\00" diff --git a/tests/snapshots/snapshots__pipeline@or_null.pr.snap b/tests/snapshots/snapshots__pipeline@or_null.pr.snap index dba36532..98d2a43d 100644 --- a/tests/snapshots/snapshots__pipeline@or_null.pr.snap +++ b/tests/snapshots/snapshots__pipeline@or_null.pr.snap @@ -317,40 +317,44 @@ fn main() = fn find(hit) = return hit to t@0 if t@0 then - drop t@0 return 7 to t@1 return This(t@1) else - drop t@0 return Null fn unwrap_or(o, d) = return o to t@2 case t@2 of Null => - drop t@2 return d This(n) => - dup n - drop t@2 drop d + dup n return n fn main() = return true to t@3 - find(t@3) to t@4 + find(t@3) to %rc0 + drop t@3 + return %rc0 to t@4 return 0 to t@5 - unwrap_or(t@4, t@5) to t@9 + unwrap_or(t@4, t@5) to %rc1 + drop t@4 + return %rc1 to t@9 return false to t@6 - find(t@6) to t@7 + find(t@6) to %rc2 + drop t@6 + return %rc2 to t@7 return 100 to t@8 - unwrap_or(t@7, t@8) to t@10 + unwrap_or(t@7, t@8) to %rc3 + drop t@7 + return %rc3 to t@10 t@9 + t@10 == llvm == ; ModuleID = 'prism' source_filename = "prism" -@prism_native_kont_table = constant [937 x i8] c"scheme prism-core-hash-v2\0Abundle db7eaba7e2e8c4c5970fcd0a6209656102eb7a8275aa50583bcf2d75ed42fe31\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:db7eaba7e2e8c4c5970fcd0a6209656102eb7a8275aa50583bcf2d75ed42fe31\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_find fa17afdaed2825aa2fd6698e8c76a9e486fc2595908c9942a58a90fc30b47005 find\0Afn prismfn_main e9ce96b8cc0365fa6e337282b8e3b76b21fe029acc47396aad843d13ec3aba19 main\0Afn prismfn_unwrap_or 97f5ccddd3f26be4de5022881b65e0019b7c1fed9f30a65eb5a79ccc09a31036 unwrap_or\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [1089 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle db7eaba7e2e8c4c5970fcd0a6209656102eb7a8275aa50583bcf2d75ed42fe31\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:db7eaba7e2e8c4c5970fcd0a6209656102eb7a8275aa50583bcf2d75ed42fe31\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_find fa17afdaed2825aa2fd6698e8c76a9e486fc2595908c9942a58a90fc30b47005 find arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_main e9ce96b8cc0365fa6e337282b8e3b76b21fe029acc47396aad843d13ec3aba19 main arity 0 slots abi-word[]\0Astate prismfn_unwrap_or 97f5ccddd3f26be4de5022881b65e0019b7c1fed9f30a65eb5a79ccc09a31036 unwrap_or arity 2 slots abi-word[arg0=%a0:word,arg1=%a1:word]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [962 x i8] c"scheme prism-core-hash-v2\0Abundle db7eaba7e2e8c4c5970fcd0a6209656102eb7a8275aa50583bcf2d75ed42fe31\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:db7eaba7e2e8c4c5970fcd0a6209656102eb7a8275aa50583bcf2d75ed42fe31\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_find fa17afdaed2825aa2fd6698e8c76a9e486fc2595908c9942a58a90fc30b47005 find\0Afn prismfn_main e9ce96b8cc0365fa6e337282b8e3b76b21fe029acc47396aad843d13ec3aba19 main\0Afn prismfn_unwrap_or 97f5ccddd3f26be4de5022881b65e0019b7c1fed9f30a65eb5a79ccc09a31036 unwrap_or\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [1114 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle db7eaba7e2e8c4c5970fcd0a6209656102eb7a8275aa50583bcf2d75ed42fe31\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:db7eaba7e2e8c4c5970fcd0a6209656102eb7a8275aa50583bcf2d75ed42fe31\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_find fa17afdaed2825aa2fd6698e8c76a9e486fc2595908c9942a58a90fc30b47005 find arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_main e9ce96b8cc0365fa6e337282b8e3b76b21fe029acc47396aad843d13ec3aba19 main arity 0 slots abi-word[]\0Astate prismfn_unwrap_or 97f5ccddd3f26be4de5022881b65e0019b7c1fed9f30a65eb5a79ccc09a31036 unwrap_or arity 2 slots abi-word[arg0=%a0:word,arg1=%a1:word]\0A\00", section ",.prism_kont", align 1 @.kont_symbol0 = private constant [13 x i8] c"prismfn_find\00" @.kont_hash0 = private constant [65 x i8] c"fa17afdaed2825aa2fd6698e8c76a9e486fc2595908c9942a58a90fc30b47005\00" @.kont_name0 = private constant [5 x i8] c"find\00" @@ -371,17 +375,12 @@ entry: br i1 %t1, label %b0, label %b1 b0: ; preds = %entry - call void @prism_rc_dec(i64 %a0) ret i64 15 b1: ; preds = %entry - call void @prism_rc_dec(i64 %a0) ret i64 0 } -; Function Attrs: nounwind -declare void @prism_rc_dec(i64) #0 - ; Function Attrs: nounwind define i64 @prismfn_unwrap_or(i64 %a0, i64 %a1) #0 { entry: @@ -394,13 +393,11 @@ entry: ] b2: ; preds = %entry - call void @prism_rc_dec(i64 %a0) ret i64 %a1 b3: ; preds = %entry - call void @prism_rc_inc(i64 %a0) - call void @prism_rc_dec(i64 %a0) call void @prism_rc_dec(i64 %a1) + call void @prism_rc_inc(i64 %a0) ret i64 %a0 b1: ; preds = %entry @@ -411,6 +408,9 @@ b1: ; preds = %entry ; Function Attrs: nounwind declare void @prism_match_error() #0 +; Function Attrs: nounwind +declare void @prism_rc_dec(i64) #0 + ; Function Attrs: nounwind declare void @prism_rc_inc(i64) #0 @@ -419,35 +419,37 @@ define i64 @prismfn_main() #0 { entry: %t1 = call i64 @prismfn_find(i64 3) %t3 = call i64 @prismfn_unwrap_or(i64 %t1, i64 1) - %t5 = call i64 @prismfn_find(i64 1) - %t7 = call i64 @prismfn_unwrap_or(i64 %t5, i64 201) - %t8 = and i64 %t3, %t7 - %t10 = and i64 %t8, 1 - %t11 = icmp eq i64 %t10, 1 - br i1 %t11, label %b0, label %b1 + call void @prism_rc_dec(i64 %t1) + %t6 = call i64 @prismfn_find(i64 1) + %t8 = call i64 @prismfn_unwrap_or(i64 %t6, i64 201) + call void @prism_rc_dec(i64 %t6) + %t10 = and i64 %t3, %t8 + %t12 = and i64 %t10, 1 + %t13 = icmp eq i64 %t12, 1 + br i1 %t13, label %b0, label %b1 b0: ; preds = %entry - %t13 = ashr i64 %t3, 1 - %t15 = ashr i64 %t7, 1 - %t16 = add i64 %t13, %t15 - %t18 = shl i64 %t16, 1 - %t19 = ashr i64 %t18, 1 - %t20 = icmp eq i64 %t19, %t16 - br i1 %t20, label %b3, label %b1 + %t15 = ashr i64 %t3, 1 + %t17 = ashr i64 %t8, 1 + %t18 = add i64 %t15, %t17 + %t20 = shl i64 %t18, 1 + %t21 = ashr i64 %t20, 1 + %t22 = icmp eq i64 %t21, %t18 + br i1 %t22, label %b3, label %b1 b1: ; preds = %b0, %entry - %t22 = call i64 @prism_rt_int_add(i64 %t3, i64 %t7) + %t24 = call i64 @prism_rt_int_add(i64 %t3, i64 %t8) br label %b2 b3: ; preds = %b0 - %t21 = or i64 %t18, 1 + %t23 = or i64 %t20, 1 br label %b2 b2: ; preds = %b1, %b3 - %t23 = phi i64 [ %t21, %b3 ], [ %t22, %b1 ] + %t25 = phi i64 [ %t23, %b3 ], [ %t24, %b1 ] call void @prism_rc_dec(i64 %t3) - call void @prism_rc_dec(i64 %t7) - ret i64 %t23 + call void @prism_rc_dec(i64 %t8) + ret i64 %t25 } ; Function Attrs: nounwind diff --git a/tests/snapshots/snapshots__pipeline@path_lit.pr.snap b/tests/snapshots/snapshots__pipeline@path_lit.pr.snap index 6eab6379..00cfc879 100644 --- a/tests/snapshots/snapshots__pipeline@path_lit.pr.snap +++ b/tests/snapshots/snapshots__pipeline@path_lit.pr.snap @@ -803,10 +803,9 @@ fn read(l, p) = return l to t@23 case t@23 of MkLens(get, _set) => - dup get - drop t@23 return p to t@24 return get to t@25 + dup t@25 (force t@25)(t@24) fn main() = return 3 to t@26 @@ -818,14 +817,18 @@ fn main() = dup p hp_lens() to t@30 return p to t@31 - read(t@30, t@31) to t@32 + read(t@30, t@31) to %rc0 + drop t@30 + return %rc0 to t@32 print t@32 to t@33 drop t@33 print_nl drop _ depth_lens() to t@34 return p to t@35 - read(t@34, t@35) to t@36 + read(t@34, t@35) to %rc1 + drop t@34 + return %rc1 to t@36 print t@36 to t@37 drop t@37 print_nl @@ -834,8 +837,8 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@prism_native_kont_table = constant [1131 x i8] c"scheme prism-core-hash-v2\0Abundle b99846199af3afe31ba07f683ae771743408e1fa9d983dd01b5378fd5ee7cefe\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:b99846199af3afe31ba07f683ae771743408e1fa9d983dd01b5378fd5ee7cefe\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_depth_lens 7a2671d76a235bd1bf0a7e685966775dba88c6249a06fa9c2b738af8b242766c depth_lens\0Afn prismfn_hp_lens 78e9e214ec525f57cab7432fa910c17893189dadd97148908dd1bd36972acb73 hp_lens\0Afn prismfn_lens 704068e56261f9c8691b0fdd406ba5681ff0883527815f6b83673e68b84e9e62 lens\0Afn prismfn_main e163ba746073168d3a7c10193dedf594cbcb37d9beb5ffbf5a2c9026875bb0e1 main\0Afn prismfn_read 91ac6b20fbb7805137083c0063aa8fad71d4f651fe8848003a3086ece8f1226c read\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [1339 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle b99846199af3afe31ba07f683ae771743408e1fa9d983dd01b5378fd5ee7cefe\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:b99846199af3afe31ba07f683ae771743408e1fa9d983dd01b5378fd5ee7cefe\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_depth_lens 7a2671d76a235bd1bf0a7e685966775dba88c6249a06fa9c2b738af8b242766c depth_lens arity 0 slots abi-word[]\0Astate prismfn_hp_lens 78e9e214ec525f57cab7432fa910c17893189dadd97148908dd1bd36972acb73 hp_lens arity 0 slots abi-word[]\0Astate prismfn_lens 704068e56261f9c8691b0fdd406ba5681ff0883527815f6b83673e68b84e9e62 lens arity 2 slots abi-word[arg0=%a0:word,arg1=%a1:word]\0Astate prismfn_main e163ba746073168d3a7c10193dedf594cbcb37d9beb5ffbf5a2c9026875bb0e1 main arity 0 slots abi-word[]\0Astate prismfn_read 91ac6b20fbb7805137083c0063aa8fad71d4f651fe8848003a3086ece8f1226c read arity 2 slots abi-word[arg0=%a0:word,arg1=%a1:word]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [1156 x i8] c"scheme prism-core-hash-v2\0Abundle b99846199af3afe31ba07f683ae771743408e1fa9d983dd01b5378fd5ee7cefe\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:b99846199af3afe31ba07f683ae771743408e1fa9d983dd01b5378fd5ee7cefe\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_depth_lens 7a2671d76a235bd1bf0a7e685966775dba88c6249a06fa9c2b738af8b242766c depth_lens\0Afn prismfn_hp_lens 78e9e214ec525f57cab7432fa910c17893189dadd97148908dd1bd36972acb73 hp_lens\0Afn prismfn_lens 704068e56261f9c8691b0fdd406ba5681ff0883527815f6b83673e68b84e9e62 lens\0Afn prismfn_main e163ba746073168d3a7c10193dedf594cbcb37d9beb5ffbf5a2c9026875bb0e1 main\0Afn prismfn_read 91ac6b20fbb7805137083c0063aa8fad71d4f651fe8848003a3086ece8f1226c read\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [1364 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle b99846199af3afe31ba07f683ae771743408e1fa9d983dd01b5378fd5ee7cefe\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:b99846199af3afe31ba07f683ae771743408e1fa9d983dd01b5378fd5ee7cefe\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_depth_lens 7a2671d76a235bd1bf0a7e685966775dba88c6249a06fa9c2b738af8b242766c depth_lens arity 0 slots abi-word[]\0Astate prismfn_hp_lens 78e9e214ec525f57cab7432fa910c17893189dadd97148908dd1bd36972acb73 hp_lens arity 0 slots abi-word[]\0Astate prismfn_lens 704068e56261f9c8691b0fdd406ba5681ff0883527815f6b83673e68b84e9e62 lens arity 2 slots abi-word[arg0=%a0:word,arg1=%a1:word]\0Astate prismfn_main e163ba746073168d3a7c10193dedf594cbcb37d9beb5ffbf5a2c9026875bb0e1 main arity 0 slots abi-word[]\0Astate prismfn_read 91ac6b20fbb7805137083c0063aa8fad71d4f651fe8848003a3086ece8f1226c read arity 2 slots abi-word[arg0=%a0:word,arg1=%a1:word]\0A\00", section ",.prism_kont", align 1 @.kont_symbol2 = private constant [13 x i8] c"prismfn_lens\00" @.kont_hash2 = private constant [65 x i8] c"704068e56261f9c8691b0fdd406ba5681ff0883527815f6b83673e68b84e9e62\00" @.kont_name2 = private constant [5 x i8] c"lens\00" @@ -882,10 +885,9 @@ b2: ; preds = %entry %t5 = getelementptr inbounds i8, ptr %t0, i64 32 %t6 = load i64, ptr %t5, align 8 call void @prism_rc_inc(i64 %t4) - call void @prism_rc_dec(i64 %a0) - %t9 = call i64 @prismap_1(i64 %t4, i64 %a1) + %t8 = call i64 @prismap_1(i64 %t4, i64 %a1) call void @prism_rc_dec(i64 %t4) - ret i64 %t9 + ret i64 %t8 b1: ; preds = %entry call void @prism_match_error() @@ -898,9 +900,6 @@ declare void @prism_match_error() #0 ; Function Attrs: nounwind declare void @prism_rc_inc(i64) #0 -; Function Attrs: nounwind -declare void @prism_rc_dec(i64) #0 - ; Function Attrs: nounwind define i64 @prismap_1(i64 %_clos, i64 %_a0) #0 { entry: @@ -963,6 +962,9 @@ _merge: ; preds = %_lam161622761563162 ret i64 %_result } +; Function Attrs: nounwind +declare void @prism_rc_dec(i64) #0 + ; Function Attrs: nounwind define i64 @prismfn_main() #0 { entry: @@ -993,23 +995,25 @@ entry: %t27 = ptrtoint ptr %t24 to i64 %t28 = call i64 @prismfn_lens(i64 %t22, i64 %t27) %t29 = call i64 @prismfn_read(i64 %t28, i64 %t16) + call void @prism_rc_dec(i64 %t28) call void @prism_print_int(i64 %t29) call void @prism_rc_dec(i64 %t29) call void @prism_rc_dec(i64 0) call void @prism_print_nl() call void @prism_rc_dec(i64 0) - %t35 = call ptr @prism_alloc(i64 0) - %t36 = getelementptr inbounds i8, ptr %t35, i64 8 - store i64 7170891667029511803, ptr %t36, align 8 - %t38 = ptrtoint ptr %t35 to i64 - %t40 = call ptr @prism_alloc(i64 0) - %t41 = getelementptr inbounds i8, ptr %t40, i64 8 - store i64 7743101094664659736, ptr %t41, align 8 - %t43 = ptrtoint ptr %t40 to i64 - %t44 = call i64 @prismfn_lens(i64 %t38, i64 %t43) - %t45 = call i64 @prismfn_read(i64 %t44, i64 %t16) - call void @prism_print_int(i64 %t45) + %t36 = call ptr @prism_alloc(i64 0) + %t37 = getelementptr inbounds i8, ptr %t36, i64 8 + store i64 7170891667029511803, ptr %t37, align 8 + %t39 = ptrtoint ptr %t36 to i64 + %t41 = call ptr @prism_alloc(i64 0) + %t42 = getelementptr inbounds i8, ptr %t41, i64 8 + store i64 7743101094664659736, ptr %t42, align 8 + %t44 = ptrtoint ptr %t41 to i64 + %t45 = call i64 @prismfn_lens(i64 %t39, i64 %t44) + %t46 = call i64 @prismfn_read(i64 %t45, i64 %t16) call void @prism_rc_dec(i64 %t45) + call void @prism_print_int(i64 %t46) + call void @prism_rc_dec(i64 %t46) call void @prism_rc_dec(i64 0) call void @prism_print_nl() ret i64 0 diff --git a/tests/snapshots/snapshots__pipeline@poly.pr.snap b/tests/snapshots/snapshots__pipeline@poly.pr.snap index 2f149b31..e27e85f2 100644 --- a/tests/snapshots/snapshots__pipeline@poly.pr.snap +++ b/tests/snapshots/snapshots__pipeline@poly.pr.snap @@ -206,8 +206,8 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@prism_native_kont_table = constant [927 x i8] c"scheme prism-core-hash-v2\0Abundle 1389054d4216caa91001c30e0d96d393d55f64922f28c23bf30b992c87c33be3\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:1389054d4216caa91001c30e0d96d393d55f64922f28c23bf30b992c87c33be3\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_inc 36f40b821a0c1580edfd3cc04acb6604c346c442639063ccde1e8f6aa70bcf7a inc\0Afn prismfn_main 42e1e9fb13c9f2e6b469437ce16198a5f7f5ae0609235ba5dcaab193dba4420a main\0Afn prismfn_twice 0fba23e5aaf3c0ba23d96bdd468dd8a2a51bfa67a8dc6fb933f667f223af10dc twice\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [1079 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 1389054d4216caa91001c30e0d96d393d55f64922f28c23bf30b992c87c33be3\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:1389054d4216caa91001c30e0d96d393d55f64922f28c23bf30b992c87c33be3\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_inc 36f40b821a0c1580edfd3cc04acb6604c346c442639063ccde1e8f6aa70bcf7a inc arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_main 42e1e9fb13c9f2e6b469437ce16198a5f7f5ae0609235ba5dcaab193dba4420a main arity 0 slots abi-word[]\0Astate prismfn_twice 0fba23e5aaf3c0ba23d96bdd468dd8a2a51bfa67a8dc6fb933f667f223af10dc twice arity 2 slots abi-word[arg0=%a0:word,arg1=%a1:word]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [952 x i8] c"scheme prism-core-hash-v2\0Abundle 1389054d4216caa91001c30e0d96d393d55f64922f28c23bf30b992c87c33be3\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:1389054d4216caa91001c30e0d96d393d55f64922f28c23bf30b992c87c33be3\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_inc 36f40b821a0c1580edfd3cc04acb6604c346c442639063ccde1e8f6aa70bcf7a inc\0Afn prismfn_main 42e1e9fb13c9f2e6b469437ce16198a5f7f5ae0609235ba5dcaab193dba4420a main\0Afn prismfn_twice 0fba23e5aaf3c0ba23d96bdd468dd8a2a51bfa67a8dc6fb933f667f223af10dc twice\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [1104 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 1389054d4216caa91001c30e0d96d393d55f64922f28c23bf30b992c87c33be3\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:1389054d4216caa91001c30e0d96d393d55f64922f28c23bf30b992c87c33be3\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_inc 36f40b821a0c1580edfd3cc04acb6604c346c442639063ccde1e8f6aa70bcf7a inc arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_main 42e1e9fb13c9f2e6b469437ce16198a5f7f5ae0609235ba5dcaab193dba4420a main arity 0 slots abi-word[]\0Astate prismfn_twice 0fba23e5aaf3c0ba23d96bdd468dd8a2a51bfa67a8dc6fb933f667f223af10dc twice arity 2 slots abi-word[arg0=%a0:word,arg1=%a1:word]\0A\00", section ",.prism_kont", align 1 @.kont_symbol1 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash1 = private constant [65 x i8] c"42e1e9fb13c9f2e6b469437ce16198a5f7f5ae0609235ba5dcaab193dba4420a\00" @.kont_name1 = private constant [5 x i8] c"main\00" diff --git a/tests/snapshots/snapshots__pipeline@probe.pr.snap b/tests/snapshots/snapshots__pipeline@probe.pr.snap index 7c152293..1b473dc5 100644 --- a/tests/snapshots/snapshots__pipeline@probe.pr.snap +++ b/tests/snapshots/snapshots__pipeline@probe.pr.snap @@ -89,9 +89,9 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@.str0 = private constant [6 x i8] c"trace\00" -@prism_native_kont_table = constant [741 x i8] c"scheme prism-core-hash-v2\0Abundle fda90147ead1b49843c415bf6190b7d71cadce1feb867970fa3f95c3facd56f6\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:fda90147ead1b49843c415bf6190b7d71cadce1feb867970fa3f95c3facd56f6\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_main 32263e0010182d612411f5e5dfbe9c7251570e2a61e3d59f0e617161bddee416 main\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [811 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle fda90147ead1b49843c415bf6190b7d71cadce1feb867970fa3f95c3facd56f6\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:fda90147ead1b49843c415bf6190b7d71cadce1feb867970fa3f95c3facd56f6\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main 32263e0010182d612411f5e5dfbe9c7251570e2a61e3d59f0e617161bddee416 main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 +@.str0 = private constant { i64, i64, i64, [6 x i8] } { i64 1152921504606846976, i64 1398034944, i64 5, [6 x i8] c"trace\00" }, align 8 +@prism_native_kont_table = constant [766 x i8] c"scheme prism-core-hash-v2\0Abundle fda90147ead1b49843c415bf6190b7d71cadce1feb867970fa3f95c3facd56f6\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:fda90147ead1b49843c415bf6190b7d71cadce1feb867970fa3f95c3facd56f6\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_main 32263e0010182d612411f5e5dfbe9c7251570e2a61e3d59f0e617161bddee416 main\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [836 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle fda90147ead1b49843c415bf6190b7d71cadce1feb867970fa3f95c3facd56f6\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:fda90147ead1b49843c415bf6190b7d71cadce1feb867970fa3f95c3facd56f6\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main 32263e0010182d612411f5e5dfbe9c7251570e2a61e3d59f0e617161bddee416 main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 @.kont_symbol0 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash0 = private constant [65 x i8] c"32263e0010182d612411f5e5dfbe9c7251570e2a61e3d59f0e617161bddee416\00" @.kont_name0 = private constant [5 x i8] c"main\00" @@ -102,11 +102,10 @@ source_filename = "prism" ; Function Attrs: nounwind define i64 @prismfn_main() #0 { entry: - %t0 = call i64 @prism_str_lit(ptr @.str0, i64 5) - %t1 = call i64 @prism_probe_enabled(i64 %t0) + %t1 = call i64 @prism_probe_enabled(i64 ptrtoint (ptr @.str0 to i64)) %t3 = shl i64 %t1, 1 %t4 = or i64 %t3, 1 - call void @prism_rc_dec(i64 %t0) + call void @prism_rc_dec(i64 ptrtoint (ptr @.str0 to i64)) %t6 = icmp ne i64 %t4, 1 br i1 %t6, label %b0, label %b1 @@ -123,9 +122,6 @@ b1: ; preds = %entry ret i64 0 } -; Function Attrs: nounwind -declare i64 @prism_str_lit(ptr, i64) #0 - ; Function Attrs: nounwind declare i64 @prism_probe_enabled(i64) #0 diff --git a/tests/snapshots/snapshots__pipeline@rank2.pr.snap b/tests/snapshots/snapshots__pipeline@rank2.pr.snap index 5bdb94e7..1b67712d 100644 --- a/tests/snapshots/snapshots__pipeline@rank2.pr.snap +++ b/tests/snapshots/snapshots__pipeline@rank2.pr.snap @@ -225,8 +225,8 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@prism_native_kont_table = constant [834 x i8] c"scheme prism-core-hash-v2\0Abundle 480ccc59a99611e643f7e49b8ef046c36637b34f9f028c9419ee265a3a87aa20\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:480ccc59a99611e643f7e49b8ef046c36637b34f9f028c9419ee265a3a87aa20\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_main 13aa7ea55c1d4c1ab9dc1aaeb0bc4f2a7bfde2859b43ed031c64b31ddd19aa9a main\0Afn prismfn_pick fbb7da799ec12b795ed34fa492bc26c8e63e7a66689a5a0da67dfd5acee76887 pick\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [938 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 480ccc59a99611e643f7e49b8ef046c36637b34f9f028c9419ee265a3a87aa20\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:480ccc59a99611e643f7e49b8ef046c36637b34f9f028c9419ee265a3a87aa20\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main 13aa7ea55c1d4c1ab9dc1aaeb0bc4f2a7bfde2859b43ed031c64b31ddd19aa9a main arity 0 slots abi-word[]\0Astate prismfn_pick fbb7da799ec12b795ed34fa492bc26c8e63e7a66689a5a0da67dfd5acee76887 pick arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [859 x i8] c"scheme prism-core-hash-v2\0Abundle 480ccc59a99611e643f7e49b8ef046c36637b34f9f028c9419ee265a3a87aa20\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:480ccc59a99611e643f7e49b8ef046c36637b34f9f028c9419ee265a3a87aa20\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_main 13aa7ea55c1d4c1ab9dc1aaeb0bc4f2a7bfde2859b43ed031c64b31ddd19aa9a main\0Afn prismfn_pick fbb7da799ec12b795ed34fa492bc26c8e63e7a66689a5a0da67dfd5acee76887 pick\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [963 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 480ccc59a99611e643f7e49b8ef046c36637b34f9f028c9419ee265a3a87aa20\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:480ccc59a99611e643f7e49b8ef046c36637b34f9f028c9419ee265a3a87aa20\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main 13aa7ea55c1d4c1ab9dc1aaeb0bc4f2a7bfde2859b43ed031c64b31ddd19aa9a main arity 0 slots abi-word[]\0Astate prismfn_pick fbb7da799ec12b795ed34fa492bc26c8e63e7a66689a5a0da67dfd5acee76887 pick arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 @.kont_symbol0 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash0 = private constant [65 x i8] c"13aa7ea55c1d4c1ab9dc1aaeb0bc4f2a7bfde2859b43ed031c64b31ddd19aa9a\00" @.kont_name0 = private constant [5 x i8] c"main\00" diff --git a/tests/snapshots/snapshots__pipeline@rankn_church.pr.snap b/tests/snapshots/snapshots__pipeline@rankn_church.pr.snap index de1c98be..2536241d 100644 --- a/tests/snapshots/snapshots__pipeline@rankn_church.pr.snap +++ b/tests/snapshots/snapshots__pipeline@rankn_church.pr.snap @@ -494,8 +494,8 @@ fn csucc(n) = dup f return f to t@2 return x to t@3 - dup g return g to t@4 + dup t@4 (force t@4)(t@2, t@3) to t@5 return f to t@6 (force t@6)(t@5) @@ -505,8 +505,6 @@ fn toInt(n) = return n to t@8 case t@8 of CNat(g) => - dup g - drop t@8 thunk { \k. return k to t@9 return 1 to t@10 @@ -514,13 +512,16 @@ fn toInt(n) = } to t@11 return 0 to t@12 return g to t@13 + dup t@13 (force t@13)(t@11, t@12) fn main() = czero() to t@14 csucc(t@14) to t@15 csucc(t@15) to t@16 csucc(t@16) to t@17 - toInt(t@17) to t@18 + toInt(t@17) to %rc0 + drop t@17 + return %rc0 to t@18 print t@18 to t@19 drop t@19 print_nl @@ -529,8 +530,8 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@prism_native_kont_table = constant [1026 x i8] c"scheme prism-core-hash-v2\0Abundle 39bb3410e5768d73f63211383a3ccb5193dcd76a7f958b323b8615ae108c55a7\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:39bb3410e5768d73f63211383a3ccb5193dcd76a7f958b323b8615ae108c55a7\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_csucc ad2a6f36b80cbe8d6f8fbde36f915427f466ead6d5507eb26b20bfd60226c007 csucc\0Afn prismfn_czero 1b1948cae070fa34117083f2e096dce894a90fa2caa71c560ec33bd2d13dff38 czero\0Afn prismfn_main d56bc8b7aaf499774aa64c642abdf75da08bbc58d11e71fe96729689c2671aeb main\0Afn prismfn_toInt 8f71a583135ce045b9b7cbed5d1507c895cbcee3aeb4a43d10932bd23501ef8b toInt\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [1185 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 39bb3410e5768d73f63211383a3ccb5193dcd76a7f958b323b8615ae108c55a7\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:39bb3410e5768d73f63211383a3ccb5193dcd76a7f958b323b8615ae108c55a7\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_csucc ad2a6f36b80cbe8d6f8fbde36f915427f466ead6d5507eb26b20bfd60226c007 csucc arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_czero 1b1948cae070fa34117083f2e096dce894a90fa2caa71c560ec33bd2d13dff38 czero arity 0 slots abi-word[]\0Astate prismfn_main d56bc8b7aaf499774aa64c642abdf75da08bbc58d11e71fe96729689c2671aeb main arity 0 slots abi-word[]\0Astate prismfn_toInt 8f71a583135ce045b9b7cbed5d1507c895cbcee3aeb4a43d10932bd23501ef8b toInt arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [1051 x i8] c"scheme prism-core-hash-v2\0Abundle 39bb3410e5768d73f63211383a3ccb5193dcd76a7f958b323b8615ae108c55a7\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:39bb3410e5768d73f63211383a3ccb5193dcd76a7f958b323b8615ae108c55a7\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_csucc ad2a6f36b80cbe8d6f8fbde36f915427f466ead6d5507eb26b20bfd60226c007 csucc\0Afn prismfn_czero 1b1948cae070fa34117083f2e096dce894a90fa2caa71c560ec33bd2d13dff38 czero\0Afn prismfn_main d56bc8b7aaf499774aa64c642abdf75da08bbc58d11e71fe96729689c2671aeb main\0Afn prismfn_toInt 8f71a583135ce045b9b7cbed5d1507c895cbcee3aeb4a43d10932bd23501ef8b toInt\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [1210 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 39bb3410e5768d73f63211383a3ccb5193dcd76a7f958b323b8615ae108c55a7\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:39bb3410e5768d73f63211383a3ccb5193dcd76a7f958b323b8615ae108c55a7\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_csucc ad2a6f36b80cbe8d6f8fbde36f915427f466ead6d5507eb26b20bfd60226c007 csucc arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_czero 1b1948cae070fa34117083f2e096dce894a90fa2caa71c560ec33bd2d13dff38 czero arity 0 slots abi-word[]\0Astate prismfn_main d56bc8b7aaf499774aa64c642abdf75da08bbc58d11e71fe96729689c2671aeb main arity 0 slots abi-word[]\0Astate prismfn_toInt 8f71a583135ce045b9b7cbed5d1507c895cbcee3aeb4a43d10932bd23501ef8b toInt arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 @.kont_symbol0 = private constant [14 x i8] c"prismfn_csucc\00" @.kont_hash0 = private constant [65 x i8] c"ad2a6f36b80cbe8d6f8fbde36f915427f466ead6d5507eb26b20bfd60226c007\00" @.kont_name0 = private constant [6 x i8] c"csucc\00" diff --git a/tests/snapshots/snapshots__pipeline@rankn_data_field.pr.snap b/tests/snapshots/snapshots__pipeline@rankn_data_field.pr.snap index c452ec60..ead983c7 100644 --- a/tests/snapshots/snapshots__pipeline@rankn_data_field.pr.snap +++ b/tests/snapshots/snapshots__pipeline@rankn_data_field.pr.snap @@ -216,15 +216,16 @@ fn useit(w) = return w to t@0 case t@0 of Wrap(f) => - dup f - drop t@0 return 42 to t@1 return f to t@2 + dup t@2 (force t@2)(t@1) fn main() = return thunk { \x. return x } to t@3 return Wrap(t@3) to t@4 - useit(t@4) to t@5 + useit(t@4) to %rc0 + drop t@4 + return %rc0 to t@5 print t@5 to t@6 drop t@6 print_nl @@ -233,8 +234,8 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@prism_native_kont_table = constant [836 x i8] c"scheme prism-core-hash-v2\0Abundle 6d017fe7175631ddafc2f44c654d4834568067f2b91ba857669bddfb275da0bd\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:6d017fe7175631ddafc2f44c654d4834568067f2b91ba857669bddfb275da0bd\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_main 08bbf0bc80f5ce0e6c7c0c4c613d80c08a3c69fdfddeef217b522c40ddcae7dd main\0Afn prismfn_useit 504269893e7af592b0c1b36d8897d58d699301f45c59571b1f525dc3a53af793 useit\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [940 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 6d017fe7175631ddafc2f44c654d4834568067f2b91ba857669bddfb275da0bd\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:6d017fe7175631ddafc2f44c654d4834568067f2b91ba857669bddfb275da0bd\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main 08bbf0bc80f5ce0e6c7c0c4c613d80c08a3c69fdfddeef217b522c40ddcae7dd main arity 0 slots abi-word[]\0Astate prismfn_useit 504269893e7af592b0c1b36d8897d58d699301f45c59571b1f525dc3a53af793 useit arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [861 x i8] c"scheme prism-core-hash-v2\0Abundle 6d017fe7175631ddafc2f44c654d4834568067f2b91ba857669bddfb275da0bd\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:6d017fe7175631ddafc2f44c654d4834568067f2b91ba857669bddfb275da0bd\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_main 08bbf0bc80f5ce0e6c7c0c4c613d80c08a3c69fdfddeef217b522c40ddcae7dd main\0Afn prismfn_useit 504269893e7af592b0c1b36d8897d58d699301f45c59571b1f525dc3a53af793 useit\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [965 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 6d017fe7175631ddafc2f44c654d4834568067f2b91ba857669bddfb275da0bd\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:6d017fe7175631ddafc2f44c654d4834568067f2b91ba857669bddfb275da0bd\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main 08bbf0bc80f5ce0e6c7c0c4c613d80c08a3c69fdfddeef217b522c40ddcae7dd main arity 0 slots abi-word[]\0Astate prismfn_useit 504269893e7af592b0c1b36d8897d58d699301f45c59571b1f525dc3a53af793 useit arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 @.kont_symbol0 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash0 = private constant [65 x i8] c"08bbf0bc80f5ce0e6c7c0c4c613d80c08a3c69fdfddeef217b522c40ddcae7dd\00" @.kont_name0 = private constant [5 x i8] c"main\00" diff --git a/tests/snapshots/snapshots__pipeline@rankn_effect_poly.pr.snap b/tests/snapshots/snapshots__pipeline@rankn_effect_poly.pr.snap index 07efe281..4ca91c74 100644 --- a/tests/snapshots/snapshots__pipeline@rankn_effect_poly.pr.snap +++ b/tests/snapshots/snapshots__pipeline@rankn_effect_poly.pr.snap @@ -258,8 +258,8 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@prism_native_kont_table = constant [939 x i8] c"scheme prism-core-hash-v2\0Abundle 753a1f9c73ae4b711764b2f2d7ef1001c08a1030e9c77c65d038df10780357c5\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:753a1f9c73ae4b711764b2f2d7ef1001c08a1030e9c77c65d038df10780357c5\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_apply_io 583cce27692a6243a6e93c99e4c31206ec5b5961058d94a9204c1f402795ead0 apply_io\0Afn prismfn_main c83cde4a76c3e307774a1fbc3cc61c9b1739cfe834fcd8cf3e7c5ad8ab206985 main\0Afn prismfn_runEff 4bf009054a50cffa1dfef8c85fbc02c6f63bc723b24a387c951f32d4faa4cd66 runEff\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [1077 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 753a1f9c73ae4b711764b2f2d7ef1001c08a1030e9c77c65d038df10780357c5\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:753a1f9c73ae4b711764b2f2d7ef1001c08a1030e9c77c65d038df10780357c5\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_apply_io 583cce27692a6243a6e93c99e4c31206ec5b5961058d94a9204c1f402795ead0 apply_io arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_main c83cde4a76c3e307774a1fbc3cc61c9b1739cfe834fcd8cf3e7c5ad8ab206985 main arity 0 slots abi-word[]\0Astate prismfn_runEff 4bf009054a50cffa1dfef8c85fbc02c6f63bc723b24a387c951f32d4faa4cd66 runEff arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [964 x i8] c"scheme prism-core-hash-v2\0Abundle 753a1f9c73ae4b711764b2f2d7ef1001c08a1030e9c77c65d038df10780357c5\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:753a1f9c73ae4b711764b2f2d7ef1001c08a1030e9c77c65d038df10780357c5\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_apply_io 583cce27692a6243a6e93c99e4c31206ec5b5961058d94a9204c1f402795ead0 apply_io\0Afn prismfn_main c83cde4a76c3e307774a1fbc3cc61c9b1739cfe834fcd8cf3e7c5ad8ab206985 main\0Afn prismfn_runEff 4bf009054a50cffa1dfef8c85fbc02c6f63bc723b24a387c951f32d4faa4cd66 runEff\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [1102 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 753a1f9c73ae4b711764b2f2d7ef1001c08a1030e9c77c65d038df10780357c5\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:753a1f9c73ae4b711764b2f2d7ef1001c08a1030e9c77c65d038df10780357c5\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_apply_io 583cce27692a6243a6e93c99e4c31206ec5b5961058d94a9204c1f402795ead0 apply_io arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_main c83cde4a76c3e307774a1fbc3cc61c9b1739cfe834fcd8cf3e7c5ad8ab206985 main arity 0 slots abi-word[]\0Astate prismfn_runEff 4bf009054a50cffa1dfef8c85fbc02c6f63bc723b24a387c951f32d4faa4cd66 runEff arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 @.kont_symbol1 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash1 = private constant [65 x i8] c"c83cde4a76c3e307774a1fbc3cc61c9b1739cfe834fcd8cf3e7c5ad8ab206985\00" @.kont_name1 = private constant [5 x i8] c"main\00" diff --git a/tests/snapshots/snapshots__pipeline@rankn_rank3.pr.snap b/tests/snapshots/snapshots__pipeline@rankn_rank3.pr.snap index 95b0f1dc..7e5e2e85 100644 --- a/tests/snapshots/snapshots__pipeline@rankn_rank3.pr.snap +++ b/tests/snapshots/snapshots__pipeline@rankn_rank3.pr.snap @@ -224,8 +224,8 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@prism_native_kont_table = constant [836 x i8] c"scheme prism-core-hash-v2\0Abundle 5d7b54f14b510b4574ed0e295d89760c032849b828bdc6a1225184ae4fa8a716\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:5d7b54f14b510b4574ed0e295d89760c032849b828bdc6a1225184ae4fa8a716\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_main ee774b1a0c10ad7a485f2846e81c31411f94f287b0a12a0641741ae285193b61 main\0Afn prismfn_rank3 29ef00b61d7e02e2b0de87cb061c0876a619bff105bb27dcad7ea83b279e28a0 rank3\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [940 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 5d7b54f14b510b4574ed0e295d89760c032849b828bdc6a1225184ae4fa8a716\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:5d7b54f14b510b4574ed0e295d89760c032849b828bdc6a1225184ae4fa8a716\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main ee774b1a0c10ad7a485f2846e81c31411f94f287b0a12a0641741ae285193b61 main arity 0 slots abi-word[]\0Astate prismfn_rank3 29ef00b61d7e02e2b0de87cb061c0876a619bff105bb27dcad7ea83b279e28a0 rank3 arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [861 x i8] c"scheme prism-core-hash-v2\0Abundle 5d7b54f14b510b4574ed0e295d89760c032849b828bdc6a1225184ae4fa8a716\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:5d7b54f14b510b4574ed0e295d89760c032849b828bdc6a1225184ae4fa8a716\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_main ee774b1a0c10ad7a485f2846e81c31411f94f287b0a12a0641741ae285193b61 main\0Afn prismfn_rank3 29ef00b61d7e02e2b0de87cb061c0876a619bff105bb27dcad7ea83b279e28a0 rank3\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [965 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 5d7b54f14b510b4574ed0e295d89760c032849b828bdc6a1225184ae4fa8a716\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:5d7b54f14b510b4574ed0e295d89760c032849b828bdc6a1225184ae4fa8a716\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main ee774b1a0c10ad7a485f2846e81c31411f94f287b0a12a0641741ae285193b61 main arity 0 slots abi-word[]\0Astate prismfn_rank3 29ef00b61d7e02e2b0de87cb061c0876a619bff105bb27dcad7ea83b279e28a0 rank3 arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 @.kont_symbol0 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash0 = private constant [65 x i8] c"ee774b1a0c10ad7a485f2846e81c31411f94f287b0a12a0641741ae285193b61\00" @.kont_name0 = private constant [5 x i8] c"main\00" diff --git a/tests/snapshots/snapshots__pipeline@rankn_relay.pr.snap b/tests/snapshots/snapshots__pipeline@rankn_relay.pr.snap index 663ed586..b1481b6f 100644 --- a/tests/snapshots/snapshots__pipeline@rankn_relay.pr.snap +++ b/tests/snapshots/snapshots__pipeline@rankn_relay.pr.snap @@ -286,8 +286,8 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@prism_native_kont_table = constant [929 x i8] c"scheme prism-core-hash-v2\0Abundle 78c900b0d896c2528f1515904e350c0889120817f023f0ef42418ed6184fcdbd\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:78c900b0d896c2528f1515904e350c0889120817f023f0ef42418ed6184fcdbd\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_main 2eda6e4a7d662346c36b6c4262ba5ce21b7c47f1b0b02a577b10861907cc0b48 main\0Afn prismfn_pick fbb7da799ec12b795ed34fa492bc26c8e63e7a66689a5a0da67dfd5acee76887 pick\0Afn prismfn_relay 3626d8f2d03294538282f24a96e5809d7a46bb5a5fdf39983a83305fa6d855ce relay\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [1067 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 78c900b0d896c2528f1515904e350c0889120817f023f0ef42418ed6184fcdbd\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:78c900b0d896c2528f1515904e350c0889120817f023f0ef42418ed6184fcdbd\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main 2eda6e4a7d662346c36b6c4262ba5ce21b7c47f1b0b02a577b10861907cc0b48 main arity 0 slots abi-word[]\0Astate prismfn_pick fbb7da799ec12b795ed34fa492bc26c8e63e7a66689a5a0da67dfd5acee76887 pick arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_relay 3626d8f2d03294538282f24a96e5809d7a46bb5a5fdf39983a83305fa6d855ce relay arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [954 x i8] c"scheme prism-core-hash-v2\0Abundle 78c900b0d896c2528f1515904e350c0889120817f023f0ef42418ed6184fcdbd\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:78c900b0d896c2528f1515904e350c0889120817f023f0ef42418ed6184fcdbd\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_main 2eda6e4a7d662346c36b6c4262ba5ce21b7c47f1b0b02a577b10861907cc0b48 main\0Afn prismfn_pick fbb7da799ec12b795ed34fa492bc26c8e63e7a66689a5a0da67dfd5acee76887 pick\0Afn prismfn_relay 3626d8f2d03294538282f24a96e5809d7a46bb5a5fdf39983a83305fa6d855ce relay\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [1092 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 78c900b0d896c2528f1515904e350c0889120817f023f0ef42418ed6184fcdbd\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:78c900b0d896c2528f1515904e350c0889120817f023f0ef42418ed6184fcdbd\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main 2eda6e4a7d662346c36b6c4262ba5ce21b7c47f1b0b02a577b10861907cc0b48 main arity 0 slots abi-word[]\0Astate prismfn_pick fbb7da799ec12b795ed34fa492bc26c8e63e7a66689a5a0da67dfd5acee76887 pick arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_relay 3626d8f2d03294538282f24a96e5809d7a46bb5a5fdf39983a83305fa6d855ce relay arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 @.kont_symbol0 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash0 = private constant [65 x i8] c"2eda6e4a7d662346c36b6c4262ba5ce21b7c47f1b0b02a577b10861907cc0b48\00" @.kont_name0 = private constant [5 x i8] c"main\00" diff --git a/tests/snapshots/snapshots__pipeline@rankn_return.pr.snap b/tests/snapshots/snapshots__pipeline@rankn_return.pr.snap index 622b6104..4701d0c3 100644 --- a/tests/snapshots/snapshots__pipeline@rankn_return.pr.snap +++ b/tests/snapshots/snapshots__pipeline@rankn_return.pr.snap @@ -142,8 +142,8 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@prism_native_kont_table = constant [834 x i8] c"scheme prism-core-hash-v2\0Abundle 626043baf4d21a232352ee2abf57272eceaab82828522b8e9815fd50714701e2\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:626043baf4d21a232352ee2abf57272eceaab82828522b8e9815fd50714701e2\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_main 5196c23b946d285aa40486e45e9e958a84649a89ab8dbd89a25e2b7bd010ce1a main\0Afn prismfn_mkid d29ad19445151b8a301daee379d752aafc269d00f978249cec7b04de1ca48f2b mkid\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [925 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 626043baf4d21a232352ee2abf57272eceaab82828522b8e9815fd50714701e2\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:626043baf4d21a232352ee2abf57272eceaab82828522b8e9815fd50714701e2\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main 5196c23b946d285aa40486e45e9e958a84649a89ab8dbd89a25e2b7bd010ce1a main arity 0 slots abi-word[]\0Astate prismfn_mkid d29ad19445151b8a301daee379d752aafc269d00f978249cec7b04de1ca48f2b mkid arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [859 x i8] c"scheme prism-core-hash-v2\0Abundle 626043baf4d21a232352ee2abf57272eceaab82828522b8e9815fd50714701e2\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:626043baf4d21a232352ee2abf57272eceaab82828522b8e9815fd50714701e2\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_main 5196c23b946d285aa40486e45e9e958a84649a89ab8dbd89a25e2b7bd010ce1a main\0Afn prismfn_mkid d29ad19445151b8a301daee379d752aafc269d00f978249cec7b04de1ca48f2b mkid\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [950 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 626043baf4d21a232352ee2abf57272eceaab82828522b8e9815fd50714701e2\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:626043baf4d21a232352ee2abf57272eceaab82828522b8e9815fd50714701e2\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main 5196c23b946d285aa40486e45e9e958a84649a89ab8dbd89a25e2b7bd010ce1a main arity 0 slots abi-word[]\0Astate prismfn_mkid d29ad19445151b8a301daee379d752aafc269d00f978249cec7b04de1ca48f2b mkid arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 @.kont_symbol0 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash0 = private constant [65 x i8] c"5196c23b946d285aa40486e45e9e958a84649a89ab8dbd89a25e2b7bd010ce1a\00" @.kont_name0 = private constant [5 x i8] c"main\00" diff --git a/tests/snapshots/snapshots__pipeline@record_spread_sum.pr.snap b/tests/snapshots/snapshots__pipeline@record_spread_sum.pr.snap new file mode 100644 index 00000000..43594bd9 --- /dev/null +++ b/tests/snapshots/snapshots__pipeline@record_spread_sum.pr.snap @@ -0,0 +1,150 @@ +--- +source: tests/snapshots.rs +expression: "normalize_pipeline_report(&prism::report(&src))" +input_file: tests/cases/record_spread_sum.pr +--- +== tokens == +VOpen Type UIdent("Shape") Eq UIdent("Circle") LBrace Ident("radius") Colon KwInt RBrace Bar UIdent("Square") LBrace Ident("side") Colon KwInt RBrace VSemi Fn Ident("resize") LParen Ident("shape") Colon UIdent("Shape") RParen Colon UIdent("Shape") Eq UIdent("Circle") LBrace DotDot Ident("shape") Comma Ident("radius") Eq Int(IntLit { value: 2, suffix: None }) RBrace VSemi Fn Ident("main") LParen RParen Eq Ident("println") LParen Int(IntLit { value: 0, suffix: None }) RParen VClose + +== ast == +Program { + types: [ + DataDecl { + name: "Shape", + params: [], + param_kinds: [], + ctors: [ + Ctor { + name: "Circle", + args: [ + Int, + ], + fields: Some( + [ + ( + "radius", + Int, + ), + ], + ), + }, + Ctor { + name: "Square", + args: [ + Int, + ], + fields: Some( + [ + ( + "side", + Int, + ), + ], + ), + }, + ], + deriving: [], + newtype: false, + span: 159..217, + }, + ], + effects: [], + errors: [], + aliases: [], + classes: [], + instances: [], + fns: [ + Decl { + name: "resize", + params: [ + Param { + name: "shape", + ty: Some( + Con( + "Shape", + [], + ), + ), + borrow: false, + default: None, + }, + ], + ret: Some( + Con( + "Shape", + [], + ), + ), + eff: None, + constraints: [], + body: Spanned { + node: RecordUpdate( + Spanned { + node: Var( + "shape", + ), + span: 265..270, + }, + "Circle", + [ + ( + "radius", + Spanned { + node: Int( + IntLit { + value: 2, + suffix: None, + }, + ), + span: 281..282, + }, + ), + ], + ), + span: 254..284, + }, + wheres: [], + span: 219..284, + }, + Decl { + name: "main", + params: [], + ret: None, + eff: None, + constraints: [], + body: Spanned { + node: Call( + Spanned { + node: Var( + "println", + ), + span: 298..305, + }, + [ + Spanned { + node: Int( + IntLit { + value: 0, + suffix: None, + }, + ), + span: 306..307, + }, + ], + ), + span: 298..308, + }, + wheres: [], + span: 286..308, + }, + ], +} + +== types == +[E1024] Type Error: in `resize`: cannot spread an unrefined sum type `Shape` into record constructor `Circle`; the type has 2 constructors + ╭─[ :5:36 ] + │ + 5 │ fn resize(shape : Shape) : Shape = Circle { ..shape, radius = 2 } + │ ───────────────┬────────────── + │ ╰──────────────── in `resize`: cannot spread an unrefined sum type `Shape` into record constructor `Circle`; the type has 2 constructors +───╯ diff --git a/tests/snapshots/snapshots__pipeline@records.pr.snap b/tests/snapshots/snapshots__pipeline@records.pr.snap index 8cfaea9b..1df52882 100644 --- a/tests/snapshots/snapshots__pipeline@records.pr.snap +++ b/tests/snapshots/snapshots__pipeline@records.pr.snap @@ -450,26 +450,22 @@ fn sum_coords(p) = return p to t@2 case t@2 of Point(px, py) => - dup px - dup py - drop t@2 return px to t@3 return py to t@4 + dup t@4 + dup t@3 t@3 + t@4 fn shift_x(p, dx) = return p to t@5 - dup t@5 case t@5 of Point(t@6, _) => dup t@6 - drop t@5 return t@6 to t@6 drop t@6 case t@5 of Point(_, t@7) => dup t@7 - drop t@5 return t@7 to t@7 return dx to t@8 @@ -480,12 +476,18 @@ fn main() = make(t@9, t@10) to p dup p return p to t@11 - sum_coords(t@11) to s + sum_coords(t@11) to %rc0 + drop t@11 + return %rc0 to s return p to t@12 return 10 to t@13 - shift_x(t@12, t@13) to q + shift_x(t@12, t@13) to %rc1 + drop t@12 + return %rc1 to q return q to t@14 - sum_coords(t@14) to t + sum_coords(t@14) to %rc2 + drop t@14 + return %rc2 to t drop t return s to t@15 print t@15 to t@16 @@ -496,8 +498,8 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@prism_native_kont_table = constant [1038 x i8] c"scheme prism-core-hash-v2\0Abundle 9de09009151b8a1b419083cdc221a166812c8c5369d176d04d1081cea93569af\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:9de09009151b8a1b419083cdc221a166812c8c5369d176d04d1081cea93569af\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_main 715ad1b442631bd80ae526d3d10708dd52a5541735be49ae5e0be6da2b23f787 main\0Afn prismfn_make c8b2e4f91b9684b77f1b981de7356f40eb33aa4b9b1017edda8c1904ac670858 make\0Afn prismfn_shift_x cc11659f387f55b7038f5f8bd258f11600e745126a042a68207430c84c9b09bb shift_x\0Afn prismfn_sum_coords 997472c88fa38a6aef0b226fcbac4fbef834145a253e2c2eace273ee66d5f452 sum_coords\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [1238 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 9de09009151b8a1b419083cdc221a166812c8c5369d176d04d1081cea93569af\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:9de09009151b8a1b419083cdc221a166812c8c5369d176d04d1081cea93569af\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main 715ad1b442631bd80ae526d3d10708dd52a5541735be49ae5e0be6da2b23f787 main arity 0 slots abi-word[]\0Astate prismfn_make c8b2e4f91b9684b77f1b981de7356f40eb33aa4b9b1017edda8c1904ac670858 make arity 2 slots abi-word[arg0=%a0:word,arg1=%a1:word]\0Astate prismfn_shift_x cc11659f387f55b7038f5f8bd258f11600e745126a042a68207430c84c9b09bb shift_x arity 2 slots abi-word[arg0=%a0:word,arg1=%a1:word]\0Astate prismfn_sum_coords 997472c88fa38a6aef0b226fcbac4fbef834145a253e2c2eace273ee66d5f452 sum_coords arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [1063 x i8] c"scheme prism-core-hash-v2\0Abundle 9de09009151b8a1b419083cdc221a166812c8c5369d176d04d1081cea93569af\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:9de09009151b8a1b419083cdc221a166812c8c5369d176d04d1081cea93569af\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_main 715ad1b442631bd80ae526d3d10708dd52a5541735be49ae5e0be6da2b23f787 main\0Afn prismfn_make c8b2e4f91b9684b77f1b981de7356f40eb33aa4b9b1017edda8c1904ac670858 make\0Afn prismfn_shift_x cc11659f387f55b7038f5f8bd258f11600e745126a042a68207430c84c9b09bb shift_x\0Afn prismfn_sum_coords 997472c88fa38a6aef0b226fcbac4fbef834145a253e2c2eace273ee66d5f452 sum_coords\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [1263 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 9de09009151b8a1b419083cdc221a166812c8c5369d176d04d1081cea93569af\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:9de09009151b8a1b419083cdc221a166812c8c5369d176d04d1081cea93569af\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main 715ad1b442631bd80ae526d3d10708dd52a5541735be49ae5e0be6da2b23f787 main arity 0 slots abi-word[]\0Astate prismfn_make c8b2e4f91b9684b77f1b981de7356f40eb33aa4b9b1017edda8c1904ac670858 make arity 2 slots abi-word[arg0=%a0:word,arg1=%a1:word]\0Astate prismfn_shift_x cc11659f387f55b7038f5f8bd258f11600e745126a042a68207430c84c9b09bb shift_x arity 2 slots abi-word[arg0=%a0:word,arg1=%a1:word]\0Astate prismfn_sum_coords 997472c88fa38a6aef0b226fcbac4fbef834145a253e2c2eace273ee66d5f452 sum_coords arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 @.kont_symbol0 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash0 = private constant [65 x i8] c"715ad1b442631bd80ae526d3d10708dd52a5541735be49ae5e0be6da2b23f787\00" @.kont_name0 = private constant [5 x i8] c"main\00" @@ -523,40 +525,39 @@ b2: ; preds = %entry %t4 = load i64, ptr %t3, align 8 %t5 = getelementptr inbounds i8, ptr %t0, i64 32 %t6 = load i64, ptr %t5, align 8 - call void @prism_rc_inc(i64 %t4) call void @prism_rc_inc(i64 %t6) - call void @prism_rc_dec(i64 %a0) - %t10 = and i64 %t4, %t6 - %t12 = and i64 %t10, 1 - %t13 = icmp eq i64 %t12, 1 - br i1 %t13, label %b3, label %b4 + call void @prism_rc_inc(i64 %t4) + %t9 = and i64 %t4, %t6 + %t11 = and i64 %t9, 1 + %t12 = icmp eq i64 %t11, 1 + br i1 %t12, label %b3, label %b4 b1: ; preds = %entry call void @prism_match_error() unreachable b3: ; preds = %b2 - %t15 = ashr i64 %t4, 1 - %t17 = ashr i64 %t6, 1 - %t18 = add i64 %t15, %t17 - %t20 = shl i64 %t18, 1 - %t21 = ashr i64 %t20, 1 - %t22 = icmp eq i64 %t21, %t18 - br i1 %t22, label %b6, label %b4 + %t14 = ashr i64 %t4, 1 + %t16 = ashr i64 %t6, 1 + %t17 = add i64 %t14, %t16 + %t19 = shl i64 %t17, 1 + %t20 = ashr i64 %t19, 1 + %t21 = icmp eq i64 %t20, %t17 + br i1 %t21, label %b6, label %b4 b4: ; preds = %b3, %b2 - %t24 = call i64 @prism_rt_int_add(i64 %t4, i64 %t6) + %t23 = call i64 @prism_rt_int_add(i64 %t4, i64 %t6) br label %b5 b6: ; preds = %b3 - %t23 = or i64 %t20, 1 + %t22 = or i64 %t19, 1 br label %b5 b5: ; preds = %b4, %b6 - %t25 = phi i64 [ %t23, %b6 ], [ %t24, %b4 ] + %t24 = phi i64 [ %t22, %b6 ], [ %t23, %b4 ] call void @prism_rc_dec(i64 %t4) call void @prism_rc_dec(i64 %t6) - ret i64 %t25 + ret i64 %t24 } ; Function Attrs: nounwind @@ -566,10 +567,10 @@ declare void @prism_match_error() #0 declare void @prism_rc_inc(i64) #0 ; Function Attrs: nounwind -declare void @prism_rc_dec(i64) #0 +declare i64 @prism_rt_int_add(i64, i64) #0 ; Function Attrs: nounwind -declare i64 @prism_rt_int_add(i64, i64) #0 +declare void @prism_rc_dec(i64) #0 ; Function Attrs: nounwind define i64 @prismfn_main() #0 { @@ -583,16 +584,18 @@ entry: store i64 9, ptr %t7, align 8 %t8 = ptrtoint ptr %t3 to i64 %t9 = call i64 @prismfn_sum_coords(i64 %t8) - %t13 = call ptr @prism_alloc(i64 2) - %t14 = getelementptr inbounds i8, ptr %t13, i64 8 - store i64 0, ptr %t14, align 8 - %t16 = getelementptr inbounds i8, ptr %t13, i64 24 - store i64 21, ptr %t16, align 8 - %t17 = getelementptr inbounds i8, ptr %t13, i64 32 - store i64 9, ptr %t17, align 8 - %t18 = ptrtoint ptr %t13 to i64 - %t19 = call i64 @prismfn_sum_coords(i64 %t18) + call void @prism_rc_dec(i64 %t8) + %t14 = call ptr @prism_alloc(i64 2) + %t15 = getelementptr inbounds i8, ptr %t14, i64 8 + store i64 0, ptr %t15, align 8 + %t17 = getelementptr inbounds i8, ptr %t14, i64 24 + store i64 21, ptr %t17, align 8 + %t18 = getelementptr inbounds i8, ptr %t14, i64 32 + store i64 9, ptr %t18, align 8 + %t19 = ptrtoint ptr %t14 to i64 + %t20 = call i64 @prismfn_sum_coords(i64 %t19) call void @prism_rc_dec(i64 %t19) + call void @prism_rc_dec(i64 %t20) call void @prism_print_int(i64 %t9) call void @prism_rc_dec(i64 %t9) call void @prism_rc_dec(i64 0) diff --git a/tests/snapshots/snapshots__pipeline@reuseclash.pr.snap b/tests/snapshots/snapshots__pipeline@reuseclash.pr.snap index 2faa70d2..ceafee5e 100644 --- a/tests/snapshots/snapshots__pipeline@reuseclash.pr.snap +++ b/tests/snapshots/snapshots__pipeline@reuseclash.pr.snap @@ -303,6 +303,7 @@ fn bump(p, _reuse_p) = reuse_token t@0 to reuse#t@0 return a to t@1 return _reuse_p to t@2 + dup t@2 t@1 + t@2 to t@3 return b to t@4 reuse reuse#t@0 as Pair(t@3, t@4) @@ -311,7 +312,9 @@ fn main() = return 2 to t@6 return Pair(t@5, t@6) to t@7 return 10 to t@8 - bump(t@7, t@8) to t@9 + bump(t@7, t@8) to %rc0 + drop t@8 + return %rc0 to t@9 case t@9 of Pair(x, y) => dup x @@ -328,8 +331,8 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@prism_native_kont_table = constant [834 x i8] c"scheme prism-core-hash-v2\0Abundle 0b6e92ccb03f1350c3e4039173e2c3065ae012ce764dfa821f0581698a07d82f\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:0b6e92ccb03f1350c3e4039173e2c3065ae012ce764dfa821f0581698a07d82f\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_bump 0e2f4d23e7923d9b46b2eef455134c45ff9163cc0fbc7c61399a380a074666c6 bump\0Afn prismfn_main e02996c36b0d51e6364b334970c24e93217e0e910eef6a0b442e34cd9958b5df main\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [952 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 0b6e92ccb03f1350c3e4039173e2c3065ae012ce764dfa821f0581698a07d82f\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:0b6e92ccb03f1350c3e4039173e2c3065ae012ce764dfa821f0581698a07d82f\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_bump 0e2f4d23e7923d9b46b2eef455134c45ff9163cc0fbc7c61399a380a074666c6 bump arity 2 slots abi-word[arg0=%a0:word,arg1=%a1:word]\0Astate prismfn_main e02996c36b0d51e6364b334970c24e93217e0e910eef6a0b442e34cd9958b5df main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [859 x i8] c"scheme prism-core-hash-v2\0Abundle 0b6e92ccb03f1350c3e4039173e2c3065ae012ce764dfa821f0581698a07d82f\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:0b6e92ccb03f1350c3e4039173e2c3065ae012ce764dfa821f0581698a07d82f\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_bump 0e2f4d23e7923d9b46b2eef455134c45ff9163cc0fbc7c61399a380a074666c6 bump\0Afn prismfn_main e02996c36b0d51e6364b334970c24e93217e0e910eef6a0b442e34cd9958b5df main\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [977 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 0b6e92ccb03f1350c3e4039173e2c3065ae012ce764dfa821f0581698a07d82f\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:0b6e92ccb03f1350c3e4039173e2c3065ae012ce764dfa821f0581698a07d82f\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_bump 0e2f4d23e7923d9b46b2eef455134c45ff9163cc0fbc7c61399a380a074666c6 bump arity 2 slots abi-word[arg0=%a0:word,arg1=%a1:word]\0Astate prismfn_main e02996c36b0d51e6364b334970c24e93217e0e910eef6a0b442e34cd9958b5df main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 @.kont_symbol1 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash1 = private constant [65 x i8] c"e02996c36b0d51e6364b334970c24e93217e0e910eef6a0b442e34cd9958b5df\00" @.kont_name1 = private constant [5 x i8] c"main\00" diff --git a/tests/snapshots/snapshots__pipeline@rowcapture.pr.snap b/tests/snapshots/snapshots__pipeline@rowcapture.pr.snap index 650d657d..4212601d 100644 --- a/tests/snapshots/snapshots__pipeline@rowcapture.pr.snap +++ b/tests/snapshots/snapshots__pipeline@rowcapture.pr.snap @@ -337,8 +337,8 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@prism_native_kont_table = constant [1046 x i8] c"scheme prism-core-hash-v2\0Abundle a5fd66bf47aba568cace748f2f01888699e79cc6e28418e6e75e10689e23abd3\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:a5fd66bf47aba568cace748f2f01888699e79cc6e28418e6e75e10689e23abd3\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_apply_io 583cce27692a6243a6e93c99e4c31206ec5b5961058d94a9204c1f402795ead0 apply_io\0Afn prismfn_call_poly 2f6af4859e63091b8c929055d2b5872a8625caf7b7dfaaa74d56f0a921d594d4 call_poly\0Afn prismfn_main 716ff34eee0cc6d2a9a5ccfd65dbc0f753ba2606f5c6c2b82cfab1312d43f43d main\0Afn prismfn_use_pure 5912aa8066f3bd684170054e675507a98a7123b5813e4a36dc833fb86ce8cfe5 use_pure\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [1081 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle a5fd66bf47aba568cace748f2f01888699e79cc6e28418e6e75e10689e23abd3\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:a5fd66bf47aba568cace748f2f01888699e79cc6e28418e6e75e10689e23abd3\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_apply_io 583cce27692a6243a6e93c99e4c31206ec5b5961058d94a9204c1f402795ead0 apply_io arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_main 716ff34eee0cc6d2a9a5ccfd65dbc0f753ba2606f5c6c2b82cfab1312d43f43d main arity 0 slots abi-word[]\0Astate prismfn_use_pure 5912aa8066f3bd684170054e675507a98a7123b5813e4a36dc833fb86ce8cfe5 use_pure arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [1071 x i8] c"scheme prism-core-hash-v2\0Abundle a5fd66bf47aba568cace748f2f01888699e79cc6e28418e6e75e10689e23abd3\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:a5fd66bf47aba568cace748f2f01888699e79cc6e28418e6e75e10689e23abd3\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_apply_io 583cce27692a6243a6e93c99e4c31206ec5b5961058d94a9204c1f402795ead0 apply_io\0Afn prismfn_call_poly 2f6af4859e63091b8c929055d2b5872a8625caf7b7dfaaa74d56f0a921d594d4 call_poly\0Afn prismfn_main 716ff34eee0cc6d2a9a5ccfd65dbc0f753ba2606f5c6c2b82cfab1312d43f43d main\0Afn prismfn_use_pure 5912aa8066f3bd684170054e675507a98a7123b5813e4a36dc833fb86ce8cfe5 use_pure\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [1106 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle a5fd66bf47aba568cace748f2f01888699e79cc6e28418e6e75e10689e23abd3\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:a5fd66bf47aba568cace748f2f01888699e79cc6e28418e6e75e10689e23abd3\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_apply_io 583cce27692a6243a6e93c99e4c31206ec5b5961058d94a9204c1f402795ead0 apply_io arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_main 716ff34eee0cc6d2a9a5ccfd65dbc0f753ba2606f5c6c2b82cfab1312d43f43d main arity 0 slots abi-word[]\0Astate prismfn_use_pure 5912aa8066f3bd684170054e675507a98a7123b5813e4a36dc833fb86ce8cfe5 use_pure arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 @.kont_symbol2 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash2 = private constant [65 x i8] c"716ff34eee0cc6d2a9a5ccfd65dbc0f753ba2606f5c6c2b82cfab1312d43f43d\00" @.kont_name2 = private constant [5 x i8] c"main\00" diff --git a/tests/snapshots/snapshots__pipeline@rowforall.pr.snap b/tests/snapshots/snapshots__pipeline@rowforall.pr.snap index 8598588a..e986cbe9 100644 --- a/tests/snapshots/snapshots__pipeline@rowforall.pr.snap +++ b/tests/snapshots/snapshots__pipeline@rowforall.pr.snap @@ -258,8 +258,8 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@prism_native_kont_table = constant [943 x i8] c"scheme prism-core-hash-v2\0Abundle dceac1a550993bfe2c90bf8f9b29124aaa22a76763366ecb63a309c85b096b8a\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:dceac1a550993bfe2c90bf8f9b29124aaa22a76763366ecb63a309c85b096b8a\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_apply_io 583cce27692a6243a6e93c99e4c31206ec5b5961058d94a9204c1f402795ead0 apply_io\0Afn prismfn_main c83cde4a76c3e307774a1fbc3cc61c9b1739cfe834fcd8cf3e7c5ad8ab206985 main\0Afn prismfn_use_pure 4bf009054a50cffa1dfef8c85fbc02c6f63bc723b24a387c951f32d4faa4cd66 use_pure\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [1081 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle dceac1a550993bfe2c90bf8f9b29124aaa22a76763366ecb63a309c85b096b8a\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:dceac1a550993bfe2c90bf8f9b29124aaa22a76763366ecb63a309c85b096b8a\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_apply_io 583cce27692a6243a6e93c99e4c31206ec5b5961058d94a9204c1f402795ead0 apply_io arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_main c83cde4a76c3e307774a1fbc3cc61c9b1739cfe834fcd8cf3e7c5ad8ab206985 main arity 0 slots abi-word[]\0Astate prismfn_use_pure 4bf009054a50cffa1dfef8c85fbc02c6f63bc723b24a387c951f32d4faa4cd66 use_pure arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [968 x i8] c"scheme prism-core-hash-v2\0Abundle dceac1a550993bfe2c90bf8f9b29124aaa22a76763366ecb63a309c85b096b8a\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:dceac1a550993bfe2c90bf8f9b29124aaa22a76763366ecb63a309c85b096b8a\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_apply_io 583cce27692a6243a6e93c99e4c31206ec5b5961058d94a9204c1f402795ead0 apply_io\0Afn prismfn_main c83cde4a76c3e307774a1fbc3cc61c9b1739cfe834fcd8cf3e7c5ad8ab206985 main\0Afn prismfn_use_pure 4bf009054a50cffa1dfef8c85fbc02c6f63bc723b24a387c951f32d4faa4cd66 use_pure\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [1106 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle dceac1a550993bfe2c90bf8f9b29124aaa22a76763366ecb63a309c85b096b8a\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:dceac1a550993bfe2c90bf8f9b29124aaa22a76763366ecb63a309c85b096b8a\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_apply_io 583cce27692a6243a6e93c99e4c31206ec5b5961058d94a9204c1f402795ead0 apply_io arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_main c83cde4a76c3e307774a1fbc3cc61c9b1739cfe834fcd8cf3e7c5ad8ab206985 main arity 0 slots abi-word[]\0Astate prismfn_use_pure 4bf009054a50cffa1dfef8c85fbc02c6f63bc723b24a387c951f32d4faa4cd66 use_pure arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 @.kont_symbol1 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash1 = private constant [65 x i8] c"c83cde4a76c3e307774a1fbc3cc61c9b1739cfe834fcd8cf3e7c5ad8ab206985\00" @.kont_name1 = private constant [5 x i8] c"main\00" diff --git a/tests/snapshots/snapshots__pipeline@strings.pr.snap b/tests/snapshots/snapshots__pipeline@strings.pr.snap index 6e4692aa..004b894c 100644 --- a/tests/snapshots/snapshots__pipeline@strings.pr.snap +++ b/tests/snapshots/snapshots__pipeline@strings.pr.snap @@ -481,14 +481,14 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@.str0 = private constant [6 x i8] c" item\00" -@.str1 = private constant [7 x i8] c" items\00" -@.str2 = private constant [7 x i8] c"hello \00" -@.str3 = private constant [6 x i8] c"world\00" +@.str0 = private constant { i64, i64, i64, [6 x i8] } { i64 1152921504606846976, i64 1398034944, i64 5, [6 x i8] c" item\00" }, align 8 +@.str1 = private constant { i64, i64, i64, [7 x i8] } { i64 1152921504606846976, i64 1398034944, i64 6, [7 x i8] c" items\00" }, align 8 +@.str2 = private constant { i64, i64, i64, [7 x i8] } { i64 1152921504606846976, i64 1398034944, i64 6, [7 x i8] c"hello \00" }, align 8 +@.str3 = private constant { i64, i64, i64, [6 x i8] } { i64 1152921504606846976, i64 1398034944, i64 5, [6 x i8] c"world\00" }, align 8 @.fmts = private constant [3 x i8] c"%s\00" -@.str4 = private constant [6 x i8] c"prism\00" -@prism_native_kont_table = constant [937 x i8] c"scheme prism-core-hash-v2\0Abundle b6737e648bb2a7f250501dc800bb5a1039c17c5336a9afc5ae7aabde7ef490fe\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:b6737e648bb2a7f250501dc800bb5a1039c17c5336a9afc5ae7aabde7ef490fe\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_describe 18e63c20be3a5d54856fcbc182a0428d49a49ac0c146bd2329e0df18b7f177bd describe\0Afn prismfn_greet c29f78e53a40fc6f1a9897c39349a598f60c970a4615f97da3f5f7ea9a925a9b greet\0Afn prismfn_main 45e72d6ce79839422755da7f72c46ec6ca90f0e20f8a1277db1aa7bf8641e679 main\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [1075 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle b6737e648bb2a7f250501dc800bb5a1039c17c5336a9afc5ae7aabde7ef490fe\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:b6737e648bb2a7f250501dc800bb5a1039c17c5336a9afc5ae7aabde7ef490fe\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_describe 18e63c20be3a5d54856fcbc182a0428d49a49ac0c146bd2329e0df18b7f177bd describe arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_greet c29f78e53a40fc6f1a9897c39349a598f60c970a4615f97da3f5f7ea9a925a9b greet arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_main 45e72d6ce79839422755da7f72c46ec6ca90f0e20f8a1277db1aa7bf8641e679 main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 +@.str4 = private constant { i64, i64, i64, [6 x i8] } { i64 1152921504606846976, i64 1398034944, i64 5, [6 x i8] c"prism\00" }, align 8 +@prism_native_kont_table = constant [962 x i8] c"scheme prism-core-hash-v2\0Abundle b6737e648bb2a7f250501dc800bb5a1039c17c5336a9afc5ae7aabde7ef490fe\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:b6737e648bb2a7f250501dc800bb5a1039c17c5336a9afc5ae7aabde7ef490fe\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_describe 18e63c20be3a5d54856fcbc182a0428d49a49ac0c146bd2329e0df18b7f177bd describe\0Afn prismfn_greet c29f78e53a40fc6f1a9897c39349a598f60c970a4615f97da3f5f7ea9a925a9b greet\0Afn prismfn_main 45e72d6ce79839422755da7f72c46ec6ca90f0e20f8a1277db1aa7bf8641e679 main\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [1100 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle b6737e648bb2a7f250501dc800bb5a1039c17c5336a9afc5ae7aabde7ef490fe\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:b6737e648bb2a7f250501dc800bb5a1039c17c5336a9afc5ae7aabde7ef490fe\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_describe 18e63c20be3a5d54856fcbc182a0428d49a49ac0c146bd2329e0df18b7f177bd describe arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_greet c29f78e53a40fc6f1a9897c39349a598f60c970a4615f97da3f5f7ea9a925a9b greet arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_main 45e72d6ce79839422755da7f72c46ec6ca90f0e20f8a1277db1aa7bf8641e679 main arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 @.kont_symbol0 = private constant [17 x i8] c"prismfn_describe\00" @.kont_hash0 = private constant [65 x i8] c"18e63c20be3a5d54856fcbc182a0428d49a49ac0c146bd2329e0df18b7f177bd\00" @.kont_name0 = private constant [9 x i8] c"describe\00" @@ -534,16 +534,14 @@ b2: ; preds = %b1, %b0 b3: ; preds = %b2 call void @prism_rc_dec(i64 %t19) - %t23 = call i64 @prism_str_lit(ptr @.str0, i64 5) br label %b5 b4: ; preds = %b2 call void @prism_rc_dec(i64 %t19) - %t25 = call i64 @prism_str_lit(ptr @.str1, i64 6) br label %b5 b5: ; preds = %b4, %b3 - %t26 = phi i64 [ %t23, %b3 ], [ %t25, %b4 ] + %t26 = phi i64 [ ptrtoint (ptr @.str0 to i64), %b3 ], [ ptrtoint (ptr @.str1 to i64), %b4 ] %t27 = call i64 @prism_str_concat(i64 %t1, i64 %t26) call void @prism_rc_dec(i64 %t1) call void @prism_rc_dec(i64 %t26) @@ -562,20 +560,15 @@ declare void @prism_rc_dec(i64) #0 ; Function Attrs: nounwind declare i64 @prism_rt_int_cmp(i64, i64) #0 -; Function Attrs: nounwind -declare i64 @prism_str_lit(ptr, i64) #0 - ; Function Attrs: nounwind declare i64 @prism_str_concat(i64, i64) #0 ; Function Attrs: nounwind define i64 @prismfn_main() #0 { entry: - %t0 = call i64 @prism_str_lit(ptr @.str2, i64 6) - %t1 = call i64 @prism_str_lit(ptr @.str3, i64 5) - %t2 = call i64 @prism_str_concat(i64 %t0, i64 %t1) - call void @prism_rc_dec(i64 %t0) - call void @prism_rc_dec(i64 %t1) + %t2 = call i64 @prism_str_concat(i64 ptrtoint (ptr @.str2 to i64), i64 ptrtoint (ptr @.str3 to i64)) + call void @prism_rc_dec(i64 ptrtoint (ptr @.str2 to i64)) + call void @prism_rc_dec(i64 ptrtoint (ptr @.str3 to i64)) %t3 = inttoptr i64 %t2 to ptr %t4 = getelementptr inbounds i8, ptr %t3, i64 24 %0 = call i32 (ptr, ...) @printf(ptr @.fmts, ptr %t4) @@ -608,11 +601,10 @@ entry: call void @prism_rc_dec(i64 0) call void @prism_print_nl() call void @prism_rc_dec(i64 0) - %t35 = call i64 @prism_str_lit(ptr @.str4, i64 5) - %t36 = call i64 @prism_str_len(i64 %t35) + %t36 = call i64 @prism_str_len(i64 ptrtoint (ptr @.str4 to i64)) %t38 = shl i64 %t36, 1 %t39 = or i64 %t38, 1 - call void @prism_rc_dec(i64 %t35) + call void @prism_rc_dec(i64 ptrtoint (ptr @.str4 to i64)) %t40 = call i64 @prism_show_int(i64 %t39) call void @prism_rc_dec(i64 %t39) %t41 = inttoptr i64 %t40 to ptr diff --git a/tests/snapshots/snapshots__pipeline@synonyms.pr.snap b/tests/snapshots/snapshots__pipeline@synonyms.pr.snap index 3aa21da7..81ba3ac1 100644 --- a/tests/snapshots/snapshots__pipeline@synonyms.pr.snap +++ b/tests/snapshots/snapshots__pipeline@synonyms.pr.snap @@ -326,7 +326,6 @@ fn fst(p) = case t@0 of (a, b) => dup a - drop t@0 return a fn quad(x) = dup x @@ -347,15 +346,17 @@ fn main() = dup l drop t@8 return l to t@9 - fst(t@9) to t@10 + fst(t@9) to %rc0 + drop t@9 + return %rc0 to t@10 print t@10 == llvm == ; ModuleID = 'prism' source_filename = "prism" -@prism_native_kont_table = constant [925 x i8] c"scheme prism-core-hash-v2\0Abundle 4988fa66fa21c6ce96e5d76d1c6c526a3c53ed90d32c8d39ec458b06eb82948e\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:4988fa66fa21c6ce96e5d76d1c6c526a3c53ed90d32c8d39ec458b06eb82948e\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_fst 0c794ffc4b6d79856a941321cd04eb90758d0a2c94b3f2c352a2b1e62ef7941d fst\0Afn prismfn_main 7a61ada4c91dd0ba60386d7952f41628b6f01b11f1d8b685c4ad4991e684be69 main\0Afn prismfn_quad 9e55865a39ff20a9aa4e28ad3828624a249f3769da5d1e241fb067f3e8eb6139 quad\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [1063 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 4988fa66fa21c6ce96e5d76d1c6c526a3c53ed90d32c8d39ec458b06eb82948e\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:4988fa66fa21c6ce96e5d76d1c6c526a3c53ed90d32c8d39ec458b06eb82948e\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_fst 0c794ffc4b6d79856a941321cd04eb90758d0a2c94b3f2c352a2b1e62ef7941d fst arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_main 7a61ada4c91dd0ba60386d7952f41628b6f01b11f1d8b685c4ad4991e684be69 main arity 0 slots abi-word[]\0Astate prismfn_quad 9e55865a39ff20a9aa4e28ad3828624a249f3769da5d1e241fb067f3e8eb6139 quad arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [950 x i8] c"scheme prism-core-hash-v2\0Abundle 4988fa66fa21c6ce96e5d76d1c6c526a3c53ed90d32c8d39ec458b06eb82948e\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:4988fa66fa21c6ce96e5d76d1c6c526a3c53ed90d32c8d39ec458b06eb82948e\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_fst 0c794ffc4b6d79856a941321cd04eb90758d0a2c94b3f2c352a2b1e62ef7941d fst\0Afn prismfn_main 7a61ada4c91dd0ba60386d7952f41628b6f01b11f1d8b685c4ad4991e684be69 main\0Afn prismfn_quad 9e55865a39ff20a9aa4e28ad3828624a249f3769da5d1e241fb067f3e8eb6139 quad\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [1088 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 4988fa66fa21c6ce96e5d76d1c6c526a3c53ed90d32c8d39ec458b06eb82948e\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:4988fa66fa21c6ce96e5d76d1c6c526a3c53ed90d32c8d39ec458b06eb82948e\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_fst 0c794ffc4b6d79856a941321cd04eb90758d0a2c94b3f2c352a2b1e62ef7941d fst arity 1 slots abi-word[arg0=%a0:word]\0Astate prismfn_main 7a61ada4c91dd0ba60386d7952f41628b6f01b11f1d8b685c4ad4991e684be69 main arity 0 slots abi-word[]\0Astate prismfn_quad 9e55865a39ff20a9aa4e28ad3828624a249f3769da5d1e241fb067f3e8eb6139 quad arity 1 slots abi-word[arg0=%a0:word]\0A\00", section ",.prism_kont", align 1 @.kont_symbol1 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash1 = private constant [65 x i8] c"7a61ada4c91dd0ba60386d7952f41628b6f01b11f1d8b685c4ad4991e684be69\00" @.kont_name1 = private constant [5 x i8] c"main\00" diff --git a/tests/snapshots/snapshots__pipeline@unboxed.pr.snap b/tests/snapshots/snapshots__pipeline@unboxed.pr.snap index 2576a8a7..95f06c3b 100644 --- a/tests/snapshots/snapshots__pipeline@unboxed.pr.snap +++ b/tests/snapshots/snapshots__pipeline@unboxed.pr.snap @@ -219,7 +219,6 @@ fn main() = == fbip (rc) == fn area(p) = - drop p return 0 fn point() = return 1 to t@0 @@ -250,8 +249,8 @@ fn main() = ; ModuleID = 'prism' source_filename = "prism" -@prism_native_kont_table = constant [929 x i8] c"scheme prism-core-hash-v2\0Abundle 2400d14acf9bfd847f93bcb38ce8400717f42d0919b2aa449b01d67f9638b03c\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:2400d14acf9bfd847f93bcb38ce8400717f42d0919b2aa449b01d67f9638b03c\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_area e5d34191d00f3c1b46ab44cd9178f165531c44e60d6e8d6db1d969cd824c6415 area\0Afn prismfn_main be28a15c73c71ffa74d35c52f6eb342e72f24d1127646c6be4b1c65b8ab48299 main\0Afn prismfn_point a2a44ba6dbf4057b1b1cf26097368bc2cf1efb453a067696f605182c4f058d10 point\0A\00", section ",.prism_kont", align 1 -@prism_native_kont_state_map = constant [927 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 2400d14acf9bfd847f93bcb38ce8400717f42d0919b2aa449b01d67f9638b03c\0Acompiler 0.19.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:2400d14acf9bfd847f93bcb38ce8400717f42d0919b2aa449b01d67f9638b03c\0Aflag stdlib-root prism-core-hash-v2:47ba5c0026d3903e5af056d51900f8d172356c8a5b65bd0b27d8dc9e1046f298\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main be28a15c73c71ffa74d35c52f6eb342e72f24d1127646c6be4b1c65b8ab48299 main arity 0 slots abi-word[]\0Astate prismfn_point a2a44ba6dbf4057b1b1cf26097368bc2cf1efb453a067696f605182c4f058d10 point arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_table = constant [954 x i8] c"scheme prism-core-hash-v2\0Abundle 2400d14acf9bfd847f93bcb38ce8400717f42d0919b2aa449b01d67f9638b03c\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:2400d14acf9bfd847f93bcb38ce8400717f42d0919b2aa449b01d67f9638b03c\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Afn prismfn_area e5d34191d00f3c1b46ab44cd9178f165531c44e60d6e8d6db1d969cd824c6415 area\0Afn prismfn_main be28a15c73c71ffa74d35c52f6eb342e72f24d1127646c6be4b1c65b8ab48299 main\0Afn prismfn_point a2a44ba6dbf4057b1b1cf26097368bc2cf1efb453a067696f605182c4f058d10 point\0A\00", section ",.prism_kont", align 1 +@prism_native_kont_state_map = constant [952 x i8] c"state-map 1\0Ascheme prism-core-hash-v2\0Abundle 2400d14acf9bfd847f93bcb38ce8400717f42d0919b2aa449b01d67f9638b03c\0Acompiler 0.20.0\0Atarget aarch64-apple-darwin\0Abackend llvm\0Aflag source-root prism-core-hash-v2:2400d14acf9bfd847f93bcb38ce8400717f42d0919b2aa449b01d67f9638b03c\0Aflag stdlib-root prism-core-hash-v2:c2fabaf2406a2ef30bbf1b94effcea78e6a8be638bf47330d87f21e090cd6115\0Aflag opt O1\0Aflag passes level-default\0Aflag disabled none\0Aflag backend-opt 2\0Aflag scheduler cooperative\0Aflag effect-tier auto\0Aflag erasures true\0Aflag native-effects true\0Aflag trampoline true\0Aflag fuse false\0Aflag borrow-infer true\0Aflag rt-checks false\0Aflag native-kont-frames false\0Aslot-format prism-native-abi-word-v1\0Astate prismfn_main be28a15c73c71ffa74d35c52f6eb342e72f24d1127646c6be4b1c65b8ab48299 main arity 0 slots abi-word[]\0Astate prismfn_point a2a44ba6dbf4057b1b1cf26097368bc2cf1efb453a067696f605182c4f058d10 point arity 0 slots abi-word[]\0A\00", section ",.prism_kont", align 1 @.kont_symbol1 = private constant [13 x i8] c"prismfn_main\00" @.kont_hash1 = private constant [65 x i8] c"be28a15c73c71ffa74d35c52f6eb342e72f24d1127646c6be4b1c65b8ab48299\00" @.kont_name1 = private constant [5 x i8] c"main\00" diff --git a/tests/snapshots/snapshots__prelude_type_checks.snap b/tests/snapshots/snapshots__prelude_type_checks.snap index d2e8c6e4..93bd1831 100644 --- a/tests/snapshots/snapshots__prelude_type_checks.snap +++ b/tests/snapshots/snapshots__prelude_type_checks.snap @@ -138,6 +138,7 @@ Data.String.starts_with : (String, String) -> Bool Data.String.str_join : (String, List(String)) -> String Data.String.str_of_char : (Char) -> String Data.String.str_repeat : (String, Int) -> String +Data.String.str_slice : (String, Int, Int) -> String Data.String.to_lower : (String) -> String Data.String.to_upper : (String) -> String Data.String.trim : (String) -> String @@ -235,7 +236,7 @@ read_line : () -> String ! {Console} ! {Console} repeat : forall e0 a. (Int, () -> a ! {e0}) -> Unit ! {e0} repeat_while : forall e0 a. (() -> Bool ! {e0}, () -> a ! {e0}) -> Unit ! {e0} run_io : forall e0 a. ((Unit) -> a ! {Console, Entropy, Env, FileSystem, IO, Output, Random, e0}) -> a ! {IO, e0} ! {IO} -scollect : forall e0 a b. ((Unit) -> b ! {Emit(a), e0}) -> List(a) ! {e0} +scollect : forall e0 a b. ((Unit) -> a ! {Emit(b), e0}) -> List(b) ! {e0} sfold : forall e0 a b c. ((Unit) -> b ! {Emit(c), e0}, a, (a, c) -> a ! {e0}) -> a ! {e0} show_list_body : forall a. (List(a), Bool) -> String signum : (Int) -> Int @@ -251,7 +252,7 @@ sort_by_ord : forall a. (List(a)) -> List(a) srange : forall a. (Int, Int) -> (a) -> Unit ! {Emit(Int)} srange_go : (Int, Int) -> Unit ! {Emit(Int)} ! {Emit} ssum : forall e0 a. ((Unit) -> a ! {Emit(Int), e0}) -> Int ! {e0} -stake : forall e1 a b c. ((Unit) -> b ! {Emit(a), e1}, Int) -> (c) -> Unit ! {Emit(a), e1} +stake : forall e1 a b c. ((Unit) -> a ! {Emit(b), e1}, Int) -> (c) -> Unit ! {Emit(b), e1} stake_go : forall e0 a b. ((Unit) -> a ! {Emit(b), e0}, Int) -> Unit ! {Emit(b), e0} ! {Emit} str_escape : (String) -> String str_hash : (String) -> U64 diff --git a/tests/snapshots/snapshots__stdlib_shape_digests.snap b/tests/snapshots/snapshots__stdlib_shape_digests.snap index 0a8033bf..a27e8530 100644 --- a/tests/snapshots/snapshots__stdlib_shape_digests.snap +++ b/tests/snapshots/snapshots__stdlib_shape_digests.snap @@ -53,32 +53,27 @@ inst 12662120fff3a63f blitArray inst 1484c21e1d7a4303 eqSyntax.Parse.ParseFailure inst 15262316bdb7f36d eqSyntax.Query.KindCount inst 155202e77a932cc7 monadOption -inst 172b9ac493da7a1c serializeTriple inst 18a6834e96b5f0ee divFloat inst 18aa123ccc461d74 ordBinder -inst 18c3fe8466a599db serializeUnit inst 18c8015f439c9554 eqSyntax.Lex.LexRaw inst 1939f104c7a5f4e3 toJsonBool inst 19eca12ba454ff08 ordData.Bind.Level inst 1a23c70818661885 ordName +inst 1b9d3137fc40309d serializeTriple inst 1be194ffb372ab90 showSyntax.Query.QueryReport inst 1e2136aedb09fff2 flatI64 -inst 1fd4115f38a9dafa serializeMap inst 2313595426d0d3ae ordU64 inst 24a897ee39810b9a showName inst 250ad4e22e091083 semigroupAny inst 27434c51cfdeb201 fromJsonInt inst 276c486c7867c8eb eqCli.Tokens -inst 276dc1051da45c1b blitBytes inst 294357f2a6d5b292 eqSyntax.Query.QueryReport inst 29abe537b0c15b8b eqSyntax.Token.Token inst 2a298eb2b2ed6cb6 eqSyntax.Lex.LexError inst 2b896f87943944b3 eqSyntax.Cursor.Infix -inst 2d72adc9332dabc2 serializeOption inst 2e2c2d736236457b fromJsonFloat inst 319798ef1c1dd0ca showSyntax.Lex.LexRaw inst 3222919cbc788740 showFloat -inst 3225f6c191564fec serializeI64 inst 326e2fbca02c0580 showData.Monoid.Any inst 3348aacecf95fb1c eqCli.Outcome inst 35aa6822d1a70e17 functorList @@ -92,7 +87,6 @@ inst 393574500cf310d7 eqData.Monoid.Min inst 39e5a0f780543811 arbitraryFloat inst 3a34d3003bb86071 showData.Validation.Validation inst 3a4452947308ddb2 showCli.ArgSpec -inst 3b410fc9485e4ebc serializeIncr@SnapEntry inst 3c4a64f86714f291 numU64 inst 3cce3edaf0be461a showSyntax.Token.TokenKind inst 3df19ff1bde256ed eqSyntax.Diagnostic.DiagPhase @@ -107,7 +101,6 @@ inst 432fb1c9e01d34cf showSyntax.Diagnostic.Diagnostic inst 43324d7381116f3e eqSyntax.TcInput.TcEffect inst 4350b4a8678f83ed showSyntax.Cursor.Assoc inst 4387745f00fecb1f showSyntax.TcInput.TcConstraint -inst 4393be89af3be815 serializeString inst 441f7472f2cffc7e showSyntax.Analysis.ExprCensus inst 451b31f7a22794fc monoidAny inst 456e8ac670885c5c eqSyntax.TcInput.TcFunction @@ -117,6 +110,7 @@ inst 46203e1bebcc14e5 eqStr inst 4658084ab4d6d055 showSyntax.Edit.EditError inst 46d5cea31a8579f1 toJsonInt inst 47d4ab0b853bc8aa showSyntax.Rename.RnUse +inst 485cbb7324c34466 serializeIncr@TSnapEntry inst 48da301982fa1669 showTime.Instant inst 4927437748bc5fb0 showSyntax.Cursor.Prefix inst 49e9307ea8a4e66a eqBinder @@ -129,6 +123,7 @@ inst 4da38a4d8d977ede ordFloat inst 4faa5ad05d5fe093 ordPair inst 516ef485f41c0348 eqTime.Duration inst 5190192c51196dfd eqSyntax.Cursor.Prefix +inst 530fe4b2ec98acd4 serializeUnit inst 531ad1aa88171b32 arbitraryOption inst 53dbf87c21339156 eqSyntax.Rename.RnUse inst 545196b38e7ceb5d hashFloat @@ -136,6 +131,7 @@ inst 55263a330734a799 showSyntax.TcInput.TcEffect inst 56bea178d8a67f0d arbitraryU64 inst 5715719f253e5fdb showTime.Wall inst 57f0070e727db976 eqSyntax.Rename.RenameRefusal +inst 5817f3ea276df450 serializeMap inst 584202c87221c87b showSyntax.Source.SourceFile inst 5870897b29d0a7f6 eqData.Monoid.All inst 5877388a5c27f668 semigroupAll @@ -147,6 +143,7 @@ inst 5a4a77c144347274 showOption inst 5a635e17638b9e31 showData.Monoid.Sum inst 5b0732503ad33892 eqList inst 5be2e4128704a85c eqTime.Wall +inst 5c3d9d8b2375924e serializeInt inst 5d6b08ee47ca1bb7 showData.Bind.Index inst 5db4dba496339c4e monoidUnit inst 5f378a5775166add divU64 @@ -154,12 +151,13 @@ inst 60490cb87c7b53a4 eqSyntax.TcInput.TcImport inst 60d26278c2fac508 showSyntax.Diagnostic.DiagPhase inst 611b171f8ee103cf showChar inst 617202a059bfcb39 showSyntax.Lex.LexError -inst 630d81a13b74a95e serializeBool inst 6439e31dad89fca7 eqName inst 65793ca677346371 applicativeOption +inst 658ca0080398f99a serializeBytes inst 65ac1b0c736a5fdd toJsonList inst 65c8144901dbb384 showU64 inst 663c26f7e16274b1 showConcurrent.Outcome +inst 68897a0bd1df1704 blitBytes inst 6955d71f87691cb7 eqCli.OptSpec inst 69bcebf96630c05f eqSyntax.Diagnostic.Diagnostic inst 6bf77963d2835d11 hashBool @@ -174,33 +172,35 @@ inst 74fd5c789c54f243 showSyntax.TcInput.TcImport inst 7528b87e60c57c69 eqSyntax.Source.Span inst 75f0f31a4a08adad showCli.OptSpec inst 760d53e85a8a819c showData.IntMap.IntMap -inst 770970b350260689 serializePair inst 771a3525c4de1f5b latUnit inst 778974430a2f68f4 powInt inst 796e55a895811093 showWire.Policy inst 7a486d1ae36b621b ordBool -inst 7b83846a4f033cb8 serializeList inst 7da1ea6c3b1625ab eqQuickcheck.Config +inst 7dfa206b0a5496aa serializeIncr@Snap inst 7e00a37cdc210523 showSyntax.Source.Span inst 7edfb1ccf9457547 hashUnit inst 7f11c117397713e8 showControl.Rewrite.RwFix inst 7f409130acdf05f6 showData.Bind.Level +inst 80eaea65911f4d8a serializeBool inst 82e5029163b4d70b eqOption inst 855588616d5e3b2a functorOption inst 85affa761110c80e showSyntax.TcInput.TcClass inst 870166dd73dce003 eqWire.Policy inst 8733104c0685b7d3 showWire.Loss inst 88cf534397895037 eqChar -inst 8935825707225238 serializeIncr@TSnapEntry inst 897eb0986ec107fc showSyntax.Token.Token +inst 8b63d3bbf2904a4c serializeString inst 8c85ef7899d0d1ac monoidAll +inst 8de5f51d940d3d0f serializeIncr@SnapEntry inst 8fd5f1bfdd3d2314 eqSyntax.Query.DeclHead inst 9059d6c006508fed showSyntax.Query.KindCount inst 94a9c34b4e69cc1f eqSyntax.TcInput.TcOp +inst 95a34f8c50fc788e serializeI64 inst 962889464f037b33 showSyntax.Parse.ParseFailure inst 962b9299beec1f9e showData.Monoid.All inst 96399032f8ab4eb4 eqData.Monoid.Any -inst 97e0af036de0795a serializeInt +inst 97ba659b07acb02a serializeIncr@TSnap inst 9832aa91b1d20cfd eqSyntax.Codec.CodecError inst 99ad3e49bdebc28e eqSyntax.TcInput.TcCtor inst 9a6dd53a6b70aef5 showBinder @@ -231,7 +231,7 @@ inst b1f66a0388184347 bifunctorResult inst b20b8f19d05d99e5 hashChar inst b34d9589c1e95130 eqInt inst b359629e0e1407d8 blitString -inst b45ffa5726cf7763 serializeChar +inst b469194cac8b518a serializeList inst b5b0697f731597f0 checkedI64 inst b720fe616f8961ca showQuickcheck.Outcome inst b7d52d59f55b6b5d eqSyntax.TcInput.TcInstance @@ -239,6 +239,7 @@ inst b823d36a0af0d09c arbitraryChar inst bd3e843ed0fd1f89 eqSyntax.TcInput.TcInputDoc inst bd40d9f8234bfb81 showList inst bdd26b06bb1abc23 arbitraryList +inst bf673cf47af5a65e serializePair inst c1f3044611e44093 traversableList inst c2f299544e1e7a10 hashStr inst c360aade83d16f2d powFloat @@ -262,11 +263,11 @@ inst cfed3932020f9cd5 eqWire.Loss inst d1b45419ba188eaf eqData.Monoid.Sum inst d2bfe6c67dd2219f arbitraryUnit inst d38d96ac6f6ff8f9 fromJsonList +inst d5a2895353a02d6a serializeU64 inst d65f0a2fb5847703 eqCli.ArgSpec -inst d7892b27a00d0811 serializeU64 inst d941abbb4ecfacee monoidSum inst d971cc3a09f3e66d eqData.Bind.Index -inst da3d5a90b97f4607 serializeIncr@Snap +inst da2b4c93adce4719 serializeOption inst da7086ba65c49766 monoidList inst da87a31f04ad5151 eqSyntax.Parse.Decl@PVis inst dad228a62ff84d13 eqSyntax.Cursor.Assoc @@ -280,17 +281,16 @@ inst e17ac32f8118517f showSyntax.Codec.CodecError inst e28f86dcb851b498 hashU64 inst e66b271edbd02162 showSyntax.TcInput.TcData inst e7489627cec790ec toJsonString +inst e81c9e8c2d5a7ef3 serializeChar inst eab8b1754a636ae1 showTime.Duration inst ec436d594144372f toJsonPair inst ec81211e527dcd79 semigroupSum inst ec9c63d8291e89aa eqSyntax.TcInput.TcData -inst f018210d26c18b13 serializeIncr@TSnap inst f0a21d3f32db8773 ordData.Bind.Index inst f12b6d84fd9ec9ac semigroupUnit inst f2fc3cb7479015cd showSyntax.TcInput.TcMethod inst f3f6cb752a0d94d7 eqResult inst f3f9e4041b99e067 showCli.Tokens -inst f64f2e798da09214 serializeBytes inst f6f92c468264bc5d checkedU64 inst f77dba1299264640 eqTriple inst f7cc806d65f8cf6e eqData.Monoid.Max @@ -302,6 +302,7 @@ inst feb937ab6b23c813 eqSyntax.Analysis.ExprCensus inst fec82c79772b6dc1 showData.Monoid.Product inst ff348c158014edfa showData.UnionFind.Payload.UfError inst ffb6d98740340d10 showSyntax.Resolved.RParam +shape 0116142250dfb483 Syntax.Parse.Expr@VarHead shape 0252c4bc01b7f95e Time.Instant shape 03408321fe6b4079 Cli.OptSpec shape 04a506598cb8be59 Json.Json @@ -429,6 +430,7 @@ shape 73ecab2c30a7a12e Entropy shape 74a0e58db23cf4cb Cli.CliError shape 75c61dbbd7df2da4 Control.Validate.Validate shape 75fe6fd31ba6e33f Syntax.Query.DeclHead +shape 761e164992fb2815 Syntax.Parse.Expr@LPath shape 76528ba1767ee920 Quickcheck.Outcome shape 790be282c8a4d4eb Return shape 7a20630919e52308 Syntax.Ast.CtorShape @@ -497,6 +499,7 @@ shape b9e75f3bdea89502 Cli.Tokens shape bd2fd5c66e7ea983 Var@j@12 shape bd7ab6242edfef40 Syntax.Report@RpLine shape c06c1a65ad8f1fe4 Syntax.Parse.GeneratedType@GeneratedDelimited +shape c183b9e6fb5b8dff Wire.Bytes shape c28a647880cea7a8 Syntax.Token.TokenKind shape c59fc44447161326 Var@log@4 shape c81bdb450e355fc1 Syntax.TcInput.TcInstance @@ -541,7 +544,6 @@ shape e7a5c13c0613cbde Wire.Policy shape e8383fd5daa5950d Sequence.Step shape e87f632e32548ecf Control.Solve.Solve shape eb6e20d9f732bcfd Syntax.Cursor.Prefix -shape eb83ea7dac4fb051 Wire.Bytes shape ebd32a7e8114ee8f Syntax.Source.SourceFile shape ecae7e8f9fb45ab9 Data.Monoid.Min shape ecb58e20c3e28430 Quickcheck.Gen diff --git a/tests/store_pkg/store_layout.rs b/tests/store_pkg/store_layout.rs index f8789196..75409e9f 100644 --- a/tests/store_pkg/store_layout.rs +++ b/tests/store_pkg/store_layout.rs @@ -4,19 +4,40 @@ use std::fs; use std::path::Path; +use std::sync::Mutex; +use std::time::{Duration, SystemTime}; use prism::core::HASH_SCHEME; use prism::store::disk::{ - resolve_store_path, CanonicalKey, DefMeta, Store, StoreHash, VerifiedRecord, Written, + resolve_store_path, CanonicalKey, DefMeta, GcProgress, Store, StoreHash, VerifiedRecord, + Written, OBJECT_SHARD_BUDGET, QUERY_SHARD_BUDGET, }; use prism::{commit_to_store, default_roots, with_prelude, Config}; use crate::support::TempDir; +// Backdate a file's mtime so gc's age cutoff treats it as old, independent of +// wall-clock delays between test setup and the `gc` call. +fn backdate(path: &Path, age: Duration) { + fs::File::open(path) + .unwrap() + .set_modified(SystemTime::now() - age) + .unwrap(); +} + // A representative full-length hex hash and a second distinct one. const H1: &str = "ab00112233445566778899aabbccddeeff00112233445566778899aabbccddee"; const H2: &str = "cd00112233445566778899aabbccddeeff00112233445566778899aabbccddee"; +// A query binding's on-disk home: sharded on the key's first two hex +// characters like every other layer, under one directory per kind. +fn query_path(root: &Path, kind: &str, key: &str) -> std::path::PathBuf { + root.join("queries") + .join(kind) + .join(&key[..2]) + .join(&key[2..]) +} + #[test] fn store_hash_rejects_noncanonical_text() { assert!(StoreHash::new(H1).is_ok()); @@ -191,14 +212,161 @@ fn concurrent_identical_query_writers_converge() { fn malformed_query_entry_is_never_a_hit() { let tmp = TempDir::new("store", "query-corrupt"); let store = Store::open_or_create(tmp.store_root()).unwrap(); - let path = tmp + let path = query_path(&tmp.store_root(), "linked-native", H1); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::write(path, b"not-a-query\n").unwrap(); + assert!(store.get_query("linked-native", H1).is_err()); +} + +#[test] +fn query_bindings_are_sharded_and_layout_stamped() { + let tmp = TempDir::new("store", "query-sharded"); + let store = Store::open_or_create(tmp.store_root()).unwrap(); + store.put(H2, b"native-output").unwrap(); + store.put_query("linked-native", H1, H2).unwrap(); + + // The binding lives at the sharded path, no flat sibling beside it, and + // the layer carries its own layout stamp (independent of the store-wide + // VERSION file, which must not move for a query layout change). + assert!(query_path(&tmp.store_root(), "linked-native", H1).is_file()); + assert!(!tmp + .store_root() + .join("queries") + .join("linked-native") + .join(H1) + .exists()); + let stamp = fs::read_to_string(tmp.store_root().join("queries").join("LAYOUT")).unwrap(); + assert_eq!(stamp, "prism-query-layout-v2\n"); +} + +#[test] +fn pre_shard_flat_binding_reads_empty_and_gc_retires_it() { + let tmp = TempDir::new("store", "query-pre-shard"); + let store = Store::open_or_create(tmp.store_root()).unwrap(); + store.put(H2, b"native-output").unwrap(); + + // A relic of the flat pre-sharding layout: a well-formed binding written + // directly under the kind directory, fresher than any gc cutoff. + let relic = tmp .store_root() .join("queries") .join("linked-native") .join(H1); - fs::create_dir_all(path.parent().unwrap()).unwrap(); - fs::write(path, b"not-a-query\n").unwrap(); - assert!(store.get_query("linked-native", H1).is_err()); + fs::create_dir_all(relic.parent().unwrap()).unwrap(); + fs::write(&relic, format!("prism-query-index-v1\n{H2}\n")).unwrap(); + + // The sharded read path never opens it: the binding is an ordinary miss. + assert_eq!(store.get_query("linked-native", H1).unwrap(), None); + + // Gc retires it regardless of age (bulk invalidation, not migration), and + // the relic never marks its output live: the object survives here only + // because it is still fresh. + let stats = store.gc(Duration::from_hours(24), false).unwrap(); + assert_eq!(stats.queries_removed, 1); + assert!(!relic.exists()); + assert!(store.has(H2)); +} + +#[test] +fn an_overfull_query_shard_sheds_its_oldest_bindings_on_publish() { + let tmp = TempDir::new("store", "query-evict"); + let store = Store::open_or_create(tmp.store_root()).unwrap(); + store.put(H2, b"native-output").unwrap(); + + // Keys crafted into one shard, published oldest-first; each binding is + // backdated to a distinct age so the eviction order is deterministic + // regardless of filesystem timestamp resolution. + let total = QUERY_SHARD_BUDGET.cap + 2; + let keys: Vec = (0..total).map(|i| format!("ab{i:062x}")).collect(); + for (i, key) in keys.iter().enumerate() { + store.put_query("linked-native", key, H2).unwrap(); + backdate( + &query_path(&tmp.store_root(), "linked-native", key), + Duration::from_secs((total - i) as u64), + ); + } + + // The publish that pushed the shard past its cap trimmed it back to the + // low-water mark (plus the entry just published), oldest bindings first. + let evicted = QUERY_SHARD_BUDGET.cap + 1 - QUERY_SHARD_BUDGET.low; + let shard = tmp + .store_root() + .join("queries") + .join("linked-native") + .join("ab"); + assert_eq!( + fs::read_dir(&shard).unwrap().count(), + QUERY_SHARD_BUDGET.low + 1 + ); + // An evicted binding is an ordinary miss, never an error. + assert_eq!(store.get_query("linked-native", &keys[0]).unwrap(), None); + assert_eq!( + store + .get_query("linked-native", &keys[evicted - 1]) + .unwrap(), + None + ); + assert_eq!( + store + .get_query("linked-native", &keys[evicted]) + .unwrap() + .as_deref(), + Some(H2) + ); + assert_eq!( + store + .get_query("linked-native", &keys[total - 1]) + .unwrap() + .as_deref(), + Some(H2) + ); +} + +#[test] +fn an_overfull_object_shard_sheds_oldest_and_a_hit_refreshes_age() { + let tmp = TempDir::new("store", "object-evict"); + let store = Store::open_or_create(tmp.store_root()).unwrap(); + + // A hit refreshes the stored object's age, keeping a re-derived object + // ahead of cold generations when its shard evicts. + store.put(H1, b"hot").unwrap(); + let hot = tmp + .store_root() + .join("objects") + .join(&H1[..2]) + .join(&H1[2..]); + backdate(&hot, Duration::from_hours(24)); + assert_eq!(store.put(H1, b"hot").unwrap(), Written::Hit); + let age = SystemTime::now() + .duration_since(fs::metadata(&hot).unwrap().modified().unwrap()) + .unwrap_or_default(); + assert!(age < Duration::from_hours(1)); + + // Same shape as the query-shard test, one layer down: overfilling one + // object shard trims it back to its low-water mark, oldest first, and an + // evicted object simply reads as absent. + let total = OBJECT_SHARD_BUDGET.cap + 2; + let hashes: Vec = (0..total).map(|i| format!("ef{i:062x}")).collect(); + for (i, hash) in hashes.iter().enumerate() { + store.put(hash, b"generation").unwrap(); + backdate( + &tmp.store_root() + .join("objects") + .join(&hash[..2]) + .join(&hash[2..]), + Duration::from_secs((total - i) as u64), + ); + } + let evicted = OBJECT_SHARD_BUDGET.cap + 1 - OBJECT_SHARD_BUDGET.low; + let shard = tmp.store_root().join("objects").join("ef"); + assert_eq!( + fs::read_dir(&shard).unwrap().count(), + OBJECT_SHARD_BUDGET.low + 1 + ); + assert!(!store.has(&hashes[0])); + assert!(!store.has(&hashes[evicted - 1])); + assert!(store.has(&hashes[evicted])); + assert!(store.has(&hashes[total - 1])); } #[test] @@ -294,3 +462,272 @@ fn unboxed_program_commits_without_panicking() { let stats = commit_to_store(&src, &roots, &cfg).expect("unboxed program commits"); assert!(stats.objects_written > 0); } + +#[test] +fn gc_removes_a_stale_unreferenced_object() { + let tmp = TempDir::new("store", "gc-stale-object"); + let store = Store::open_or_create(tmp.store_root()).unwrap(); + store.put(H1, b"stale").unwrap(); + let path = tmp + .store_root() + .join("objects") + .join(&H1[..2]) + .join(&H1[2..]); + backdate(&path, Duration::from_hours(48)); + + let stats = store.gc(Duration::from_hours(24), false).unwrap(); + + assert_eq!(stats.objects_removed, 1); + assert_eq!(stats.bytes_removed, 5); + assert!(!store.has(H1)); +} + +#[test] +fn gc_dry_run_never_touches_the_filesystem() { + let tmp = TempDir::new("store", "gc-dry-run"); + let store = Store::open_or_create(tmp.store_root()).unwrap(); + store.put(H1, b"stale").unwrap(); + let path = tmp + .store_root() + .join("objects") + .join(&H1[..2]) + .join(&H1[2..]); + backdate(&path, Duration::from_hours(48)); + + let stats = store.gc(Duration::from_hours(24), true).unwrap(); + + assert_eq!( + stats.objects_removed, 1, + "dry run still predicts what it would remove" + ); + assert!(store.has(H1), "dry run must not delete anything"); +} + +#[test] +fn gc_spares_an_object_still_bound_by_a_live_query() { + let tmp = TempDir::new("store", "gc-live-query"); + let store = Store::open_or_create(tmp.store_root()).unwrap(); + store.put(H2, b"native-output").unwrap(); + store.put_query("linked-native", H1, H2).unwrap(); + // Age the object, not the query binding: a fresh binding must keep an old + // object alive, proving the sweep consults liveness and not just an + // object's own mtime. + let object_path = tmp + .store_root() + .join("objects") + .join(&H2[..2]) + .join(&H2[2..]); + backdate(&object_path, Duration::from_hours(48)); + + let stats = store.gc(Duration::from_hours(24), false).unwrap(); + + assert_eq!(stats.objects_removed, 0); + assert!(store.has(H2)); + assert_eq!( + store.get_query("linked-native", H1).unwrap().as_deref(), + Some(H2) + ); +} + +#[test] +fn gc_spares_a_ref_protected_object_regardless_of_age() { + let tmp = TempDir::new("store", "gc-ref-protected"); + let store = Store::open_or_create(tmp.store_root()).unwrap(); + store.put(H1, b"pinned").unwrap(); + store.set_ref("pkg-root-test", H1).unwrap(); + // Age both the object and the ref index itself: unlike a query binding, a + // `refs` entry never expires on its own; only `remove_ref` drops it. + let object_path = tmp + .store_root() + .join("objects") + .join(&H1[..2]) + .join(&H1[2..]); + backdate(&object_path, Duration::from_hours(48)); + backdate( + &tmp.store_root().join("index").join("refs"), + Duration::from_hours(48), + ); + + let stats = store.gc(Duration::from_hours(24), false).unwrap(); + + assert_eq!(stats.objects_removed, 0); + assert!(store.has(H1)); +} + +#[test] +fn gc_prunes_a_stale_query_binding() { + let tmp = TempDir::new("store", "gc-stale-query"); + let store = Store::open_or_create(tmp.store_root()).unwrap(); + store.put(H2, b"native-output").unwrap(); + store.put_query("linked-native", H1, H2).unwrap(); + let binding_path = query_path(&tmp.store_root(), "linked-native", H1); + backdate(&binding_path, Duration::from_hours(48)); + + let stats = store.gc(Duration::from_hours(24), false).unwrap(); + + assert_eq!(stats.queries_removed, 1); + assert_eq!(store.get_query("linked-native", H1).unwrap(), None); + // The now-dangling object the pruned binding pointed to is itself + // unreferenced but still fresh, so this same pass leaves it alone. + assert!(store.has(H2)); +} + +#[test] +fn gc_spares_a_fresh_unreferenced_object() { + let tmp = TempDir::new("store", "gc-fresh"); + let store = Store::open_or_create(tmp.store_root()).unwrap(); + store.put(H1, b"just written").unwrap(); + + let stats = store.gc(Duration::from_hours(24), false).unwrap(); + + assert_eq!(stats.objects_removed, 0); + assert!(store.has(H1)); +} + +#[test] +fn an_overfull_meta_shard_sheds_its_oldest_blobs_on_publish() { + let tmp = TempDir::new("store", "meta-evict"); + let store = Store::open_or_create(tmp.store_root()).unwrap(); + + // Same shape as the object-shard test: metadata rides the object budget, + // so overfilling one meta shard trims it back to the low-water mark and + // an evicted blob simply reads as absent (its facts are re-derived). + let total = OBJECT_SHARD_BUDGET.cap + 2; + let hashes: Vec = (0..total).map(|i| format!("ef{i:062x}")).collect(); + let m = DefMeta { + name: "Data.Map.get".into(), + ty: "Map k v -> k -> Option v".into(), + doc: String::new(), + }; + for (i, hash) in hashes.iter().enumerate() { + store.put_meta(hash, &m).unwrap(); + backdate( + &tmp.store_root() + .join("meta") + .join(&hash[..2]) + .join(&hash[2..]), + Duration::from_secs((total - i) as u64), + ); + } + let evicted = OBJECT_SHARD_BUDGET.cap + 1 - OBJECT_SHARD_BUDGET.low; + let shard = tmp.store_root().join("meta").join("ef"); + assert_eq!( + fs::read_dir(&shard).unwrap().count(), + OBJECT_SHARD_BUDGET.low + 1 + ); + assert_eq!(store.get_meta(&hashes[0]).unwrap(), None); + assert_eq!(store.get_meta(&hashes[evicted - 1]).unwrap(), None); + assert_eq!(store.get_meta(&hashes[evicted]).unwrap(), Some(m.clone())); + assert_eq!(store.get_meta(&hashes[total - 1]).unwrap(), Some(m)); +} + +#[test] +fn an_object_shard_over_its_byte_budget_sheds_oldest_by_size() { + let tmp = TempDir::new("store", "object-byte-evict"); + let store = Store::open_or_create(tmp.store_root()).unwrap(); + + // Twenty 1 MiB objects in one shard stay far under the entry cap but blow + // through the 16 MiB byte cap; the publish that crosses it trims oldest + // entries until the shard is back under the 12 MiB byte low-water mark. + let payload = vec![0u8; 1 << 20]; + let total = 20; + let hashes: Vec = (0..total).map(|i| format!("ee{i:062x}")).collect(); + for (i, hash) in hashes.iter().enumerate() { + store.put(hash, &payload).unwrap(); + backdate( + &tmp.store_root() + .join("objects") + .join(&hash[..2]) + .join(&hash[2..]), + Duration::from_secs((total - i) as u64), + ); + } + // The trigger put's shard held 17 MiB besides the entry just published; + // trimming to 12 MiB removed the five oldest, and later puts stayed under + // the cap, so fifteen objects remain. + let shard = tmp.store_root().join("objects").join("ee"); + assert_eq!(fs::read_dir(&shard).unwrap().count(), 15); + for gone in &hashes[..5] { + assert!(!store.has(gone)); + } + for kept in &hashes[5..] { + assert!(store.has(kept)); + } +} + +#[test] +fn gc_drains_a_retired_object_tree_salvaging_live_and_fresh_entries() { + let tmp = TempDir::new("store", "gc-retired-drain"); + let store = Store::open_or_create(tmp.store_root()).unwrap(); + let root = tmp.store_root(); + + // Manufacture what a crashed bulk retirement leaves behind: a renamed + // objects tree at the store root, its manifest recording the origin, one + // shard holding a still-referenced entry, a fresh in-flight entry, and a + // dead one. + let live = H1; + let fresh = format!("ab{:062x}", 0x1111); + let dead = format!("ab{:062x}", 0xdead); + store.set_ref("pkg-root-test", live).unwrap(); + let tree = root.join(".retired.test"); + let shard = tree.join("ab"); + fs::create_dir_all(&shard).unwrap(); + fs::write( + tree.join(".retired-manifest"), + "prism-store-retired-v1\nobjects\n", + ) + .unwrap(); + let two_hours = Duration::from_hours(2); + fs::write(shard.join(&live[2..]), b"referenced").unwrap(); + backdate(&shard.join(&live[2..]), two_hours); + fs::write(shard.join(&fresh[2..]), b"in-flight").unwrap(); + fs::write(shard.join(&dead[2..]), b"dead").unwrap(); + backdate(&shard.join(&dead[2..]), two_hours); + + let phases = Mutex::new(Vec::new()); + let stats = store + .gc_with_progress(Duration::from_hours(24), false, &|beat: &GcProgress| { + phases.lock().unwrap().push(beat.phase.clone()); + }) + .unwrap(); + + // The referenced entry and the fresh one came back into the live layer; + // the dead one is gone with the tree. + assert!(store.has(live)); + assert!(store.has(&fresh)); + assert!(!store.has(&dead)); + assert!(!tree.exists()); + assert_eq!(stats.salvaged, 2); + assert_eq!(stats.objects_removed, 1); + let phases = phases.into_inner().unwrap(); + assert!( + phases.iter().any(|phase| phase == "drain objects"), + "drain phase must report progress, saw {phases:?}" + ); +} + +#[test] +fn census_counts_every_layer_by_name() { + let tmp = TempDir::new("store", "census"); + let store = Store::open_or_create(tmp.store_root()).unwrap(); + store.put(H1, b"one").unwrap(); + store.put(H2, b"two").unwrap(); + store + .put_meta( + H1, + &DefMeta { + name: "main".into(), + ty: "() -> Int".into(), + doc: String::new(), + }, + ) + .unwrap(); + store.put_query("linked-native", H1, H2).unwrap(); + + let census = store.census().unwrap(); + assert_eq!(census.files("objects"), 2); + assert_eq!(census.files("meta"), 1); + assert_eq!(census.files("queries/linked-native"), 1); + assert_eq!(census.files("no-such-layer"), 0); + assert!(census.total() >= 4); +} diff --git a/tests/support/mod.rs b/tests/support/mod.rs index 6b4a4eba..4da37571 100644 --- a/tests/support/mod.rs +++ b/tests/support/mod.rs @@ -9,6 +9,7 @@ // spelling, and we side with plain `pub`. #![allow(dead_code, unreachable_pub)] +use std::mem::size_of; use std::num::NonZeroUsize; use std::path::{Path, PathBuf}; use std::process::Command; @@ -34,18 +35,46 @@ pub const ALLOC_STATS: &str = "PRISM_ALLOC_STATS"; /// that arms a counter strips the reports out of the program's own stderr with /// [`program_stderr`] and reads a value back with [`counter_report`], so no /// harness re-types the line format. +/// +/// The list must name every counter the runtime can emit, not only the ones a +/// gate reads back, because an oracle that arms one counter still has to strip +/// its neighbours: the parity gate arms the balance and the allocation reports +/// together, so an unlisted line from either lands in the observation trace and +/// diverges the whole corpus against an interpreter that never printed it. +/// [`counter_reports_are_registered`] holds the list against the runtime source. const COUNTER_PREFIX: &str = "prism: "; const LEAKED_SUFFIX: &str = " cells leaked"; const ALLOCATED_SUFFIX: &str = " cells allocated"; +/// Companion byte total to [`ALLOCATED_SUFFIX`], reported under the same env var. +/// Cells are not one size, so a pass that stops copying a payload and starts +/// sharing it moves this and leaves the count alone. +pub const ALLOCATED_BYTES_SUFFIX: &str = " cell bytes allocated"; const REUSED_SUFFIX: &str = " cells reused"; const EFF_OPS_SUFFIX: &str = " eff ops allocated"; const DRIVE_STEPS_SUFFIX: &str = " drive steps"; +const PROMOTED_SUFFIX: &str = " cells promoted"; +const PROMOTION_SHARED_SUFFIX: &str = " promotion copies shared"; +const PROMOTION_NODES_SUFFIX: &str = " promotion nodes visited"; +const PROMOTION_EDGES_SUFFIX: &str = " promotion edges visited"; +const RC_INCS_SUFFIX: &str = " rc increments"; +const RC_CELL_INCS_SUFFIX: &str = " rc increments on cells"; +const RC_DECS_SUFFIX: &str = " rc decrements"; +const RC_CELL_DECS_SUFFIX: &str = " rc decrements on cells"; const COUNTER_SUFFIXES: &[&str] = &[ LEAKED_SUFFIX, ALLOCATED_SUFFIX, + ALLOCATED_BYTES_SUFFIX, REUSED_SUFFIX, EFF_OPS_SUFFIX, DRIVE_STEPS_SUFFIX, + PROMOTED_SUFFIX, + PROMOTION_SHARED_SUFFIX, + PROMOTION_NODES_SUFFIX, + PROMOTION_EDGES_SUFFIX, + RC_INCS_SUFFIX, + RC_CELL_INCS_SUFFIX, + RC_DECS_SUFFIX, + RC_CELL_DECS_SUFFIX, ]; /// A balanced run leaks nothing. const NO_LEAKED_CELLS: i64 = 0; @@ -59,6 +88,66 @@ fn is_counter_report(line: &str) -> bool { line.starts_with(COUNTER_PREFIX) && COUNTER_SUFFIXES.iter().any(|s| line.ends_with(s)) } +/// Directory holding the C runtime sources, relative to the crate root tests run +/// from. +const RUNTIME_DIR: &str = "runtime"; +/// A counter report's format string, up to and after the counted value. Keying on +/// `%ld` is what separates a counter from a genuine diagnostic: `read_file`'s +/// failure line also opens with [`COUNTER_PREFIX`] but interpolates a `%s`, and it +/// is program-visible stderr that must never be stripped. +const REPORT_FORMAT_OPEN: &str = "\"prism: %ld "; +const REPORT_FORMAT_CLOSE: &str = "\\n"; + +/// [`COUNTER_SUFFIXES`] and the runtime name the same set of counters, in two +/// languages, and nothing but this test makes them agree. Read the format strings +/// back out of the C source and check both directions: an unregistered report is +/// the expensive failure (it lands in the observation trace and diverges every +/// parity case against an interpreter that never printed it), and a registered +/// suffix the runtime no longer emits is a stale strip rule that would hide a +/// future line of real program output. +#[test] +fn counter_reports_are_registered() { + let mut emitted = Vec::new(); + let entries = + fs::read_dir(RUNTIME_DIR).unwrap_or_else(|e| panic!("cannot read {RUNTIME_DIR}: {e}")); + for entry in entries { + let path = entry.expect("runtime directory entry").path(); + if path.extension().and_then(|e| e.to_str()) != Some("c") { + continue; + } + let src = fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display())); + for rest in src.split(REPORT_FORMAT_OPEN).skip(1) { + // The literal ends at the next quote; these format strings escape none. + let literal = &rest[..rest.find('"').expect("unterminated format string")]; + let suffix = literal + .strip_suffix(REPORT_FORMAT_CLOSE) + .expect("counter report is one whole line"); + emitted.push((path.clone(), format!(" {suffix}"))); + } + } + + let unregistered: Vec<_> = emitted + .iter() + .filter(|(_, suffix)| !COUNTER_SUFFIXES.contains(&suffix.as_str())) + .map(|(path, suffix)| format!(" {} emits {suffix:?}", path.display())) + .collect(); + assert!( + unregistered.is_empty(), + "the runtime emits counter reports this module does not strip:\n{}", + unregistered.join("\n") + ); + + let stale: Vec<_> = COUNTER_SUFFIXES + .iter() + .filter(|suffix| !emitted.iter().any(|(_, e)| e == *suffix)) + .collect(); + assert!( + stale.is_empty(), + "these registered suffixes are no longer emitted by the runtime: {stale:?}" + ); +} + /// The value the runtime reported for the counter named by `suffix`, or `None` /// when that counter did not report (its env var was not set) or its line is /// malformed. Callers that armed the counter treat `None` as a failure: a missing @@ -76,6 +165,11 @@ pub fn counter_report(stderr: &str, suffix: &str) -> Option { /// Opt-in memoization of verified native cases: set it to skip programs whose /// complete toolchain fingerprint is unchanged since a previous green run. const GATE_CACHE: &str = "PRISM_GATE_CACHE"; +/// A runtime compiler-tool source override is outside the repository roots a +/// reusable receipt commits to. Even source-fingerprint mode cannot prove what +/// an arbitrary external directory contained, so enabling the override makes +/// every gate run cold and prevents it from writing a misleading green marker. +const TOOL_PACKAGES_ROOT: &str = "PRISM_TOOL_PACKAGES_ROOT"; /// Selects how the compiler half of the cache key is fingerprinted. Unset (the /// default) hashes the test executable itself, maximally conservative and stable /// between local runs where cargo does not rebuild. Set to `source` to hash the @@ -243,8 +337,8 @@ const CORPUS_DIRS: [&str; 2] = ["examples", "tests/cases/run"]; /// Committed `.pr` programs intentionally outside the runnable corpus, each with /// why. `corpus_skip_list_is_exact` (tests/parity.rs) asserts the set of programs /// that actually drop out of `corpus()` equals these keys, so a regression that -/// silently stops a program interpreting (which would quietly shrink every oracle -/// built on the corpus) fails CI by name, and a program that becomes runnable +/// stops a program interpreting fails CI by name instead of shrinking every +/// corpus-based oracle. A program that becomes runnable /// again is flagged here as a stale entry. Labels are `dir/name.pr`. pub const CORPUS_SKIPS: &[(&str, &str)] = &[ ("examples/capabilities.pr", "off-platform: getenv"), @@ -434,6 +528,140 @@ pub fn temp_bin(tag: &str, stem: &str) -> PathBuf { env::temp_dir().join(format!("prism_parity_{tag}_{}_{stem}", std::process::id())) } +/// Bytes of context printed either side of a first difference. +const DIFFERENCE_WINDOW: usize = 12; +/// Digest characters that name a binary; enough to tell two builds apart. +const DIGEST_PREFIX: usize = 16; + +/// Byte offsets into the ELF64 header and its section headers, named so the +/// reader below walks a documented layout instead of a pile of literals. +const ELF_MAGIC: &[u8] = b"\x7fELF"; +const ELF_CLASS_OFFSET: usize = 4; +const ELF_CLASS_64: u8 = 2; +const ELF_DATA_OFFSET: usize = 5; +const ELF_DATA_LITTLE_ENDIAN: u8 = 1; +const ELF_SECTION_TABLE_OFFSET: usize = 0x28; +const ELF_SECTION_ENTRY_SIZE_OFFSET: usize = 0x3a; +const ELF_SECTION_COUNT_OFFSET: usize = 0x3c; +const ELF_SECTION_NAME_TABLE_OFFSET: usize = 0x3e; +const SECTION_NAME_OFFSET: usize = 0x00; +const SECTION_TYPE_OFFSET: usize = 0x04; +const SECTION_FILE_OFFSET: usize = 0x18; +const SECTION_SIZE_OFFSET: usize = 0x20; +/// A section that occupies no file bytes, so no file offset falls inside it. +const SECTION_TYPE_NOBITS: u64 = 8; + +/// A little-endian integer field of `width` bytes. +fn le_number(bytes: &[u8], at: usize, width: usize) -> Option { + let field = bytes.get(at..at.checked_add(width)?)?; + Some(field.iter().enumerate().fold(0, |value, (index, byte)| { + value | u64::from(*byte) << (index * 8) + })) +} + +/// The name of the ELF section holding a file offset. +/// +/// `None` for any other container (a Mach-O image, a truncated file), where the +/// offset and the bytes around it have to speak for themselves. +fn elf_section_at(bytes: &[u8], offset: u64) -> Option { + if !bytes.starts_with(ELF_MAGIC) + || bytes.get(ELF_CLASS_OFFSET) != Some(&ELF_CLASS_64) + || bytes.get(ELF_DATA_OFFSET) != Some(&ELF_DATA_LITTLE_ENDIAN) + { + return None; + } + let table = le_number(bytes, ELF_SECTION_TABLE_OFFSET, size_of::())?; + let entry = le_number(bytes, ELF_SECTION_ENTRY_SIZE_OFFSET, size_of::())?; + let count = le_number(bytes, ELF_SECTION_COUNT_OFFSET, size_of::())?; + let name_section = le_number(bytes, ELF_SECTION_NAME_TABLE_OFFSET, size_of::())?; + let section_header = + |index: u64| usize::try_from(table.checked_add(index.checked_mul(entry)?)?).ok(); + let names = le_number( + bytes, + section_header(name_section)? + SECTION_FILE_OFFSET, + size_of::(), + )?; + for index in 0..count { + let header = section_header(index)?; + if le_number(bytes, header + SECTION_TYPE_OFFSET, size_of::())? == SECTION_TYPE_NOBITS + { + continue; + } + let start = le_number(bytes, header + SECTION_FILE_OFFSET, size_of::())?; + let size = le_number(bytes, header + SECTION_SIZE_OFFSET, size_of::())?; + if offset < start || offset >= start.checked_add(size)? { + continue; + } + let name = le_number(bytes, header + SECTION_NAME_OFFSET, size_of::())?; + let at = usize::try_from(names.checked_add(name)?).ok()?; + let text = bytes.get(at..)?; + let end = text.iter().position(|byte| *byte == 0)?; + return String::from_utf8(text[..end].to_vec()).ok(); + } + None +} + +fn short_digest(bytes: &[u8]) -> String { + blake3::hash(bytes).to_hex()[..DIGEST_PREFIX].to_string() +} + +fn hex_window(bytes: &[u8], at: usize) -> String { + let at = at.min(bytes.len()); + let start = at.saturating_sub(DIFFERENCE_WINDOW); + let end = (at + DIFFERENCE_WINDOW).min(bytes.len()); + bytes[start..end] + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::>() + .join(" ") +} + +/// Asserts two linked artifacts are byte-identical, and explains any difference. +/// +/// Byte identity is the assertion; legibility is why this exists. Comparing the +/// two byte vectors directly prints both binaries as decimal arrays, tens of +/// thousands of lines that truncate a CI log and still never say what moved. A +/// linked binary differs structurally, so report structure: a digest naming each +/// side, the two lengths, where they first diverge and in which section, how +/// many bytes differ in all, and the bytes around the divergence. That is the +/// difference between "the link is not reproducible" and knowing whether code +/// or only its placement changed. +pub fn assert_same_binary(what: &str, expected: &[u8], actual: &[u8]) { + if expected == actual { + return; + } + let shared = expected.len().min(actual.len()); + let first = expected + .iter() + .zip(actual) + .position(|(left, right)| left != right) + .unwrap_or(shared); + let differing = expected + .iter() + .zip(actual) + .filter(|(left, right)| left != right) + .count(); + let section = u64::try_from(first) + .ok() + .and_then(|offset| elf_section_at(expected, offset)) + .map_or_else(String::new, |name| format!(" in {name}")); + panic!( + "{what}: linked bytes differ\n \ + expected {} bytes blake3 {}\n \ + actual {} bytes blake3 {}\n \ + first difference at byte {first} ({first:#x}){section}, \ + {differing} of {shared} shared bytes differ\n \ + expected {}\n \ + actual {}", + expected.len(), + short_digest(expected), + actual.len(), + short_digest(actual), + hex_window(expected, first), + hex_window(actual, first), + ); +} + /// The single leak predicate for every native oracle: the balance report must be /// present and zero, and the program itself must have written nothing to stderr. /// The second half is what makes this stricter than a search for the leak line: @@ -684,6 +912,14 @@ fn collect_files(dir: &Path, filter: RootFilter, out: &mut Vec) { /// The cache directory when memoization is enabled, else `None`. Lives under /// `target/` so `cargo clean` clears it and it never enters version control. fn gate_cache_dir() -> Option { + if env::var_os(TOOL_PACKAGES_ROOT).is_some() { + if env::var_os(GATE_CACHE).is_some() { + eprintln!( + "gate-cache: disabled because {TOOL_PACKAGES_ROOT} selects runtime tool sources" + ); + } + return None; + } env::var_os(GATE_CACHE)?; let dir = Path::new(env!("CARGO_MANIFEST_DIR")) .join("target") @@ -817,7 +1053,7 @@ fn check_native_parity_uncached( // A program whose `main` returns a tagged-immediate value exits with that // value as its code; `canonical_exit` reconstructs it the way the native // `main` shim does, so exit-code divergence (the class the `error`/`exit` - // seam exposed) is now inside the oracle, not just the empty-stdin instances. + // seam exposed) is now inside the oracle for every instance. // A crash by signal reports no code and diverges here as well as truncating // stdout above. let want_exit = canonical_exit(&reference); @@ -901,19 +1137,29 @@ pub fn parallel_check( pub fn parallel_collect( cases: &[PathBuf], check: impl Fn(&Path) -> Result + Sync, +) -> (Vec, Vec) { + parallel_each(cases, |case| check(case)) +} + +/// The worker pool underneath [`parallel_check`], generic over the item type so +/// generated-program sweeps reuse the same big-stack corpus workers as +/// path-keyed sweeps. +pub fn parallel_each( + items: &[I], + check: impl Fn(&I) -> Result + Sync, ) -> (Vec, Vec) { let next = AtomicUsize::new(0); let fails: Mutex> = Mutex::new(Vec::new()); let values: Mutex> = Mutex::new(Vec::new()); let threads = thread::available_parallelism() .map_or(DEFAULT_PARALLELISM, NonZeroUsize::get) - .min(cases.len().max(MIN_WORKER_COUNT)); + .min(items.len().max(MIN_WORKER_COUNT)); thread::scope(|s| { for _ in 0..threads { let worker = || loop { let i = next.fetch_add(1, Ordering::Relaxed); - let Some(case) = cases.get(i) else { break }; - match check(case) { + let Some(item) = items.get(i) else { break }; + match check(item) { Ok(v) => values.lock().unwrap().push(v), Err(e) => fails.lock().unwrap().push(e), } diff --git a/tests/tier_equiv/gate.rs b/tests/tier_equiv/gate.rs new file mode 100644 index 00000000..e0a22a86 --- /dev/null +++ b/tests/tier_equiv/gate.rs @@ -0,0 +1,368 @@ +//! Whole-corpus effect-tier equivalence gate. +//! +//! Tier selection is a cost choice, so every forceable `EffectTier` position +//! must produce the same canonical observation trace. Each position floors the +//! cascade at one rung; the cascade still falls back to costlier rungs, and the +//! whole-program monad is legal for every program, so all five positions lower +//! every runnable corpus program with no skip logic. The auto position is the +//! baseline: it is the only one that can take the pure and evidence rungs, so +//! diffing every floor against it covers the full ladder. +//! +//! This proves lowered-Core semantic equivalence at interpreter cost, which is +//! what lets it sweep the whole corpus and a generated corpus. The native tier +//! gates (tier parity, tier cross, tier handler parity) independently prove +//! that the backend implements each tier's Core correctly and leak-free; they +//! stay authoritative for native behavior and this gate does not replace them. +//! +//! The representative sample is the fast semantic path, the early-exit +//! discovery keeps every adjacent pair of positions engaged, and the +//! whole-corpus relation partitions by source across the CI exact-cover +//! matrix. The generated sweep points the deterministic program generator at +//! the same relation and greedily shrinks any divergence to a minimal +//! reproducer. + +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Mutex; + +use prism::{default_roots, Config, EffectTier, ObservationTrace}; + +use crate::support::fuzzgen::{generate, generate_arena, shrink, Program, ProgramFamily}; +use crate::support::{ + corpus_candidates, corpus_is_sharded, heavy_corpus_delegated, parallel_check, parallel_each, + runnable_corpus_source, sharded_corpus, source, +}; + +/// The tier axis only exists after effect lowering, so engagement scans dump +/// this phase alone: pre-lowering Core is tier-independent by construction. +const ENGAGEMENT_PHASE: &str = "lowered"; + +/// Adjacent positions on the forced ladder. Each pair must change lowered Core +/// somewhere in the corpus, otherwise two positions have collapsed and the +/// sweep between them is vacuous. +const ADJACENT_POSITIONS: [(usize, usize); 4] = [(0, 1), (1, 2), (2, 3), (3, 4)]; + +const ACTIVITY_LABELS: [&str; 4] = [ + "auto versus state-fusion", + "state-fusion versus local-partial", + "local-partial versus selective-free-monad", + "selective-free-monad versus whole-program-free-monad", +]; + +/// Committed programs whose effect plans are known to move under forcing; they +/// keep the sample and the whole-corpus extension non-vacuous. +const FIXTURE_CASES: &[&str] = &[ + "tests/fixtures/tier_cross/thunk_param.pr", + "tests/fixtures/tier_cross/convention_split_map.pr", + "tests/fixtures/tier_cross/convention_split_map_unrolled.pr", +]; + +/// Corpus programs scanned first by the engagement discovery, one per rung the +/// blind alphabetical order reaches late. The local-partial rung in particular +/// is chosen by exactly one corpus program, so without seeding it the scan +/// walks most of the corpus (five lowerings per case) before the +/// local-partial/selective pair can engage. +const ENGAGEMENT_SEED_CASES: &[&str] = &[ + "examples/accum.pr", + "examples/eff_state.pr", + "tests/cases/run/local_mono_combined.pr", + "examples/eff_yield.pr", +]; + +#[derive(Debug)] +struct Variant { + label: &'static str, + config: Config, +} + +impl Variant { + fn tier(tier: EffectTier) -> Self { + let mut config = Config::default(); + config.flags.effect_tier = tier; + config.flags.compiler_cache = false; + config.flags.quiet = true; + Self { + label: tier.label(), + config, + } + } +} + +fn variants() -> Vec { + EffectTier::ALL.into_iter().map(Variant::tier).collect() +} + +fn record_lowered_activity(lowered: &[&str], activity: &[AtomicUsize]) { + for (slot, (left, right)) in ADJACENT_POSITIONS.into_iter().enumerate() { + if lowered[left] != lowered[right] { + activity[slot].fetch_add(1, Ordering::Relaxed); + } + } +} + +fn check_source( + label: &str, + full: &str, + roots: &[prism::Root], + variants: &[Variant], + activity: &[AtomicUsize], +) -> Result<(), String> { + let mut runs: Vec<(&Variant, ObservationTrace, String)> = Vec::with_capacity(variants.len()); + for variant in variants { + let (trace, lowered) = prism::driver::observe_lowered_run_on(full, roots, &variant.config) + .map_err(|error| { + format!( + "{label}: {} failed to observe lowered Core: {error}", + variant.label + ) + })?; + runs.push((variant, trace, lowered)); + } + let lowered = runs + .iter() + .map(|(_, _, lowered)| lowered.as_str()) + .collect::>(); + record_lowered_activity(&lowered, activity); + + let Some((baseline_variant, baseline_trace, _)) = runs.first() else { + return Err(format!("{label}: tier matrix is empty")); + }; + for (variant, trace, _) in &runs[1..] { + if trace != baseline_trace { + return Err(format!( + "tier observation trace diverges for {label}:\n {}: {:?}\n {}: {:?}", + baseline_variant.label, + baseline_trace.observations, + variant.label, + trace.observations, + )); + } + } + Ok(()) +} + +fn check_case( + case: &Path, + roots: &[prism::Root], + variants: &[Variant], + activity: &[AtomicUsize], +) -> Result<(), String> { + let full = source(case); + check_source( + &case.display().to_string(), + &full, + roots, + variants, + activity, + ) +} + +fn run_cases(cases: &[PathBuf], require_engagement: bool) { + let roots = default_roots(Path::new(".")); + let variants = variants(); + let activity: Vec = (0..ACTIVITY_LABELS.len()) + .map(|_| AtomicUsize::new(0)) + .collect(); + let fails = parallel_check(cases, |case| check_case(case, &roots, &variants, &activity)); + assert!( + fails.is_empty(), + "{} of {} tier-equivalence cases failed:\n{}", + fails.len(), + cases.len(), + fails.join("\n") + ); + + eprintln!( + "tier-equiv: {} cases, {} tiers, {} lowered-Core evaluator runs", + cases.len(), + variants.len(), + cases.len() * variants.len() + ); + for (slot, label) in ACTIVITY_LABELS.into_iter().enumerate() { + let changed = activity[slot].load(Ordering::Relaxed); + eprintln!("tier-equiv: {label} changed {changed} cases"); + if require_engagement { + assert!( + changed > 0, + "{label} changed no lowered Core in the runnable corpus; the sweep is vacuous" + ); + } + } +} + +#[test] +fn tier_equivalence_representative_sample() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let cases = [ + "examples/accum.pr", + "examples/eff_state.pr", + "examples/eff_yield.pr", + "examples/handlers_funval.pr", + "examples/delim.pr", + "examples/eff_poly.pr", + "examples/effectful_traverse.pr", + "examples/imperative.pr", + "tests/fixtures/tier_cross/thunk_param.pr", + "tests/fixtures/tier_cross/convention_split_map.pr", + ] + .into_iter() + .map(|case| root.join(case)) + .collect::>(); + run_cases(&cases, false); +} + +// Keep engagement independent of the exact-cover CI split: isolated shard +// processes cannot add their counters together. This scan stops as soon as +// every adjacent pair of positions has changed lowered Core somewhere and +// performs no evaluation, so it retains the anti-vacuity contract without +// recreating the heavyweight sweep. +#[test] +fn tier_configurations_are_engaged() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let roots = default_roots(Path::new(".")); + let variants = variants(); + let activity: Vec = (0..ACTIVITY_LABELS.len()) + .map(|_| AtomicUsize::new(0)) + .collect(); + let mut cases = FIXTURE_CASES + .iter() + .chain(ENGAGEMENT_SEED_CASES) + .map(|case| root.join(case)) + .collect::>(); + cases.extend(corpus_candidates()); + + for case in cases { + let full = source(&case); + if !runnable_corpus_source(&full) { + continue; + } + let dumped = variants + .iter() + .map(|variant| prism::dump_on(ENGAGEMENT_PHASE, &full, &roots, &variant.config)) + .collect::, _>>(); + let Ok(dumped) = dumped else { continue }; + let dumped = dumped.iter().map(String::as_str).collect::>(); + record_lowered_activity(&dumped, &activity); + if activity + .iter() + .all(|changed| changed.load(Ordering::Relaxed) > 0) + { + break; + } + } + + for (slot, label) in ACTIVITY_LABELS.into_iter().enumerate() { + assert!( + activity[slot].load(Ordering::Relaxed) > 0, + "{label} changed no lowered Core in the runnable corpus; the sweep is vacuous" + ); + } +} + +#[test] +fn tier_configurations_have_identical_observation_traces() { + if heavy_corpus_delegated() { + return; + } + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let mut cases = sharded_corpus(); + cases.extend(FIXTURE_CASES.iter().map(|case| root.join(case))); + // A shard cannot see aggregate engagement counts from its siblings. The + // focused discovery test above retains that backstop; this sweep retains + // exact-cover semantic equivalence over the whole corpus. + run_cases(&cases, !corpus_is_sharded()); +} + +// The generated sweep: the deterministic program generator aimed at the tier +// relation. The generated fragment concentrates on handler shapes (full, +// partial, nested resumption arms) and arena regions, which is where the +// cascade's rungs actually disagree in structure, and any divergence shrinks +// greedily to a minimal reproducer before the test fails. + +const FUZZ_SEED: u64 = 0x7469_6572_5f66_757a; +const DEFAULT_FUZZ_CASES: usize = 128; +const ARENA_CASE_DIVISOR: usize = 4; +const FUZZ_CASES_ENV: &str = "PRISM_TIER_FUZZ_CASES"; + +fn fuzz_cases() -> usize { + std::env::var(FUZZ_CASES_ENV) + .ok() + .and_then(|value| value.parse::().ok()) + .filter(|&n| n > 0) + .unwrap_or(DEFAULT_FUZZ_CASES) +} + +fn family_count(programs: &[Program], family: ProgramFamily) -> usize { + programs + .iter() + .filter(|program| program.family() == family) + .count() +} + +#[test] +fn generated_programs_have_identical_observation_traces_across_tiers() { + let cases = fuzz_cases(); + let mut programs = generate(FUZZ_SEED, cases); + programs.extend(generate_arena(FUZZ_SEED, cases / ARENA_CASE_DIVISOR)); + for family in [ + ProgramFamily::Pure, + ProgramFamily::FullHandler, + ProgramFamily::PartialHandler, + ProgramFamily::Arena, + ] { + assert!( + family_count(&programs, family) > 0, + "tier fuzz seed {FUZZ_SEED:#018x} lost {family:?} coverage" + ); + } + + let roots = default_roots(Path::new(".")); + let variants = variants(); + let activity: Vec = (0..ACTIVITY_LABELS.len()) + .map(|_| AtomicUsize::new(0)) + .collect(); + let indexed: Vec<(usize, &Program)> = programs.iter().enumerate().collect(); + let divergences: Mutex> = Mutex::new(Vec::new()); + parallel_each(&indexed, |(index, program)| { + let full = prism::with_prelude(&program.render()); + if let Err(failure) = check_source( + &format!("generated case {index}"), + &full, + &roots, + &variants, + &activity, + ) { + divergences.lock().unwrap().push((*index, failure)); + } + Ok::<(), String>(()) + }); + + let total = programs.len(); + let mut divergences = divergences.into_inner().unwrap(); + divergences.sort_by_key(|(index, _)| *index); + if let Some((index, failure)) = divergences.into_iter().next() { + let failing = programs + .into_iter() + .nth(index) + .expect("failing index is within the deterministic corpus"); + let (minimal, failure) = shrink(failing, failure, |candidate| { + let full = prism::with_prelude(&candidate.render()); + check_source("shrink candidate", &full, &roots, &variants, &activity).err() + }); + panic!( + "tier divergence at seed {FUZZ_SEED:#018x}, case {index}, after shrinking:\n\ + {failure}\n\nminimal reproducer:\n{}", + minimal.render() + ); + } + + eprintln!( + "tier-fuzz: {total} generated programs, {} tiers, {} lowered-Core evaluator runs", + variants.len(), + total * variants.len() + ); + for (slot, label) in ACTIVITY_LABELS.into_iter().enumerate() { + let changed = activity[slot].load(Ordering::Relaxed); + eprintln!("tier-fuzz: {label} changed {changed} generated cases"); + } +} diff --git a/tests/tier_manifest.txt b/tests/tier_manifest.txt index b9710b04..1f5cbfd4 100644 --- a/tests/tier_manifest.txt +++ b/tests/tier_manifest.txt @@ -38,7 +38,7 @@ examples/eff_amb.pr selective-free-monad examples/eff_exn.pr selective-free-monad examples/eff_forward.pr selective-free-monad examples/eff_nontail.pr selective-free-monad -examples/eff_poly.pr whole-program-free-monad +examples/eff_poly.pr selective-free-monad examples/eff_reader.pr evidence examples/eff_rows.pr selective-free-monad examples/eff_state.pr state-fusion @@ -102,8 +102,8 @@ examples/recursion_zoo.pr pure examples/regex.pr pure examples/replay_concurrent.pr whole-program-free-monad examples/result_pipeline.pr selective-free-monad -examples/same_fringe.pr selective-free-monad -examples/scheduler.pr selective-free-monad +examples/same_fringe.pr whole-program-free-monad +examples/scheduler.pr whole-program-free-monad examples/scheduler_policy.pr whole-program-free-monad examples/show.pr pure examples/simd.pr pure @@ -148,6 +148,7 @@ tests/cases/run/borrow.pr pure tests/cases/run/buf_shared.pr pure tests/cases/run/buffer_ops.pr pure tests/cases/run/bytes_codec.pr pure +tests/cases/run/bytes_view.pr pure tests/cases/run/cancel_await.pr whole-program-free-monad tests/cases/run/cancel_completed.pr whole-program-free-monad tests/cases/run/cancel_finalizer.pr whole-program-free-monad @@ -166,7 +167,7 @@ tests/cases/run/comp_in_annotated_row.pr selective-free-monad tests/cases/run/comp_map_once.pr state-fusion tests/cases/run/constrained_mutual.pr evidence tests/cases/run/control_effects.pr evidence -tests/cases/run/control_validate.pr whole-program-free-monad +tests/cases/run/control_validate.pr selective-free-monad tests/cases/run/curry.pr pure tests/cases/run/curry_effect.pr evidence tests/cases/run/deep_effect_recursion.pr state-fusion @@ -176,7 +177,7 @@ tests/cases/run/deriving.pr pure tests/cases/run/deriving_json.pr selective-free-monad tests/cases/run/deriving_lens.pr pure tests/cases/run/deriving_phantom.pr pure -tests/cases/run/deriving_plate.pr whole-program-free-monad +tests/cases/run/deriving_plate.pr selective-free-monad tests/cases/run/dot_chains.pr pure tests/cases/run/dot_records.pr pure tests/cases/run/dot_strings.pr pure @@ -187,15 +188,20 @@ tests/cases/run/eff_fn_list.pr whole-program-free-monad tests/cases/run/eff_fuse.pr evidence tests/cases/run/eff_pending_arg.pr evidence tests/cases/run/eff_poly_fn_arg.pr whole-program-free-monad +tests/cases/run/eff_poly_handler_install.pr evidence tests/cases/run/eff_row_forward.pr evidence +tests/cases/run/eff_row_unwitnessed.pr whole-program-free-monad tests/cases/run/eff_two_handlers.pr evidence tests/cases/run/effects_demo.pr pure tests/cases/run/effop_tax.pr evidence tests/cases/run/errors.pr selective-free-monad +tests/cases/run/evidence_residual_row_after_handle.pr evidence +tests/cases/run/evidence_residual_row_clause.pr evidence tests/cases/run/factorial.pr pure tests/cases/run/fail_guard.pr selective-free-monad tests/cases/run/fib.pr pure tests/cases/run/fib_var.pr pure +tests/cases/run/field_projection_single.pr pure tests/cases/run/final_ctl.pr selective-free-monad tests/cases/run/fip.pr pure tests/cases/run/fip_inplace.pr pure @@ -216,6 +222,7 @@ tests/cases/run/graph_algorithms.pr pure tests/cases/run/guards.pr pure tests/cases/run/handler_arms_answer_apart.pr selective-free-monad tests/cases/run/handler_funval.pr evidence +tests/cases/run/handler_implicit_return.pr selective-free-monad tests/cases/run/handler_partial_forward.pr selective-free-monad tests/cases/run/higher.pr pure tests/cases/run/ho_row_inst.pr pure @@ -243,7 +250,11 @@ tests/cases/run/list_show.pr pure tests/cases/run/list_singleton.pr pure tests/cases/run/local_constrained_let.pr pure tests/cases/run/local_mono_combined.pr local-partial +tests/cases/run/local_mono_effectful_helper.pr local-partial tests/cases/run/local_mono_escape.pr whole-program-free-monad +tests/cases/run/local_mono_nontail_resume.pr local-partial +tests/cases/run/local_mono_state_rest.pr local-partial +tests/cases/run/local_mono_two_entries.pr local-partial tests/cases/run/loop_break.pr pure tests/cases/run/loop_control.pr pure tests/cases/run/looptest.pr pure @@ -295,12 +306,14 @@ tests/cases/run/primes.pr pure tests/cases/run/print_structural.pr pure tests/cases/run/quickcheck_detect.pr whole-program-free-monad tests/cases/run/ranges.pr pure +tests/cases/run/record_pattern_rest.pr pure tests/cases/run/reflect.pr pure tests/cases/run/reified_under_world.pr whole-program-free-monad tests/cases/run/repeat_block.pr pure tests/cases/run/replayable_ok.pr evidence tests/cases/run/rewrite_strategies.pr whole-program-free-monad tests/cases/run/rollback.pr selective-free-monad +tests/cases/run/row_widen_named_effect_list.pr whole-program-free-monad tests/cases/run/row_widen_task_list.pr whole-program-free-monad tests/cases/run/row_widen_user_effect.pr whole-program-free-monad tests/cases/run/scc_effect_row.pr evidence @@ -308,6 +321,7 @@ tests/cases/run/scientific.pr pure tests/cases/run/sequence_pull.pr pure tests/cases/run/set_ops.pr pure tests/cases/run/shadow_prelude.pr pure +tests/cases/run/shadowed_field_binder.pr pure tests/cases/run/show.pr pure tests/cases/run/show_mangle_clash.pr pure tests/cases/run/simd4_edge.pr pure @@ -315,6 +329,7 @@ tests/cases/run/simd_edge.pr pure tests/cases/run/sort_demo.pr pure tests/cases/run/stable_ladder.pr whole-program-free-monad tests/cases/run/stable_migrations.pr selective-free-monad +tests/cases/run/str_view.pr pure tests/cases/run/stream_take.pr state-fusion tests/cases/run/streams_edge.pr state-fusion tests/cases/run/string_edges.pr pure diff --git a/tests/tooling.rs b/tests/tooling.rs index 9f07f9da..268008da 100644 --- a/tests/tooling.rs +++ b/tests/tooling.rs @@ -6,12 +6,16 @@ mod support; #[path = "tooling/bootstrap.rs"] mod bootstrap; +#[path = "tooling/bootstrap_worker.rs"] +mod bootstrap_worker; #[path = "tooling/durable_driver.rs"] mod durable_driver; #[path = "tooling/index.rs"] mod index; #[path = "tooling/isa_fixture.rs"] mod isa_fixture; +#[path = "tooling/lane_ledger.rs"] +mod lane_ledger; #[path = "tooling/prism_test.rs"] mod prism_test; #[path = "tooling/stable_lock.rs"] @@ -25,6 +29,8 @@ mod certificates; mod pkg; #[path = "package/pkg_transport.rs"] mod pkg_transport; +#[path = "package/receipt.rs"] +mod receipt; #[path = "store_pkg/store_coherence.rs"] mod store_coherence; @@ -42,6 +48,8 @@ mod run_lineage; #[path = "lineage_suite/world_lineage.rs"] mod world_lineage; +#[path = "compiler/parser_receipt.rs"] +mod parser_receipt; #[path = "compiler/stdlib_hash.rs"] mod stdlib_hash; diff --git a/tests/tooling/bootstrap.rs b/tests/tooling/bootstrap.rs index 8253450d..486701c9 100644 --- a/tests/tooling/bootstrap.rs +++ b/tests/tooling/bootstrap.rs @@ -1,37 +1,63 @@ +use std::fs; use std::path::Path; use std::process::Command; use serde_json::Value; -/// The pure first-order fixture and the coverage it is pinned at: one -/// declaration out of reach, over a list. +/// The pure first-order fixture and its pinned fail-closed boundary: a list, +/// three rank-n paths, and a higher-kinded application stay uncovered. const PURE_FIXTURE: &str = "tests/fixtures/bootstrap/t1.pr"; -const PURE_SUPPORTED: u64 = 45; -const PURE_TOTAL: u64 = 48; -const PURE_UNCOVERED: &[(&str, &str)] = &[("later", "list")]; +const PURE_SUPPORTED: u64 = 81; +const PURE_TOTAL: u64 = 94; +const PURE_UNCOVERED: &[(&str, &str)] = &[ + ("later", "list"), + ("nested", "nested-forall"), + ("higher_kinded", "higher-kinded-application"), + ("annotated_nested", "nested-forall"), + ("rankn_field", "nested-forall"), +]; -/// The effect-row fixture and its pinned coverage: one declaration out of -/// reach, over an effect applied to a type argument. +/// The effect-row fixture and its pinned coverage: operation schemes or label +/// arguments the shadow cannot represent remain explicit, named refusals. const ROW_FIXTURE: &str = "tests/fixtures/bootstrap/t2.pr"; -const ROW_SUPPORTED: u64 = 64; -const ROW_TOTAL: u64 = 66; -const ROW_UNCOVERED: &[(&str, &str)] = &[("later", "effect-row-applied")]; +const ROW_SUPPORTED: u64 = 133; +const ROW_TOTAL: u64 = 163; +const ROW_UNCOVERED: &[(&str, &str)] = &[ + ("nested_open", "effect-row-open"), + ("annotated_open", "effect-row-open"), + ("make_runner", "effect-row-open"), + ("strays", "external-operation"), + ("run_pure", "external-operation"), + ("make_task", "row-kinded-data-argument"), + ("task_id", "row-kinded-data-argument"), + ("launch_tick", "external-operation"), + ("unboxed_label", "unboxed-type"), + ("usage_label", "usage-qualified-type"), +]; -/// The handler fixture and its pinned coverage: two declarations out of reach, -/// a partial handler and a named handler instance, both of which hide which -/// effect a clause discharges. +const BARE_ROW_LABEL_FIXTURE: &str = "tests/fixtures/bootstrap/t2_bare_row_label.pr"; + +/// The handler fixture and its pinned coverage. Exhaustive anonymous and named +/// handlers are checked, and partial handlers are checked through operation-use +/// evidence: a walk over the handled body records which operations it is known +/// to perform, and a partial handler discharges an effect exactly when every +/// known use is covered by a clause. const HANDLER_FIXTURE: &str = "tests/fixtures/bootstrap/t3.pr"; -const HANDLER_SUPPORTED: u64 = 131; -const HANDLER_TOTAL: u64 = 145; -const HANDLER_UNCOVERED: &[(&str, &str)] = &[ - ("partial_cover", "handle-partial"), - ("named_instance", "handle-named"), -]; +const HANDLER_SUPPORTED: u64 = 299; +const HANDLER_TOTAL: u64 = 299; +const HANDLER_UNCOVERED: &[(&str, &str)] = &[]; + +/// A byte copy of the pure fixture, in a directory that also defines the two +/// modules the shadow itself imports. +const HOSTILE_FIXTURE: &str = "tests/fixtures/bootstrap/hostile/t1.pr"; +/// The report field that legitimately differs between the two copies. +const SOURCE_FIELD: &str = "source"; fn check(fixture: &str) -> Value { let root = Path::new(env!("CARGO_MANIFEST_DIR")); let output = Command::new(env!("CARGO_BIN_EXE_prism")) .current_dir(root) + .env_remove("PRISM_TOOL_PACKAGES_ROOT") .args(["bootstrap", "check", fixture, "--json"]) .output() .expect("run bootstrap check"); @@ -57,7 +83,11 @@ fn assert_parity(report: &Value, supported: u64, total: u64, uncovered: &[(&str, Some(supported) ); assert_eq!(report["coverage"]["total_nodes"].as_u64(), Some(total)); - assert!(supported < total); + if uncovered.is_empty() { + assert_eq!(supported, total); + } else { + assert!(supported < total); + } let rows = report["unsupported"].as_array().expect("unsupported"); assert_eq!(rows.len(), uncovered.len()); for (row, (function, kind)) in rows.iter().zip(uncovered) { @@ -77,12 +107,37 @@ fn assert_parity(report: &Value, supported: u64, total: u64, uncovered: &[(&str, #[test] fn bootstrap_check_reports_parity_and_coverage() { - assert_parity( - &check(PURE_FIXTURE), - PURE_SUPPORTED, - PURE_TOTAL, - PURE_UNCOVERED, + let report = check(PURE_FIXTURE); + assert_parity(&report, PURE_SUPPORTED, PURE_TOTAL, PURE_UNCOVERED); + // A written type variable is one of the declaration's quantifiers, so the + // canonical spelling places it by position rather than by the name it was + // written with. + assert_eq!(scheme(&report, "ident"), "forall $0. ($0) -> $0"); + assert_eq!( + scheme(&report, "swap"), + "forall $0 $1. (($0, $1)) -> ($1, $0)" + ); + assert_eq!( + scheme(&report, "use_fn"), + "forall $0 $1. (($0) -> $1, $0) -> $1" + ); + // The order of those quantifiers is itself part of the spelling, and this + // is the case that can tell the two orders apart: `y` is inferred and `a` + // is written, so the inferred binder is first and the written one second. + assert_eq!( + scheme(&report, "mixed_pair"), + "forall $0 $1. ($1, $0) -> ($1, $0)" + ); + // `z` is encountered before `a`, despite sorting after it. + assert_eq!( + scheme(&report, "written_order"), + "forall $0 $1. ($0, $1) -> ($0, $1)" ); + // The alias cannot escape the parameter's monomorphic class, while an + // independently owned local identity remains polymorphic. + assert_eq!(scheme(&report, "leaked"), "(Bool) -> Int"); + assert_eq!(scheme(&report, "kept_poly"), "(Int) -> (Int, Bool)"); + assert_eq!(scheme(&report, "duplicate"), "forall $0. ($0) -> ($0, $0)"); } /// Effect rows are checked, not skipped: the shadow infers what a declaration @@ -98,6 +153,94 @@ fn bootstrap_check_agrees_on_effect_rows() { assert_eq!(scheme(&report, "inferred"), "(Int) -> Int ! {Tick}"); assert_eq!(scheme(&report, "still_pure"), "(Int) -> Int"); assert_eq!(scheme(&report, "both"), "(Int) -> Unit ! {Say, Tick}"); + // An effect applied to a type argument, which is where a row stops being a + // set of names: the argument is written here, inferred and quantified in + // `borrowed`, and positional in `use_pair`, where `Pair(Int, Bool)` is what + // makes `left` the one that answers with an `Int`. + assert_eq!(scheme(&report, "later"), "() -> Int ! {Cell(Int)}"); + assert_eq!( + scheme(&report, "callback_cell"), + "forall $0. (() -> Unit ! {$0}) -> Unit ! {Cell(() -> Unit ! {$0})}" + ); + assert_eq!( + scheme(&report, "borrowed"), + "forall $0. () -> $0 ! {Cell($0)}" + ); + assert_eq!( + scheme(&report, "marked"), + "forall $0. () -> Unit ! {Mark($0)}" + ); + assert_eq!(scheme(&report, "unmarked"), "() -> Unit"); + assert_eq!( + scheme(&report, "nat_label"), + "() -> Unit ! {Mark(Sized(3))}" + ); + assert_eq!( + scheme(&report, "use_pair"), + "() -> (Bool, Int) ! {Pair(Int, Bool)}" + ); + assert_eq!( + scheme(&report, "borrowed_pair"), + "forall $0 $1. () -> ($0, $1) ! {Pair($1, $0)}" + ); + assert_eq!( + scheme(&report, "ticked_cell"), + "() -> Int ! {Cell(Int), Tick}" + ); + assert_eq!(scheme(&report, "holds"), "() -> Unit ! {Loose(Int)}"); + // Written row variables are branded after inference. One spelling shares + // within a declaration, different spellings and declarations stay fresh, + // and widening can add body effects before that brand becomes rigid. + assert_eq!( + scheme(&report, "relay"), + "forall $0 $1 $2. (($1) -> $2 ! {Tick, $0}, $1) -> $2 ! {Tick, $0}" + ); + assert_eq!( + scheme(&report, "relay2"), + "forall $0 $1 $2. ($1, ($1) -> $2 ! {$0}) -> $2 ! {$0}" + ); + assert_eq!( + scheme(&report, "two_rows"), + "forall $0 $1. ((Int) -> Int ! {$0}, (Bool) -> Bool ! {$1}) -> \ + ((Int) -> Int ! {$0}, (Bool) -> Bool ! {$1})" + ); + let same_row = "forall $0. ((Int) -> Int ! {$0}, Int) -> Int ! {$0}"; + assert_eq!(scheme(&report, "same_row_one"), same_row); + assert_eq!(scheme(&report, "same_row_two"), same_row); + assert_eq!( + scheme(&report, "widened"), + "forall $0. ((Int) -> Int ! {Say, Tick, $0}, Int) -> Int ! {Say, Tick, $0}" + ); + // The unused callback tail is hidden only from display. Passing an + // effectful callback still instantiates the structural row binder. + assert_eq!(scheme(&report, "no_call"), "(() -> Unit) -> Unit ! {Tick}"); + assert_eq!(scheme(&report, "no_call_effectful"), "() -> Unit ! {Tick}"); + assert_eq!( + scheme(&report, "shifted"), + "forall $0 $1 $2. (() -> Unit, ($1) -> $2 ! {$0}, $1) -> $2 ! {$0}" + ); +} + +/// A lowercase row name in label position is not an implicit open tail. The +/// compiler must reject it before the bootstrap shadow is asked for evidence. +#[test] +fn bootstrap_open_row_requires_tail_syntax() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let output = Command::new(env!("CARGO_BIN_EXE_prism")) + .current_dir(root) + .env_remove("PRISM_TOOL_PACKAGES_ROOT") + .args(["check", BARE_ROW_LABEL_FIXTURE]) + .output() + .expect("check bare row label fixture"); + assert!( + !output.status.success(), + "bare row label unexpectedly checked" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("[E5001]") && stderr.contains("unknown effect `e`"), + "unexpected diagnostic for bare row label:\n{stderr}" + ); } /// Handlers are checked, not skipped: installing one subtracts the effects its @@ -113,6 +256,64 @@ fn bootstrap_check_agrees_on_handlers() { assert_eq!(scheme(&report, "nested"), "() -> Int"); assert_eq!(scheme(&report, "leftover"), "() -> Unit ! {Say}"); assert_eq!(scheme(&report, "clause_performs"), "() -> Int ! {Say}"); + assert_eq!(scheme(&report, "named_instance"), "() -> Int"); + assert_eq!(scheme(&report, "applied_handler"), "forall $0. ($0) -> $0"); + assert_eq!( + scheme(&report, "function_valued_handler"), + "(() -> Int ! {Cell(Int)}) -> Int" + ); + assert_eq!(scheme(&report, "nested_applied_scope"), "() -> String"); + assert_eq!(scheme(&report, "named_consistent"), "() -> Int"); + assert_eq!( + scheme(&report, "named_ambient"), + "() -> (Int, String) ! {Cell(String)}" + ); + // Partial discharge turns on operation-use evidence, so pin all three + // sides of the rule: a use its clauses do not cover retains the effect, a + // covered use discharges it, and a use hidden behind a declared row is + // opaque and retains it. + assert_eq!(scheme(&report, "partial_cover"), "() -> Int ! {Store}"); + assert_eq!(scheme(&report, "partial_covered"), "() -> Int"); + assert_eq!(scheme(&report, "partial_opaque"), "() -> Int ! {Store}"); + assert_eq!(scheme(&report, "named_operation_shadow"), "() -> Int"); + assert_eq!( + scheme(&report, "anonymous_outer_operation_shadow"), + "() -> Unit" + ); + assert_eq!(scheme(&report, "named_outer_operation_shadow"), "() -> Int"); + assert_eq!(scheme(&report, "named_explicit_instance"), "() -> Int"); +} + +/// The compiler, not the checked project, chooses the shadow that judges it. +/// +/// Module resolution is first-hit and a source directory root precedes the +/// embedded standard library, so a target that defines `Tc` or `Syntax.Codec` +/// would supply the shadow's own checker and decoder if the shadow resolved +/// against the target's search path. The same program checked with those +/// modules present and absent must produce the same report. +#[test] +fn bootstrap_shadow_ignores_target_modules_named_like_its_own() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + assert_eq!( + fs::read(root.join(PURE_FIXTURE)).expect("pure fixture"), + fs::read(root.join(HOSTILE_FIXTURE)).expect("hostile fixture"), + "the hostile copy must stay a byte copy of {PURE_FIXTURE}, or the two \ + reports differ for an uninteresting reason" + ); + + let mut hostile = check(HOSTILE_FIXTURE); + assert_parity(&hostile, PURE_SUPPORTED, PURE_TOTAL, PURE_UNCOVERED); + + // Everything but the file it was run on is identical, so no field of the + // verdict can drift with a module the target happened to define. + let mut pure = check(PURE_FIXTURE); + for report in [&mut pure, &mut hostile] { + report + .as_object_mut() + .expect("report object") + .remove(SOURCE_FIELD); + } + assert_eq!(pure, hostile); } /// The authority's spelling for one declaration of a report. diff --git a/tests/tooling/bootstrap_worker.rs b/tests/tooling/bootstrap_worker.rs new file mode 100644 index 00000000..1de9eda0 --- /dev/null +++ b/tests/tooling/bootstrap_worker.rs @@ -0,0 +1,80 @@ +use std::fs; +use std::path::Path; +use std::process::{self, Command}; + +use serde_json::Value; + +const PURE_FIXTURE: &str = "tests/fixtures/bootstrap/t1.pr"; +const ROW_FIXTURE: &str = "tests/fixtures/bootstrap/t2.pr"; + +#[test] +fn bootstrap_batch_prepares_once_and_evaluates_each_target_fresh() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let output = Command::new(env!("CARGO_BIN_EXE_prism")) + .current_dir(root) + .env("PRISM_TIME_COMPILE", "1") + .env_remove("PRISM_TOOL_PACKAGES_ROOT") + .args(["bootstrap", "check", PURE_FIXTURE, ROW_FIXTURE, "--json"]) + .output() + .expect("run bootstrap batch"); + assert!( + output.status.success(), + "bootstrap batch failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let reports: Value = serde_json::from_slice(&output.stdout).expect("bootstrap batch JSON"); + let reports = reports.as_array().expect("batch report array"); + assert_eq!(reports.len(), 2); + assert!(reports.iter().all(|report| report["status"] == "parity")); + + let stderr = String::from_utf8_lossy(&output.stderr); + assert_eq!( + stderr + .matches("bootstrap-time\tchecker_front_prepare\t") + .count(), + 1 + ); + assert_eq!( + stderr.matches("bootstrap-time\ttarget_artifacts\t").count(), + 2 + ); + assert_eq!(stderr.matches("bootstrap-time\tshadow_eval\t").count(), 2); +} + +#[test] +fn target_modules_cannot_supply_the_checker_or_its_codec() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let hostile = + std::env::temp_dir().join(format!("prism_bootstrap_hostile_modules_{}", process::id())); + if hostile.exists() { + fs::remove_dir_all(&hostile).expect("clear stale hostile fixture"); + } + fs::create_dir_all(hostile.join("Syntax")).expect("create hostile fixture"); + fs::copy(root.join(PURE_FIXTURE), hostile.join("main.pr")).expect("copy target program"); + fs::write(hostile.join("Tc.pr"), "not valid Prism source\n").expect("write hostile Tc"); + fs::write( + hostile.join("Syntax").join("Codec.pr"), + "not valid Prism source\n", + ) + .expect("write hostile Syntax.Codec"); + + let output = Command::new(env!("CARGO_BIN_EXE_prism")) + .current_dir(root) + .env("PRISM_TOOL_PACKAGES_ROOT", root.join("packages")) + .arg("bootstrap") + .arg("check") + .arg(hostile.join("main.pr")) + .arg("--json") + .output() + .expect("run checker with hostile target modules"); + fs::remove_dir_all(&hostile).expect("remove hostile fixture"); + + assert!( + output.status.success(), + "target module shadowed compiler-owned code: {}", + String::from_utf8_lossy(&output.stderr) + ); + let report: Value = serde_json::from_slice(&output.stdout).expect("bootstrap JSON"); + assert_eq!(report["status"], "parity"); +} diff --git a/tests/tooling/index.rs b/tests/tooling/index.rs index c1a768a4..775010a3 100644 --- a/tests/tooling/index.rs +++ b/tests/tooling/index.rs @@ -161,10 +161,7 @@ fn imported_module_definitions_are_addressed_by_visibility() { assert!(targets(&index, EdgeKind::Calls, "Parser.parse").contains(&"Parser@normalize")); } -// The set a build compiles is not the set a reader reads. A module outside the -// entry's import closure — a library package's whole surface — must still be -// addressed, or the index would be empty of exactly the code someone opened it to -// review. +// Modules outside the entry's import closure must still receive addresses. #[test] fn a_module_the_entry_never_imports_is_still_addressed() { let dir = TempDir::project("library"); @@ -178,7 +175,7 @@ fn a_module_the_entry_never_imports_is_still_addressed() { ); } assert_eq!(def(&index, "Library.Doc").kind, Kind::Type); - // And its relationships resolve, not just its address. + // Its relationships resolve as well. assert!(targets(&index, EdgeKind::UsesType, "Library.render").contains(&"Library.Doc")); // Nothing reaches it, so it has no callers; that is a fact about the code, and // the reason the reader wanted to see the module in the first place. diff --git a/tests/tooling/lane_ledger.rs b/tests/tooling/lane_ledger.rs new file mode 100644 index 00000000..9d7e774c --- /dev/null +++ b/tests/tooling/lane_ledger.rs @@ -0,0 +1,303 @@ +//! The gauntlet's cost ledger, joined against the workflows it describes. +//! +//! A ledger that is read but never joined records what the suite looked like on +//! the day it was written. Both sides here are live: the job set comes out of +//! the workflow files at run time and the arm is derived from each workflow's +//! own triggers, so a job added, renamed, retired, or moved between arms fails +//! the join instead of leaving a row that still reads plausibly. The budget half +//! is checked against the numbers the ledger itself carries, which means the +//! file cannot record a figure over the cap and call it within one. +//! +//! What this cannot check is that a recorded timing is still true; that half +//! carries its provenance in the ledger's header and is refreshed by hand. + +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::path::Path; + +/// The ledger, relative to the repository root. +const LEDGER: &str = "tests/lane_ledger.txt"; +/// Every workflow the forge runs; the job set is read from all of them, so a new +/// workflow is covered the moment it exists. +const WORKFLOW_DIR: &str = ".github/workflows"; +const WORKFLOW_EXT: &str = "yml"; +/// Wall clock a single job cell may take on the per-change arm. A change's +/// latency is its slowest cell rather than the sum, so the cap is per cell. +const BUDGET_SECONDS: u64 = 2700; +/// The workflow column of a lane that is declared but not yet built. +const PLANNED_WORKFLOW: &str = "-"; +const FIELDS: usize = 6; + +/// When a job runs, derived from its workflow's triggers rather than declared. +const ARM_PER_CHANGE: &str = "per-change"; +const ARM_PATH_GATED: &str = "path-gated"; +const ARM_POST_MERGE: &str = "post-merge"; +const ARM_NIGHTLY: &str = "nightly"; +const ARM_RELEASE: &str = "release"; +const ARMS: &[&str] = &[ + ARM_PER_CHANGE, + ARM_PATH_GATED, + ARM_POST_MERGE, + ARM_NIGHTLY, + ARM_RELEASE, +]; + +/// How a row stands against the budget. +const VERDICT_WITHIN: &str = "within"; +const VERDICT_OVER: &str = "over-budget"; +const VERDICT_UNBUDGETED: &str = "unbudgeted"; +const VERDICT_PLANNED: &str = "planned"; + +/// Triggers, as they are spelled in a workflow's `on:` block. +const ON_KEY: &str = "on"; +const JOBS_KEY: &str = "jobs"; +const TRIGGER_PULL_REQUEST: &str = "pull_request"; +const TRIGGER_PUSH: &str = "push"; +const TRIGGER_SCHEDULE: &str = "schedule"; +const TRIGGER_RELEASE: &str = "release"; +const FILTER_PATHS: &str = "paths"; +const FILTER_TAGS: &str = "tags"; +/// A mapping key sits two spaces in under its top-level parent. +const NESTED_INDENT: usize = 2; + +struct Row { + workflow: String, + job: String, + arm: String, + cells: u64, + seconds: u64, + verdict: String, +} + +fn repo_root() -> &'static Path { + Path::new(env!("CARGO_MANIFEST_DIR")) +} + +fn rows() -> Vec { + let path = repo_root().join(LEDGER); + let text = + fs::read_to_string(&path).unwrap_or_else(|e| panic!("reading {}: {e}", path.display())); + text.lines() + .filter(|line| !line.trim().is_empty() && !line.starts_with('#')) + .map(|line| { + let field: Vec<&str> = line.split('\t').collect(); + assert_eq!(field.len(), FIELDS, "{LEDGER}: malformed row `{line}`"); + Row { + workflow: field[0].to_string(), + job: field[1].to_string(), + arm: field[2].to_string(), + cells: field[3].parse().expect("cell count"), + seconds: field[4].parse().expect("slowest cell seconds"), + verdict: field[5].to_string(), + } + }) + .collect() +} + +/// The lines of the top-level block introduced by `key`, comments dropped. +fn block<'a>(text: &'a str, key: &str) -> Vec<&'a str> { + let opener = format!("{key}:"); + text.lines() + .skip_while(|line| *line != opener) + .skip(1) + .take_while(|line| line.starts_with(' ') || line.trim().is_empty()) + .filter(|line| !line.trim().is_empty() && !line.trim_start().starts_with('#')) + .collect() +} + +/// The name a mapping key introduces at `indent`, if this line is one. A key +/// carrying its value inline (`tags: ['v*']`) names the same thing as one +/// opening a block, so both forms count. +fn key_at(line: &str, indent: usize) -> Option<&str> { + let depth = line.len() - line.trim_start().len(); + if depth != indent { + return None; + } + let (name, _) = line.trim().split_once(':')?; + let named = !name.is_empty() + && name + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'); + named.then_some(name) +} + +fn jobs(text: &str) -> BTreeSet { + block(text, JOBS_KEY) + .into_iter() + .filter_map(|line| key_at(line, NESTED_INDENT)) + .map(str::to_string) + .collect() +} + +/// The arm a workflow's triggers put its jobs on. A pull-request trigger gates a +/// change and outranks the rest; a path filter under it narrows the gate to the +/// changes that touch those paths rather than removing it. +fn arm(text: &str) -> &'static str { + let mut trigger = ""; + let mut filters: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new(); + for line in block(text, ON_KEY) { + if let Some(name) = key_at(line, NESTED_INDENT) { + trigger = name; + filters.entry(name).or_default(); + } else if let Some(name) = key_at(line, NESTED_INDENT * 2) { + filters.entry(trigger).or_default().insert(name); + } + } + let filtered = |name: &str, filter: &str| { + filters + .get(name) + .is_some_and(|under| under.contains(filter)) + }; + if filters.contains_key(TRIGGER_PULL_REQUEST) { + if filtered(TRIGGER_PULL_REQUEST, FILTER_PATHS) { + return ARM_PATH_GATED; + } + return ARM_PER_CHANGE; + } + if filters.contains_key(TRIGGER_SCHEDULE) { + return ARM_NIGHTLY; + } + if filters.contains_key(TRIGGER_RELEASE) || filtered(TRIGGER_PUSH, FILTER_TAGS) { + return ARM_RELEASE; + } + assert!( + filters.contains_key(TRIGGER_PUSH), + "a workflow with none of the known triggers has no arm: {filters:?}" + ); + ARM_POST_MERGE +} + +/// Every workflow file, by name, with its text. +fn workflows() -> BTreeMap { + let dir = repo_root().join(WORKFLOW_DIR); + let mut out = BTreeMap::new(); + for entry in fs::read_dir(&dir).unwrap_or_else(|e| panic!("reading {}: {e}", dir.display())) { + let path = entry.expect("workflow entry").path(); + if path.extension().is_some_and(|ext| ext == WORKFLOW_EXT) { + let name = path + .file_name() + .expect("workflow file name") + .to_string_lossy() + .into_owned(); + out.insert(name, fs::read_to_string(&path).expect("workflow text")); + } + } + assert!(!out.is_empty(), "no workflows under {}", dir.display()); + out +} + +fn live_jobs() -> BTreeSet<(String, String)> { + workflows() + .into_iter() + .flat_map(|(name, text)| { + jobs(&text) + .into_iter() + .map(move |job| (name.clone(), job)) + .collect::>() + }) + .collect() +} + +// A job with no row runs at a cost nobody declared, and a row with no job +// describes a lane that no longer exists. Both are the same failure seen from +// opposite ends, so the join asserts set equality rather than containment. +#[test] +fn every_job_declares_exactly_one_row() { + let mut declared: BTreeSet<(String, String)> = BTreeSet::new(); + for row in rows().iter().filter(|r| r.verdict != VERDICT_PLANNED) { + let key = (row.workflow.clone(), row.job.clone()); + assert!( + declared.insert(key), + "{LEDGER}: duplicate row for {row_job} in {workflow}", + row_job = row.job, + workflow = row.workflow + ); + } + assert_eq!( + declared, + live_jobs(), + "the cost ledger and {WORKFLOW_DIR} disagree about which jobs exist: update {LEDGER}" + ); +} + +// The arm is a fact about the workflow's triggers, so the ledger states it and +// the workflow decides it. A job promoted from post-merge to per-change without +// its row moving would otherwise keep a budget exemption it no longer has. +#[test] +fn declared_arms_match_the_triggers() { + let live = workflows(); + for row in rows().iter().filter(|r| r.verdict != VERDICT_PLANNED) { + let text = live + .get(&row.workflow) + .unwrap_or_else(|| panic!("{LEDGER}: no workflow {}", row.workflow)); + assert_eq!( + row.arm, + arm(text), + "{}: {} declares the wrong arm", + row.workflow, + row.job + ); + } +} + +// The verdict is a function of the budget and the row's own number, so a lane +// crossing the cap flips a word in a reviewed diff instead of passing quietly. +#[test] +fn verdicts_follow_the_budget() { + for row in rows().iter().filter(|r| r.verdict != VERDICT_PLANNED) { + assert!( + row.cells >= 1 && row.seconds >= 1, + "{}: {} records no observation", + row.workflow, + row.job + ); + let expected = if row.arm != ARM_PER_CHANGE { + VERDICT_UNBUDGETED + } else if row.seconds <= BUDGET_SECONDS { + VERDICT_WITHIN + } else { + VERDICT_OVER + }; + assert_eq!( + row.verdict, expected, + "{}: {} at {}s on the {} arm", + row.workflow, row.job, row.seconds, row.arm + ); + } +} + +// A planned lane is a declaration, not a description: the moment it runs +// anywhere it owes a workflow, an arm the triggers agree with, and a number. +#[test] +fn planned_lanes_name_no_live_job() { + let live: BTreeSet = live_jobs().into_iter().map(|(_, job)| job).collect(); + for row in rows().iter().filter(|r| r.verdict == VERDICT_PLANNED) { + assert_eq!( + row.workflow, PLANNED_WORKFLOW, + "{}: planned lanes name no workflow", + row.job + ); + assert!( + !live.contains(&row.job), + "{}: runs now, so it owes a measured row", + row.job + ); + assert!( + row.cells == 0 && row.seconds == 0, + "{}: planned lanes carry no observation", + row.job + ); + } +} + +#[test] +fn every_row_names_a_known_arm() { + for row in rows() { + assert!( + ARMS.contains(&row.arm.as_str()), + "{}: unknown arm {}", + row.job, + row.arm + ); + } +} diff --git a/tests/tooling/prism_test.rs b/tests/tooling/prism_test.rs index 16657e4d..d1d66613 100644 --- a/tests/tooling/prism_test.rs +++ b/tests/tooling/prism_test.rs @@ -463,8 +463,8 @@ fn polymorphic_test_is_rejected() { ); } -// A test-only edit leaves the emitted (effect-lowered) artifact byte-identical, -// not merely the semantic hash. This is the emitted-artifact half of production +// A test-only edit leaves the emitted, effect-lowered artifact byte-identical. +// This is the emitted-artifact half of production // neutrality, complementing the core-hash and interface checks above. #[test] fn test_only_edit_leaves_emitted_artifact_identical() { diff --git a/web/src/viewer-context.ts b/web/src/viewer-context.ts index ca600f27..91199814 100644 --- a/web/src/viewer-context.ts +++ b/web/src/viewer-context.ts @@ -1,33 +1,10 @@ -// Assembling a context packet for a set of definitions. -// -// The selection is a set of *definitions*, not a range of lines, and that is what -// makes the packet worth assembling. Handing an assistant "the file" gives it -// whatever else happens to live in that file and none of what the definition -// actually depends on. Handing it a definition's canonical name, inferred type, -// effect row, exact source, transitive dependencies, callers, and the tests that -// reach it gives it the same picture a reviewer builds by clicking around for ten -// minutes — and every part of it is a fact the compiler computed, not a guess -// from proximity. -// -// This builds the packet and stops there. Sending it somewhere is a request the -// reader makes with their own key, in their own tool; the part that needed to -// know how the codebase fits together is done here. -// -// Nothing in the viewer calls this yet. It had a UI — a `+` on every card that -// gathered definitions and a tray that copied the packet to the clipboard — and -// that UI was removed because copying a prompt is not asking a question: the -// reader wanted to ask in the browser and get an answer back. The assembly is the -// half that needed the compiler's facts, so it stays, checked, waiting for the -// half that needs a key and a network call. +// Build a Markdown context packet from compiler-index facts. Transport and UI are +// separate concerns, and no current viewer path calls this module. import type { Def, Index, Relations } from "./viewer-model.js"; import type { Review } from "./viewer-review.js"; -/// How much dependency source to include before saying so and stopping. -/// -/// A budget is not politeness, it is honesty: the alternative to truncating is -/// either an unbounded packet or a silently clipped one, and a reader who cannot -/// see that the closure was cut cannot tell whether the answer was informed. +/// Maximum dependency source included in one packet, in bytes. const DEPENDENCY_BUDGET = 24_000; export interface PacketInput { @@ -54,8 +31,7 @@ export function packet({ index, rel, review, selected }: PacketInput): string { out.push(`\n## Selected (${defs.length})`); for (const d of defs) out.push(describe(d, rel, index, review)); - // What the selection depends on, so the reader is not asked about a call it - // cannot see the body of. + // Include the selected definitions' dependency closure. const closure = dependencies(defs, rel, index); if (closure.included.length > 0) { out.push(`\n## Reached by the above (${closure.included.length})`); @@ -69,8 +45,7 @@ export function packet({ index, rel, review, selected }: PacketInput): string { ); } - // More than one definition selected means the question is probably about how - // they relate, so say how they relate. + // Relate multiple selected definitions. if (defs.length > 1) { const between = relationsAmong(defs, rel); out.push(`\n## Between the selected definitions`); @@ -112,8 +87,7 @@ function describe(d: Def, rel: Relations, index: Index, review: Review | null): : "No test in this index reaches this definition.", ); - // Behavioral duplicates come free from the addressing, and are worth stating: - // they mean an answer about one applies verbatim to the others. + // Equal content addresses identify behavioral duplicates. const twins = index.defs.filter((o) => o.hash && o.hash === d.hash && o.id !== d.id); if (twins.length > 0) { lines.push(`Identical behavior to ${named(twins.map((t) => t.id))} (same content address).`); @@ -152,13 +126,7 @@ function dependencies( return { included, omitted }; } -// How the selected definitions relate to each other, which is usually the actual -// question when more than one is selected. -// -// Reported as *paths*, not just direct edges. Two definitions a reader picks -// together are often several hops apart — that is frequently why they were picked -// together — and "no direct relation" would be a true statement that hides the -// answer. The route through the intermediate definitions is the relationship. +// Report complete call paths between selected definitions. function relationsAmong(defs: Def[], rel: Relations): string[] { const lines: string[] = []; for (const from of defs) { @@ -183,8 +151,7 @@ function path(from: string, to: string, rel: Relations): string[] | null { const back = new Map(); const seen = new Set([from]); let frontier = [from]; - // Bounded: a route long enough to need this many hops is not an explanation - // anyone reading a packet would use. + // Bound route length to keep the packet useful. for (let depth = 0; depth < 8 && frontier.length > 0; depth++) { const next: string[] = []; for (const at of frontier) { diff --git a/web/src/viewer-model.ts b/web/src/viewer-model.ts index 9866cc93..fc2947ec 100644 --- a/web/src/viewer-model.ts +++ b/web/src/viewer-model.ts @@ -91,8 +91,8 @@ export interface TokenSpan { /// Decode a definition's packed highlight spans. /// -/// The artifact stores `gap length class` triples — gap from the previous span's -/// end, class as an index into the shared table — because a pretty-printed JSON +/// The artifact stores `gap length class` triples: the gap from the previous span's +/// end and the class index into the shared table. A pretty-printed JSON /// array would spend more bytes on indentation than on the data. Unstyled spans /// are absent, so a gap is not always zero. export function decodeSpans(packed: string | undefined, classes: string[]): TokenSpan[] { @@ -158,7 +158,7 @@ export class Index { readonly byId: Map; readonly edges: { kind: EdgeKind; from: string; to: string }[]; /// The compiler's own primitives, by name. One of these has no definition - /// anywhere, so it is not a link — but it is also not missing, and saying which + /// anywhere, so it is not a link. It is also not missing, and saying which /// of the two it is is the difference between "primitive" and "this index is /// incomplete". Its signature is what makes it readable rather than merely named. readonly builtins: Map; @@ -216,7 +216,7 @@ export class Index { } const prim = this.primitive(target); if (prim) { - return [prim.name, prim.signature, prim.doc, "compiler builtin — no Prism definition"] + return [prim.name, prim.signature, prim.doc, "compiler builtin: no Prism definition"] .filter((l): l is string => Boolean(l)) .join("\n"); } @@ -250,7 +250,7 @@ export class Relations { /// The dependency graph and the text answer different questions, and a reviewer /// asks the text's. Elaboration inlines a top-level `let`, so a body that writes /// `gen_float` depends on whatever the constant expanded to and not on the -/// constant — across the standard library's 73 consts exactly one is ever a +/// constant. Across the standard library's 73 consts, exactly one is ever a /// dependency. Reading the occurrence set instead gives back what is on the page. /// /// Terms only. A written type, constructor or class method resolves to the @@ -285,8 +285,8 @@ export class Mentions { } } -/// A member of a declaration — a class method, an effect operation, a data -/// constructor — as some other definition wrote it. +/// A class method, effect operation, or data constructor as another definition +/// wrote it. export interface MemberUse { /// The name as written: `pure`, `Cons`, `get`. name: string; @@ -298,7 +298,7 @@ export interface MemberUse { /// /// A reference to a member resolves to the declaration that owns it: `Cons` to /// `List`, `pure` to `Applicative`, an operation to its effect. That is the right -/// destination — the declaration is where the member is introduced and typed — +/// destination because the declaration is where the member is introduced and typed, /// but on its own it throws away *which* member was meant, and 28% of every /// reference in the standard library is one of these. /// @@ -307,8 +307,8 @@ export interface MemberUse { /// whole reference set from the far end turns that back into "who uses `pure`", /// at member granularity, for every kind of member at once. /// -/// This is the only way to answer it. A class method is dispatched through a -/// dictionary, so the dependency graph has no edge for the call at all — +/// Member-use data is required because a class method is dispatched through a +/// dictionary, leaving no dependency-graph edge for the call. /// `Data.Monad.map2` calls `ap` and `fmap` and has *zero* outgoing edges. Without /// this the relation strip is silent in both directions. export class Members { @@ -386,7 +386,7 @@ export interface DiffEntry { /// One side of a diff: which revision it was, and the shared tables its carried /// definition records index. The records were copied out of their index, whose /// `token_classes`/`type_table` did not travel with them, and the two revisions' -/// tables can order entries differently — so each side brings its own. +/// tables can order entries differently, so each side brings its own. interface DiffSide { title: string; contract: string; @@ -416,15 +416,15 @@ export class Revisions { readonly envelope: DiffWire["envelope"]; private readonly byId = new Map(); - /// `classes` and `types` are the *viewer's* tables — the loaded index's, which - /// every card paints against. Each side's records arrive indexing its own + /// `classes` and `types` belong to the index loaded in the viewer. They are the + /// tables every card paints against. Each side's records arrive indexing its own /// revision's tables (carried in the envelope), so they are re-encoded into the /// viewer's table space first, and then their offsets get the same move from /// bytes to code units that `Index` does for its own. /// /// `against` is the loaded index's contract. A diff records the revision its /// new side came from, and overlaying it on any other index would present old - /// bodies against definitions they were never compared with — a stale `?diff=` + /// bodies against definitions they were never compared with. A stale `?diff=` /// must be refused, not rendered. constructor(wire: DiffWire, classes: string[] = [], types: string[] = [], against?: string) { if (wire?.envelope?.format !== DIFF_FORMAT) { @@ -502,8 +502,8 @@ function retable( /// The artifact counts in UTF-8 bytes, because that is what the compiler's spans /// are and what a consumer holding the file on disk needs. A JavaScript string is /// indexed in UTF-16 code units. The two agree on ASCII and nowhere else, which is -/// why the box-drawing characters in `Syntax.Report` — the standard library's first -/// non-ASCII definition bodies — put that definition's last highlight span three +/// why the box-drawing characters in `Syntax.Report`, the standard library's first +/// non-ASCII definition bodies, put that definition's last highlight span three /// bytes past the end of its own source, and slid every link after them onto the /// wrong text. Translating once, here, is what keeps the rest of the viewer from /// having to know which unit it is holding. @@ -532,8 +532,8 @@ function rebase(d: Def, classes: string[], types: string[] = []): void { } /// A byte offset to code-unit offset table, or `null` when the text is ASCII and -/// the two coincide — which is all but a handful of definitions, so the common case -/// allocates nothing. +/// the two coincide. This covers all but a handful of definitions, so the common +/// case allocates nothing. function units(text: string): Int32Array | null { let ascii = true; for (let i = 0; i < text.length; i++) { diff --git a/web/src/viewer-review.ts b/web/src/viewer-review.ts index 47c97533..086d6d9e 100644 --- a/web/src/viewer-review.ts +++ b/web/src/viewer-review.ts @@ -1,24 +1,11 @@ -// Review state: what you have read, what you thought about it, and what has -// moved since. -// -// The point of anchoring to a content address rather than to a file and a line -// is this: a mark survives a reformat, a file move, and a rename of a local, and -// when something *does* change it can say precisely what kind of change it was. -// "You reviewed this at `a4f280f`, and since then only a dependency moved" is a -// claim a line-anchored tool cannot make, because it cannot tell that case apart -// from an edit. -// -// State is local to the browser: review notes are working memory, kept where the -// reading happens rather than in anything that needs an account or a server. +// Browser-local review state anchored to definition identity and content address. import type { Def, Index } from "./viewer-model.js"; /// How a definition stands relative to the revision a mark was made against. /// -/// The same four-way split `prism index --diff` draws, for the same reason and by -/// the same rule — a stored mark is one side of a revision pair. A cross-check -/// test pins these verdicts against the compiler's own on the same inputs, so the -/// two cannot drift. +/// Uses the same four-way split as `prism index --diff`. A cross-check test pins +/// these verdicts against the compiler's classifications. export type Freshness = "current" | "cosmetic" | "cone" | "changed" | "gone"; export interface Mark { @@ -26,8 +13,7 @@ export interface Mark { id: string; /// Whether the definition has been read and accepted. reviewed: boolean; - /// Free text. A thread with an assistant would attach here too; a conversation - /// anchored at a definition is a note that happens to have turns. + /// Free-form review notes. note?: string; /// The address when the mark was last touched, and the text at that address. /// The text is what lets a later comparison separate a reformat and a @@ -41,12 +27,8 @@ export interface Mark { at: number; } -/// The review-facing facts outside a definition's hash and text: claims are -/// erased before the layer that is hashed (`total` to `assume total` swaps a -/// proof for a trust root without moving a hashed byte), and the doc comment -/// sits outside `source` entirely. Stamped into a mark so a later visit can -/// call an edit to any of them what it is — the same carve-out the compiler's -/// own diff classification makes. +/// Review-facing facts outside a definition's hash and source text. Claims erase +/// before hashing, and doc comments sit outside `source`. export function metaOf(def: Def): string { return JSON.stringify([ def.claims ?? [], @@ -58,9 +40,8 @@ export function metaOf(def: Def): string { /// Classify a definition against the revision a mark recorded. /// -/// Deliberately the same rule as the compiler's diff: equal addresses mean equal -/// behavior, so any text difference is presentation; a moved address with -/// unmoved text means something underneath it changed and this did not. +/// Matches the compiler's diff rule: equal addresses mean equal behavior, while a +/// moved address with unchanged text indicates a dependency change. export function freshness(mark: Mark, def: Def | undefined): Freshness { if (!def) return "gone"; // Claims, visibility, doc, deprecation: authored edits the hash never sees @@ -76,7 +57,7 @@ export function freshness(mark: Mark, def: Def | undefined): Freshness { } /// Whether a definition needs the reviewer's attention again: they accepted it, -/// and its behavior has since moved. A cosmetic change does not qualify — that is +/// and its behavior has since moved. A cosmetic change does not qualify; that is /// the noise this is meant to suppress. export function needsAttention(mark: Mark, def: Def | undefined): boolean { if (!mark.reviewed) return false; @@ -91,8 +72,8 @@ interface Stored { /// The indexed unit these marks belong to: the artifact's URL joined with its /// title (see `boot`). Not the revision's contract digest, which moves on /// every change and would drop the marks exactly when they become - /// interesting — and not the title alone, which two unrelated projects can - /// share, letting one project's `main` display another's review notes. + /// interesting. The title alone is also insufficient because two unrelated + /// projects can share one, letting one project's `main` display another's notes. unit: string; marks: Mark[]; } @@ -184,13 +165,13 @@ export class Review { /// Follow marks across renames and file moves, which change the canonical /// name a mark is keyed by while preserving the content the mark is *about*. - /// Without this, moving a module silently orphans every mark in it — the one - /// survival the content-address anchoring promises. + /// Without this, moving a module silently orphans every mark in it despite the + /// content-address anchor. /// /// Two sources, tried in order. A loaded diff knows moves as facts /// (`old_id` → `id`), so those re-key directly. Failing that, a mark whose id - /// left the index follows its stamped hash — but only to an *unambiguous* - /// destination, because two definitions can legitimately share a behavior + /// left the index follows its stamped hash, but only when it has an *unambiguous* + /// destination. Two definitions can legitimately share a behavior /// hash and guessing which one the mark meant would attach a review to code /// nobody reviewed. rekey(moves: Map, index: Index): void { diff --git a/web/src/viewer.css b/web/src/viewer.css index 712ea324..9f9c974f 100644 --- a/web/src/viewer.css +++ b/web/src/viewer.css @@ -46,8 +46,8 @@ /* The rail: the modules, collapsed, over a search that reaches into all of them. A query expands what it matched, so nothing a search finds stays hidden. */ /* `hidden` is `display: none` from the user-agent stylesheet, which any author - `display` beats — including the one below. Said explicitly, or putting the rail - away leaves it in place and the deck lands on top of it. */ + `display` overrides, including the one below. Keep the rule explicit so hiding + the rail does not leave it beneath the deck. */ .rail[hidden] { display: none; } @@ -377,7 +377,7 @@ kbd { /* A resolved name inside a body. Styled as text, not as a control: the body should read as code, and only reveal itself as navigable under the pointer. - The colour is inherited on purpose — a body is syntax-highlighted, so a link + The colour is inherited because a body is syntax-highlighted, so a link that repainted every reference one accent colour would overwrite the highlighting it sits inside and win an argument it should not be having. What marks a name navigable is the underline. */ @@ -420,7 +420,7 @@ button.ref--prim:hover { /* Where a declaration introduces one of its own members. Marked like a reference because it is navigable, but it leads down the card to that member's users - rather than away — there is nowhere else to go, since this is where it lives. */ + rather than away. There is nowhere else to go because this is where it lives. */ .ref--member { border-bottom-style: dashed; } @@ -484,7 +484,7 @@ button.ref--prim:hover { /* The old pane is styled exactly like the new one. It was tinted and dimmed back when the two were stacked and nothing else told them apart; side by side, the captions and the divider do that, and a wash over one side now suggests a - meaning it does not have — in a conventional diff a tinted region marks what + meaning it does not have. In a conventional diff a tinted region marks what changed, and this tints a whole pane regardless. It also made the version being reviewed the harder of the two to read. */ .card-src--was { diff --git a/web/src/viewer.ts b/web/src/viewer.ts index 1c8cf87b..63325696 100644 --- a/web/src/viewer.ts +++ b/web/src/viewer.ts @@ -8,9 +8,9 @@ // either direction along any relation, and the URL of a view is the definition's // canonical name. // -// It deliberately does not load the wasm compiler. Every fact it renders is baked -// into the artifact — the same discipline the book's typed tooltips follow, where -// hovering a subterm runs no compiler. That keeps the viewer a pure function of +// The viewer does not load the wasm compiler. Every fact it renders is baked into +// the artifact. The book's typed tooltips follow the same rule: hovering a subterm +// runs no compiler. That keeps the viewer a pure function of // one JSON file, which is what lets it open any project's index, including one // generated somewhere else and handed over. @@ -46,7 +46,7 @@ const RELATIONS: { kind: EdgeKind; dir: "in" | "out"; label: string; hint: strin kind: "handles", dir: "in", label: "handled by", - hint: "definitions that interpret this effect — what gives it its meaning", + hint: "definitions that interpret this effect and give it meaning", }, { kind: "instance-of", dir: "out", label: "instance of", hint: "the class this implements" }, { kind: "instance-of", dir: "in", label: "instances", hint: "instances of this class" }, @@ -157,7 +157,7 @@ class Viewer { this.showRail(this.review.railShown()); const counts = this.revs?.envelope.counts; const title = counts - ? `${this.index.envelope.title} — ${counts.changed} changed, ${counts.cone} in the cone` + ? `${this.index.envelope.title}: ${counts.changed} changed, ${counts.cone} in the cone` : this.index.envelope.title; this.nodes.title.innerHTML = esc(title) + testLayer(this.index.envelope.tests) + brokenModules(this.index.modules); @@ -181,8 +181,8 @@ class Viewer { } } - // The definition to render for `id`: the current revision's, or — for an id - // the diff reports as removed — the old revision's record. A removed + // Render the current revision's definition for `id`. If the diff reports it as + // removed, render the old revision's record. A removed // definition exists only on the old side, and its review row would otherwise // be a dead button pointing at something `show` refuses to open. private lookup(id: string): Def | undefined { @@ -230,9 +230,8 @@ class Viewer { // Open `id`, or focus it if it is already in the deck. // // There is no back stack. The deck *is* the trail: what you followed is still - // open, in the order you opened it, and clicking it again is how you return — - // which is the working-set model doing the job a history stack was bolted on to - // do, less well. + // open in order, and clicking one again returns to it. The working set therefore + // provides the needed history. show(id: string): void { if (!this.lookup(id) && !this.index.builtins.has(id)) return; if (!this.open.includes(id)) this.open.push(id); @@ -392,8 +391,8 @@ class Viewer { // What a query finds. // // A declaration's *members* are results in their own right. `Cons`, `Nil` and - // `pure` are not definitions and so were unfindable — only the `List` and the - // `Applicative` that introduce them were in the rail at all — and listing them + // `pure` are not definitions and so were unfindable. Only the `List` and the + // `Applicative` declarations that introduce them were in the rail. Listing them // as their owner ("List, matched Cons") answered a question nobody asked. A // constructor is a name a reader looks up by name, so it appears under its // module as itself, badged with what it is, and opening it goes to the @@ -404,8 +403,8 @@ class Viewer { // `const`, `Console`, `cons_validation` and six others above `Cons` itself. // Modules lead with their best hit for the same reason. // - // Text matches stay a separate tier at the end. They are a different question — - // "where does this string appear" rather than "what is this called" — and there + // Text matches stay in a separate tier at the end. They answer "where does this + // string appear" rather than "what is this called." There // are two hundred of them for a name like `Cons`, which would bury everything // above. private search(q: string): { groups: { module: string; hits: Hit[] }[]; text: Def[] } { @@ -496,7 +495,7 @@ class Viewer { // What to say about one member of a declaration. // - // The count is the honest one, including zero — and zero is the common case for + // The count includes zero, which is the common case for // an effect operation, because a library declares `Output` and *programs* // perform it. Saying so, and naming the handlers that give it meaning instead, // is the difference between "this index is missing something" and "this is @@ -540,8 +539,8 @@ class Viewer { ); const authored = all.filter((e) => e.status !== "cone" && e.status !== "cosmetic"); // The consequences are offered, collapsed, rather than withheld. Leading with - // them would be wrong — on a real change the cone dwarfs the edits a reviewer - // came to read — but leaving them out of the rail entirely made the header + // them would be wrong because the cone often dwarfs the edits a reviewer came + // to read. Leaving them out of the rail entirely made the header // count three cone entries the rail gave no way to reach. return ( this.changeGroup("changed in this revision", authored, true) + @@ -647,8 +646,8 @@ class Viewer { // and a second click to see what they asked for is a click that buys nothing. // Folding stays for a card being kept open for reference. const shut = this.folded.has(id); - // Nothing relates to `compose` in either direction — it calls only its own - // parameters — and an empty strip is a bordered band of nothing, so it goes. + // Nothing relates to `compose` in either direction because it calls only its + // own parameters. Omit the empty relation strip. const rel = this.relations(id); return `

@@ -736,8 +735,8 @@ class Viewer { // What has happened to this definition since it was marked read. // - // The whole reason a mark is anchored to a content address: it can say which - // *kind* of change happened. A reformat is dismissed outright, a dependency + // A content-addressed mark can identify which *kind* of change happened. A + // reformat is dismissed outright, a dependency // shift is named as one, and only a real edit asks for the definition to be // read again. A line-anchored mark can say none of this, because it cannot tell // the three apart. @@ -755,7 +754,7 @@ class Viewer { case "gone": return `
reviewed ${at}; no longer in this revision
`; default: - return `
reviewed ${at}; edited since — read again
`; + return `
reviewed ${at}; edited since; read again
`; } } @@ -763,9 +762,8 @@ class Viewer { // // A `cone` entry gets a sentence rather than a second copy of identical text: // its bytes did not move, only its address did, because something it depends on - // changed. Saying that plainly is the whole reason the classification exists — - // it is the difference between a reviewer reading three edits and scrolling - // past forty-seven. + // changed. The classification lets a reviewer read the three edits without + // scrolling past all forty-seven consequences. private before(id: string): string { const e = this.revs?.get(id); if (!e) return ""; @@ -791,7 +789,7 @@ class Viewer { // Side by side rather than stacked: the two versions of a definition are being // compared, and comparing means reading across, not scrolling. The left pane is // painted and linked exactly like the right one, from the old revision's own - // occurrence rows — a name in the version you are moving away from is as worth + // occurrence rows. A name in the version you are moving away from is as worth // following as one in the version you are moving to, and the artifact carries // what it needs to do that. A target the old revision had and this one does not // keeps its text without becoming a link, the same rule every other reference @@ -829,8 +827,8 @@ class Viewer { // The rendered type, painted and linked exactly like a body. // - // A signature is not source — no file holds it, the typechecker rendered it — - // but the artifact carries spans over it anyway, from the compiler's own lexer + // A signature is rendered by the typechecker rather than read from a source file. + // The artifact still carries spans over it from the compiler's own lexer // run across the rendered string. So `List` and `Concurrent.Async` in a // signature are the same colour and the same link they are in a body, which is // the point: the signature is the part a reader reads first. It leads with the @@ -847,8 +845,8 @@ class Viewer { // Paint one text with its highlight spans and wrap its references in links. // - // `brief` drops the module from a qualified name — `Data.Vec.Vec(a, 0)` reads as - // `Vec(a, 0)` — which is for the rendered signature, not for source. The + // `brief` drops the module from a qualified name, so `Data.Vec.Vec(a, 0)` reads as + // `Vec(a, 0)`. This applies to rendered signatures, not source. The // typechecker qualifies every name it prints because it has no scope to print // against, while a reader has this card: the module is on the header, the full // name is on the link's tooltip, and the body below writes `Vec` too. @@ -909,7 +907,7 @@ class Viewer { } at = r.end; // Where this declaration introduces one of its own members. It resolves to - // nothing to navigate to — it is already here — so it points at its own + // nothing to navigate to because it is already here, so it points at its own // list of users further down the card. if (r.ty !== undefined) { // Hoverable, not navigable: a local binds here and leads nowhere. @@ -954,7 +952,7 @@ class Viewer { // plus this declaration's own members where it introduces them. // // The occurrence rows cannot supply the second. A member's declaration site is a - // *binder*, not a use, so the renamer has nothing to record there — and a + // *binder*, not a use, so the renamer has nothing to record there. A // reference to it would resolve to the declaration we are already reading. The // artifact carries those positions separately, from the compiler's own list of // what each declaration declares. @@ -967,7 +965,7 @@ class Viewer { } // A name the checker gave a type. Only where nothing else already claims the // span: a reference's tooltip carries the *definition's* type, which is the - // better answer where there is one, so these fill in what is left — the + // better answer where there is one, so these fill in what is left: the // parameters and locals, which have no definition to point at. for (const s of decodeSpans(d.types, this.index.typeTable)) { if (marks.some((m) => s.start < m.end && m.start < s.end)) continue; @@ -983,7 +981,7 @@ class Viewer { // of looking broken. private relations(id: string): string { // Edges first, members after. On a type the member rows are the heaviest thing - // on the card — `Option` has 127 uses of `None` and 135 of `Some` — and leading + // on the card. `Option` has 127 uses of `None` and 135 of `Some`, and leading // with them buries the summary of what the definition relates to under the // detail of who writes each of its parts. return this.edgeRows(id) + this.memberRows(id); @@ -995,7 +993,7 @@ class Viewer { // These are not edges, and could not be. A class method is dispatched through a // dictionary, so the dependency graph records no call: `Data.Monad.map2` calls // `ap` and `fmap` and has no outgoing edges at all. What does know is the - // occurrence set, read from the far end — a reference to a member resolves to + // occurrence set, read from the far end. A reference to a member resolves to // the declaration that owns it, and the span it covers says which member was // meant. Without this a class card can list its instances and nothing else. private memberRows(id: string): string { @@ -1022,9 +1020,9 @@ class Viewer { // The call rows lead with what the source names, in the order it names them, // and then with what the dependency graph adds. The two are not the same set: // elaboration inlines a top-level `let`, so a body that writes `gen_float` - // depends on what the constant expanded to instead. Both are worth having — - // one is what you can point at on the page, the other is what actually runs — - // and a chip is marked when it is only the second, since a name appearing in + // depends on what the constant expanded to instead. Both are useful: one is + // what you can point at on the page, while the other is what actually runs. + // A chip is marked when it is only the second, since a name appearing in // a row and nowhere in the body it belongs to reads as a bug. const written = this.mentions.get(dir, id); const derived = edges.filter((t) => !written.includes(t)); @@ -1046,8 +1044,8 @@ class Viewer { // One relation row: a label, a count, and the targets as chips. // - // Capped, because these lists are not small — `List` is used by 374 definitions - // and its `Cons` is written by 183 — and a card that opens with six hundred + // Cap these large lists. `List` is used by 374 definitions and its `Cons` is + // written by 183; a card that opens with six hundred // chips is a card nobody reads. The count is always the true one and the // remainder is one click away, so nothing is silently dropped; what is hidden is // hidden visibly. @@ -1106,10 +1104,9 @@ const FENCE_NOTE: Record = { /// Render a docstring. /// -/// Deliberately not a markdown library, and not markdown either: it is the small -/// dialect the docstrings actually use. Across the 723 documented definitions in -/// the standard library there is not one list, heading, emphasis or link — there -/// are 602 inline code spans and 386 examples, each usually paired with the +/// Render the small docstring dialect used by the standard library. Its 723 +/// documented definitions contain no lists, headings, emphasis, or links. They +/// contain 602 inline code spans and 386 examples, each usually paired with the /// `output` block asserting what it prints. Paragraphs, inline code and fences /// cover all of it, and a construct that never appears is not worth a dependency. /// @@ -1153,7 +1150,7 @@ const inline = (s: string): string => esc(s).replace(/`([^`]+)`/g, "$1 // A `tested by` row is empty for two entirely different reasons, and an empty row // looks the same either way: the unit declares no tests, or it declares tests // whose elaboration failed and the layer could not be built. The second is the -// dangerous one — every definition then reads as untested — and the artifact knows -// which it is, so the page should not make a reader ask. +// dangerous one because every definition then reads as untested. The artifact +// distinguishes the cases, so the page should too. const testLayer = (tests: Envelope["tests"]): string => { if (tests === "included") return ""; if (tests === "empty") { @@ -1198,7 +1195,7 @@ const shortName = short; const hashChip = (d: Def): string => d.hash ? `` - : ``; + : `none`; // What each surface kind is called, and what that means. The label is the // keyword that declares it wherever Prism has one, so the badge reads the way the @@ -1227,7 +1224,7 @@ export const KINDS: Record = { // legend; the tooltip carries the sentence the word still leaves out. const kindBadge = (kind: string): string => { const k = KINDS[kind]; - const tip = k ? `${k.label} — ${k.gloss}` : kind; + const tip = k ? `${k.label}: ${k.gloss}` : kind; return `${esc(k?.label ?? kind)}`; }; @@ -1236,7 +1233,7 @@ const kindBadge = (kind: string): string => { // and "builtin" is the fact that distinguishes the row from every definition // around it. const builtinBadge = (): string => - `builtin`; + `builtin`; // `CSS.escape` is not in every target here, and card ids are canonical names that // can carry `.` and `@`; quoting them for an attribute selector is enough. @@ -1248,9 +1245,9 @@ const cssEscape = (s: string): string => s.replace(/["\\]/g, "\\$&"); /// whose entire purpose is answering "what is this" that delay is most of the /// answer's value gone. So the tooltip is ours. /// -/// Deliberately not a positioning library. The viewer is a self-contained artifact -/// reader — no wasm, no dependencies, under 20 kB — and a dependency to place a box -/// near a word would cost more than it explains. The book's typed tooltips +/// Position a tooltip without adding a library. The viewer is a self-contained +/// artifact reader with no wasm and no dependencies, under 20 kB. A positioning +/// dependency would cost more than it explains. The book's typed tooltips /// (`docs/theme/prism-tooltips.js`) solve the same problem the same way, so this /// follows that precedent rather than introducing a second approach. function wireTooltip(): void { @@ -1308,7 +1305,7 @@ function wireTooltip(): void { /// /// Tested on the tag rather than with `instanceof`, so it holds for an element /// from another realm (an iframe, a different document) where the constructor -/// identity differs but the element is just as editable — and so the rule can be +/// identity differs but the element is just as editable, so the rule can be /// checked without a DOM. export function isEditable(target: EventTarget | null): boolean { const el = target as (Partial & { tagName?: string }) | null; @@ -1326,7 +1323,7 @@ function wireNavigation(viewer: Viewer): void { const id = goto.dataset.goto ?? ""; viewer.show(id); // A rail row for a member opens the declaration it lives in, then points at - // the member inside it — otherwise a search for `Cons` lands you on `List` + // the member inside it. Otherwise a search for `Cons` lands you on `List` // with no indication of why. if (goto.dataset.member) viewer.revealMember(id, goto.dataset.member); return; diff --git a/web/test/dom.mjs b/web/test/dom.mjs index 1ee48fff..0507ae36 100644 --- a/web/test/dom.mjs +++ b/web/test/dom.mjs @@ -1,9 +1,9 @@ // The smallest DOM the viewer touches, and the harness that drives it. // // The viewer has no test runner because it has no framework: it is one module over -// one JSON file. But its render path is the half most worth checking — an offset +// one JSON file. Its render path is the half most worth checking: an offset // off by one silently corrupts a body, and a guard that forgets `