From 70cdefc4834550c1323635152a0f20d2e081b883 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Mon, 9 Nov 2026 05:38:55 +0000 Subject: [PATCH] Centralize native execution and avoid unusable Power recordings --- CONTRIBUTING.md | 6 +- Cargo.toml | 2 +- DEV.md | 62 ++-- nbs/target.qmd => TARGET.md | 2 + cli/src/worker.rs | 19 +- nbs/foundations.qmd | 149 +++------ nbs/llms.txt | 2 +- nbs/performance.qmd | 6 +- nbs/study/01_basics.ipynb | 6 +- nbs/system/explain.qmd | 4 +- nbs/web.ipynb | 4 +- py/src/lib.rs | 5 +- pyproject.toml | 4 +- python/basedpl/bpltests.py | 143 ++++---- src/array.rs | 20 +- src/array/storage.rs | 10 +- src/batch.rs | 21 +- src/derivative.rs | 9 +- src/derivative/tape.rs | 32 +- src/element.rs | 56 +--- src/eval.rs | 11 +- src/eval/assign.rs | 6 +- src/eval/operators.rs | 20 ++ src/eval/operators/agenda.rs | 8 +- src/eval/operators/each.rs | 5 +- src/eval/operators/folds.rs | 11 +- src/eval/operators/key.rs | 10 +- src/eval/operators/power.rs | 10 +- src/eval/operators/products.rs | 9 +- src/eval/rewrite.rs | 10 +- src/eval/session.rs | 14 +- src/events.rs | 90 +++++ src/explain.rs | 40 +-- src/fusion.rs | 357 ++++++++++---------- src/lib.rs | 2 + src/number.rs | 10 +- src/pervasive.rs | 68 ++-- src/pervasive/fold.rs | 13 +- src/pervasive/product.rs | 4 +- src/pervasive/scan.rs | 2 +- src/plan.rs | 169 +++++++--- src/primitive.rs | 12 +- src/primitive/numeric.rs | 2 +- src/program.rs | 131 ++++++-- src/program/blocks.rs | 392 +++++++++++----------- src/program/blocks/grouping.rs | 42 +-- src/program/blocks/items.rs | 157 ++------- src/program/blocks/record.rs | 2 +- src/program/contraction.rs | 22 +- src/program/layout.rs | 27 +- src/program/run.rs | 114 +++++-- src/program/trace.rs | 294 ++++++++-------- src/program/verify.rs | 14 +- src/search.rs | 4 - src/selection.rs | 8 +- src/settings.rs | 2 +- src/transform.rs | 4 +- testkit/src/lib.rs | 3 + testkit/src/reference.rs | 20 +- testkit/src/timing.rs | 49 ++- tests/derivatives.rs | 1 + tests/jit.rs | 1 + tests/reference.rs | 3 + tests/reference/README.md | 2 +- tests/reference/aplcart.bpl | 29 +- tests/reference/aplcart_mathish.bpl | 11 + tests/reference/april.bpl | 100 ++---- tests/reference/april_mathish.bpl | 61 ++++ tests/reference/core.bpl | 228 ++++--------- tests/reference/core_mathish.bpl | 151 +++++++++ tests/reference/dyalog.bpl | 3 - tests/reference/dyalog_mathish.bpl | 5 + tests/reference/graphics.bpl | 12 +- tests/reference/lib.bpl | 12 - tests/reference/lib_mathish.bpl | 14 + tests/reference/ngn.bpl | 3 - tests/reference/ngn_mathish.bpl | 5 + tests/reference/static.bpl | 6 +- tests/settings.rs | 49 ++- tests/speed.rs | 13 +- tests/speed/arrays.bpl | 500 ++++++++++++++-------------- tests/speed/bodies.bpl | 13 + tests/speed/calls.bpl | 4 +- tests/speed/comparisons_mathish.bpl | 14 + tests/speed/growth.bpl | 38 +-- tests/speed/images.bpl | 30 +- tests/speed/maps.bpl | 102 +++--- tests/speed/models.bpl | 62 ++-- tests/speed/moving.bpl | 14 + tests/speed/steps.bpl | 46 +-- tests/speed/structure.bpl | 89 +++-- tests/speed/tables.bpl | 4 +- tests/wasm-speed.mjs | 6 +- 93 files changed, 2324 insertions(+), 2037 deletions(-) rename nbs/target.qmd => TARGET.md (99%) create mode 100644 src/events.rs create mode 100644 tests/reference/aplcart_mathish.bpl create mode 100644 tests/reference/april_mathish.bpl create mode 100644 tests/reference/core_mathish.bpl create mode 100644 tests/reference/dyalog_mathish.bpl create mode 100644 tests/reference/lib_mathish.bpl create mode 100644 tests/reference/ngn_mathish.bpl create mode 100644 tests/speed/bodies.bpl create mode 100644 tests/speed/comparisons_mathish.bpl create mode 100644 tests/speed/moving.bpl diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ccbb7938..b97dc4d7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,10 +14,10 @@ For a standalone executable without Python, run `cargo install --path cli`. Carg ## Contributing -Install the development and documentation tools with `pip install -e '.[dev]'`. That also builds the extension and installs it in the editable package. The main commands are: +Install the development and documentation tools with `pip install -e '.[dev]'`, and install the Rust test runner with `cargo install cargo-nextest --locked`. Run `cargo develop` to build and install the extension and CLI. The main commands are: ```bash -cargo test +cargo nextest run --cargo-profile release cargo develop bpl -e '2×3+4' cargo fastfmt @@ -25,7 +25,7 @@ pytest ship-rs-build ``` -After Rust changes, run `cargo test` for the Rust tests, then `cargo develop` to install what `cargo test` built: the extension, and the native `bpl` command in your environment. Both use one compilation of each crate. `cargo develop` comes from `fastws-cli`, one of the development tools. Run `pip install -e '.[dev]'` again only when the package metadata changes. Use `cargo fastfmt`, not `cargo fmt`. +After Rust changes, run `cargo develop` to rebuild and install the extension and native `bpl` command, then `cargo nextest run --cargo-profile release` for the Rust tests. Both use the release build. `cargo develop` comes from `fastws-cli`, one of the development tools. Run `pip install -e '.[dev]'` again only when the package metadata changes. Use `cargo fastfmt`, not `cargo fmt`. Before a release, run `bpl-tests prerelease` from the repository root. Among its steps, it writes the syntax highlighters' glyph lists and the macOS keyboard layout bundle, saves the notebooks' outputs, and runs nbdev's `prepare` to export, test and clean the notebooks and render the README. DEV.md's "Release" section lists every step. diff --git a/Cargo.toml b/Cargo.toml index be9bb227..623546f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ rust-version = "1.98" license = "Apache-2.0" [workspace.dependencies] -rustymath = { version = ">=0.1.56, <1", default-features = false } +rustymath = { version = ">=0.1.59, <1", default-features = false } rustymath-buffer = ">=0.1.55, <1" rustymath-timing = ">=0.1.56, <1" pyo3 = ">=0.29.2, <1" diff --git a/DEV.md b/DEV.md index 0a27e558..5562e4dd 100644 --- a/DEV.md +++ b/DEV.md @@ -20,20 +20,24 @@ Bare number = float (`3`; integer `3ₓ`): a language rule, not an implementatio Read `nbs/llms.txt` in full. Its published links may omit unreleased changes: use the corresponding sources under `nbs/`. Before writing BPL examples or tests, read `nbs/composition.qmd`. Check a very small non-trivial expression's linear form, result and shape through `fresh` before running it at full size. +Judge simplicity by system ownership and caller obligations, not just line count. Report production code across BPL and rustymath, per repo and combined, excluding tests, comments and all tooling-related code wherever located. LoC is diagnostic, not pass/fail. If consolidation does not reduce it, check missed deletions, adapters and RM/BPL co-design. A necessary, controlled mechanism in one or two functions, with a clear API and precisely defined behavior, is much less problematic than workarounds, conditions and proof plumbing spread throughout the codebase. Use rigorous, meaningful tests proportionate to the implementation and actual uncertainty, not large mock suites. + Performance targets are analytical functions running inside Power, Each, broadcasting, scan, reduce, Agenda and similar array operators, in tacit and dfn forms and meaningful AD uses. Top-level statement overhead, scalar interactive latency, recursion/Fibonacci and guarded-dfn latency are not targets. Exclude fixed outer-statement/host setup and first-time recording/compilation from steady-state comparisons and budgets; report first-call costs separately. Measure useful repeated body work and completed operator execution, including its final force. Charge all lookup, binding, dispatch, allocation and kernel work that repeats inside the operator. Use existing calibrated tools or measured slopes to remove or amortize fixed outer costs, never an estimated blanket overhead subtraction. Report the measurement boundary and raw versus adjusted costs. If outer costs cannot be separated cleanly, report the completed workload's cost with that limitation explicit. -The current plan is `meta/eager-release-plan.md`. Release 1 is eager basedpl, with its engine taken from basedpl `96ea793`, running on current rustymath. The release's language and results are its default, plus the settled changes the plan lists, with one home per fact. The release's behaviour, edge cases and errors included, is a default rather than a requirement: where keeping a behaviour would add substantive complexity and a reasonable simpler design exists, discuss it with Jeremy first. `meta/owners.md` describes subsystem interfaces. `meta/` records reasoning and evidence, not extra instructions or file leases. `AGENTS.md` holds the founding principles. `nbs/target.qmd` and `nbs/foundations.qmd` record the parked graph design and constrain nothing. Rustymath and basedpl are co-designed: use one mechanism per job and one home per fact, changing either project where that makes the combined system simpler. +The active work plan is `meta/next-release-plan.md`; it owns proposed stages, complexity gates, the mechanism queue and current baseline/results. This reference describes implemented contracts. `meta/eager-release-plan.md` retains the earlier eager-release reasoning and settled language changes; `meta/owners.md` describes subsystem interfaces. Existing behaviour is the default: discuss a simpler alternative before a substantive semantic change. `meta/` records evidence, not extra instructions or file leases. `AGENTS.md` holds the founding principles. `nbs/foundations.qmd` explains the working eager/Program architecture and its reasons. `TARGET.md` describes the parked graph design and constrains nothing. BPL and rustymath are co-designed: change either project to keep one mechanism per job and one home per fact. -Source, builds and both projects' `meta/` directories are on Syd1 under `/home/ubuntu/aai-ws/{basedpl,rustymath}`. `meta/` isn't in git. Mac copies are snapshots: never sync them over newer Syd1 work. `meta/open.md` lists open work and links the plan. Tag `graph-parked` in both repos holds the graph-repair tree. Reference worktrees live outside `~/aai-ws`, because the uv workspace takes every sibling directory as a member and the generated cargo patches point at siblings: +Current source, builds and both projects' `meta/` directories are on this Mac under `/Users/jhoward/aai-ws/{basedpl,rustymath}`. Mac is authoritative for current planning and release work; do not overwrite it with older Syd1 copies. `meta/` isn't in git. `meta/open.md` lists current work and links the plan. Reference worktrees live outside `~/aai-ws`, because the uv workspace takes every sibling directory as a member and the generated cargo patches point at siblings. The Mac pre-rewrite pair is `/Users/jhoward/git/prewrite-mac-apVxJ7tG/{basedpl,rustymath}` at BPL `96ea793` / RM `d39f693`. Syd1 retains these earlier reference archives: - `/home/ubuntu/git/graph-refs/{basedpl,rustymath}`: tag `graph-parked`. - `/home/ubuntu/git/pre-rewrite-refs/{basedpl,rustymath}`: basedpl `96ea793` and rustymath `d39f693`, the baseline. With `json5` 1.3.1 they pass all 8,035 reference cases. `json5` 1.3.7 parses integers past `u64` as floats. Results: `meta/pre-rewrite-baseline.md`. Per-case costs: `meta/pre-rewrite-costs.tsv`. - `/home/ubuntu/git/release-refs/{basedpl,rustymath}`: v0.1.82 and v0.1.54. -Main coordinates connected work and publishes interface changes to consumers. One coordinator captures a coherent source pair for an infrequent useful build; writers and reviewers continue on the live trees. There are no file leases, global editing freezes or continuous rebuild watchers. The frozen pair lives in `/home/ubuntu/git/integration/{basedpl,rustymath}`, with basedpl's cargo config pointing at that rustymath. Use the documented snapshot/build helpers: snapshots include untracked files without moving live `HEAD` or the real index. The coordinator owns `cargo develop`, native checks and integration validation. Pin checks to the captured artifacts and fixture version in `meta/release-validation.md`; source review is not runtime proof. The plan's Process section records the current procedure. +Main coordinates connected work and publishes interface changes to consumers. Use this Mac for current development and release work. Normally use `cargo develop` to install the real work in the real environment and test it directly. No protective ambient restore or extra environment copy is needed. One coordinator owns builds, installs and validation while writers continue. Use existing automatic snapshot helpers only when they help that work continue; do not mandate extra manual captures. Artifact/source/fixture pins and `build=` serve actual before/after comparisons or diagnosis, not ceremony. Source review is not runtime proof. Add process complexity only when it clearly helps agents work faster. There are no file leases, global editing freezes or continuous rebuild watchers. + +Use local Python kernels for current work; each fork keeps its own kernel. Check `socket.gethostname()` before relying on machine identity. Review edits with `journal(os.environ['EXHASH_JOURNAL'], author=)`. Earlier Syd1 kernels and journals belong to the retained remote evidence. Keep commits local unless the current shipping instructions request a push. -Kernels use `host="syd1"`; each fork creates its own kernel and uses main's shared SSH forward. Review edits with `journal(os.environ['EXHASH_JOURNAL'], author=)`. Older kernels use `/tmp/exhash-journal/mcp/.jsonl`; `list_kernels(host="syd1")` maps their identities. Keep commits local unless the current shipping instructions request a push. +Python checks use installed builds, which the coordinator installs; never load an uninstalled extension through `PYTHONPATH` or `sys.path`. Run no formatter. Research that changes no code still records its findings in `meta/`. `create`'s reply names no host: check `socket.gethostname()` after it. -Reference evaluations keep their two-second deadline (`reference::TIMEOUT`). The worker's separate 30-second watchdog bounds work that doesn't poll cancellation. Every asserted value, kind, error and output gates, including cases that call `•explain`, `•memory` or `•storage`. Route-directive violations remain separately reported obligations; they never waive a case's assertions. Retain exhash inputs in a kernel variable. After a conflict, refresh addresses and preserve semantic intent. Use `build=` to pin checks to an exact build, and record its source snapshot and fixture version. Never shrink cases, introduce skips, relax budgets or alter expected mathematics to clear failures without Jeremy's agreement. +Reference evaluations keep their two-second deadline (`reference::TIMEOUT`). The worker's separate 30-second watchdog bounds work that doesn't poll cancellation. Every asserted value, kind, error and output gates, including cases that call `•explain`, `•memory` or `•storage`. Route-directive violations remain separately reported obligations; they never waive a case's assertions. Retain exhash inputs in a kernel variable. After a conflict, refresh addresses and preserve semantic intent. Never shrink cases, introduce skips, relax budgets or alter expected mathematics to clear failures without Jeremy's agreement. The reference below remains applicable. Read the relevant part when using its tools or changing its concern: accuracy and dynamic kinds for numerical work; build/test, routes and budgets for validation; cancellation and timing locks for jobs; browser and machine code for those targets. Do not reread unrelated tool catalogues at every handoff. @@ -47,7 +51,7 @@ Rustymath and basedpl have no outside users: treat neither as published. Machine The design and `meta/` notes for work ahead: -- `eager-release-plan.md`: release 1's plan. Its aims: about the same speed as the previous release, or better, with less code, clearer responsibilities and a better foundation for static types. Speed is measured against the baseline costs in `meta/pre-rewrite-costs.tsv`. `owners.md`: pieces, their files and the pinned interfaces between them. `nbs/target.qmd`: the parked graph design, constraining nothing. Its “Numerical algorithms” part records the approved numerical direction and the validation needed before documenting changed accuracy bounds. +- `eager-release-plan.md`: release 1's plan. Its aims: about the same speed as the previous release, or better, with less code, clearer responsibilities and a better foundation for static types. Speed is measured against the baseline costs in `meta/pre-rewrite-costs.tsv`. `owners.md`: pieces, their files and the pinned interfaces between them. `TARGET.md`: the parked graph design, constraining nothing. Its “Numerical algorithms” part records the approved numerical direction and the validation needed before documenting changed accuracy bounds. - `open.md`: every open item, and the checks for the closing pass. - `foundation-reset.md`: removals, released algorithms to reuse, deferred ideas, and the "Deletion checklist" of old code that the target's "Not" lists name and the tree still holds. - `crate-split-2026-10-31.md`: whether to split basedpl into crates: no split now; rebuild time by build step; engine-to-language references, recounted at the closing pass. @@ -68,7 +72,7 @@ Build and test: - `cargo develop`: rebuilds and installs the native extension and `bpl` with the `release` profile, which development, tests and timings all use. `fresh`, `check_file` and `pytest` load the installed build. Cargo tests build missing code but install nothing. - `cargo cs` checks every target in its workspace, tests included, without codegen. Rustymath's tests need `cargo cs` in rustymath. -- Default test command, from the kernel: `fastws.cargo.run_tests([basedpl, rustymath])`, with each repository's path. The nextest `default` profile includes every test, including performance checks. It builds both repositories' native tests once each with the configured development Cargo profile (`release` here), without a build timeout, then runs the roots concurrently. Results describe the compiled build, not later working-tree edits. `timeout` bounds each repository's nextest run, 30 seconds by default; a stopped run fails with `timed_out` set. Filters and libtest flags go in `test_args`; `timeout` takes seconds or `None`, not `worker.Limit`. Optional nextest profiles for focused diagnostics and cross-repo timing schedules are documented in [fast tests](meta/fast-tests.md); they are not needed for complete coverage. Doctests are disabled; meaningful example assertions belong in ordinary Rust tests. +- Default test command, from the kernel: `fastws.cargo.run_tests([basedpl, rustymath])`, with each repository's path. The nextest `default` profile includes every test, including performance checks. It builds both repositories' native tests once each with the configured development Cargo profile (`release` here), without a build timeout, then runs the roots concurrently. Results describe the compiled build, not later working-tree edits. `timeout` bounds each repository's nextest run, 30 seconds by default; a stopped run fails with `timed_out` set. Filters and libtest flags go in `test_args`; `timeout` takes seconds or `None`, not `worker.Limit`. Optional nextest profiles for focused diagnostics and cross-repo timing schedules are documented in [fast tests](meta/fast-tests.md); they are not needed for complete coverage. Doctests are disabled; meaningful example assertions belong in ordinary Rust tests. Ordinary validation leaves `BASEDPL_COSTS` unset because it selects calibration and footprint work rather than logging alone. - nextest runs each test in its own process and every binary at once. Separate processes run tests in parallel: the threads of one process take turns at its one session (`eval::Slot`). In one repository, `cargo nextest run --cargo-profile release` or `run_tests(root)` runs the default profile, which reports a test as slow at 5 s and stops it at 30 s, runs `reference` first with nothing beside it, runs `timed_images` with nothing beside it, and starts the other timed tests last, one at a time. Add a filterset such as `-E 'binary(core)'`, or a test name. `cargo nextest run --archive-file FILE --workspace-remap .` runs a build that `cargo nextest archive` packed, without compiling. Plain `cargo test --release` runs the same tests in turn; `tests/jit.rs`'s size tests hold a lock around the process-wide settings they change. Bare `cargo test` builds the `test` profile, which has `release`'s settings but compiles everything again into `target/debug/`. - `pytest -q`: at the end of a piece of work. Before reporting a `pytest` failure in untouched code, rebuild with the same profile and rerun. - Builds have no execution timeout; Cargo serializes them per target directory. Worker checks and timings stop after 30 s of work (`basedpl.worker.LIMIT`), excluding builds and the worker's wait for the timing lock. `timeout=` takes seconds, `None`, or a `worker.Limit` that calls share. A check stopped at its limit gives each unfinished case as `unchecked`; other tools raise `TimeoutError`. `cargo_costs` shares that budget but counts all time inside its native test binary, including lock waits. A plain shell `cargo test` has no limit: use `run_tests`. Pytest's separate `timeout = 30` watchdog bounds each test's setup, call and teardown together; it is not a suite execution budget. @@ -98,15 +102,25 @@ Instruction counts: code layout alone moves timings more than a few percent, hid - A case is an expression (`k(⌈/⊢)⌸a`) that each step evaluates at top level and forces in full, as the speed suite's timings do (`timing::time`), through the worker's `steps` request. A case naming `N` makes its own calls (`{⍵}⍣N 0`: one dfn call a step) and must make one per step: a map like `f¨N⍴0` calls its dfn once on all cells, giving no per-call cost. - Fresh single-threaded `bpl` processes at four `N`, three each (`runs=`), lowest count per `N`; each successive pair's slope, Δinstructions/ΔN, cancels startup and `setup=`; the median of the three slopes drops a one-off cost starting past some size (`≢•ucs N⍴"ab" "cde"`: about 5×10⁶ more from about 131,350 items). All `N` lie past warm-up, from rough counts at 1 step and, for a step under 10⁸ instructions, at enough steps to retire about 10⁸: allocating cases retire up to about 5×10⁶ extra, varying by process, while the allocator settles; successive `N` 5×10⁸ instructions apart keep that under 1% of a slope. `counts=` sets two or more `N`. Reads `/usr/bin/time -l` (macOS), `perf stat` (Linux). - `build=` another build; a list (`build=[base, None]`) counts each at the same `N`, side by side, with the first-to-last change. -- Counts run `bpl --worker` with a `steps` request. Builds that predate it, the release's included, can't be counted or profiled; compare them with timings. +- Counts run `bpl --worker` with a `steps` request. Builds without that request cannot be counted; compare them with timings. - Speed-case counts: a speed case's footer gives, after its time budgets, its instructions and allocated bytes for each item on each platform: `⍝⍝: 2.5 3.4 4.6 instructions macos 20.2 linux 33.1 allocated macos 8 linux 8`. Time budgets of `-` make a count-only case, as in `speed/growth.bpl` and `speed/steps.bpl`. `timing::counts` runs each case on one thread with compiling off, once to warm and once counted, or `n` times in a `steps` section, and divides by the setup's `n`. It takes no timing lock. On `BUDGET_CPU`, a count fails above 1.25× its footer value (`rustymath_timing::COUNT_MARGIN`), or 1.1× in `calls` and `steps` sections, and allocated bytes get 1 byte an item more. Footers hold measurements: after a change that moves counts, or a compiler update, run the speed tests with `BASEDPL_COSTS=1` and write the counts with `set_counts(log)`. Counts check the uncompiled path that first runs and the browser take; timings check compiled stages. -- `profile(case)`: estimated instructions by function. A fresh `bpl --worker` process runs setup and one warm step, replies, then repeats the step until it is killed. macOS `sample` or Linux `perf` samples its main thread's call stacks. Each function's self-sample share times the build's `instructions` count estimates its instructions per step, assuming one retirement rate for all functions. `profile_diff(case, base)` compares those estimates for two pinned builds; inlining can move a function's count into its caller. For costly cases, pass `counts=[1,2], runs=1` to skip calibration. `callers=N` groups each leaf with up to N caller frames. Use these before reading machine code. +- `profile(case)`: estimated instructions by function. One `profile` worker request runs setup, parses the case and forces two warm calls in the same Session before sending a flushed readiness reply. Only then does macOS `sample` or Linux `perf` sample the repeating runner's main-thread stacks. The warm passes cover recording and its first replay; readiness does not promise that every future execution path is cached. Each function's self-sample share times the build's `instructions` count estimates its instructions per step, assuming one retirement rate for all functions. `profile_diff(case, base)` compares those estimates; both workers must support `profile`, while `time_cases` still compares older builds. Inlining can move a function's count into its caller. For costly cases, pass `counts=[1,2], runs=1` to skip calibration. `callers=N` groups each leaf with up to N caller frames. Use these before reading machine code. - `call_profile(case, function)` (macOS, Apple silicon): exact instructions one call of `function` retires, callees included, by function. `lldb` runs the case once per step in a fresh process, lets `skip=` calls pass, single-steps the next to its return. `function`: regex on a demangled name (`binder::derived`), which must have code of its own. `base=`: a build shown beside `build`. Allocator functions and callees share one row (heap-state dependent); a second total excludes it. `profile`'s time-based estimates can misattribute: find the changed function with `profile_diff`, settle it with `call_profile`. Limit: calls of 100,000 instructions, at about 450 a second. On Linux, install `perf` and `binutils`, then allow user events `instructions:u` and `cycles:u`. Check the selected `perf` with `profile('1+1', build=PIN, seconds=1, callers=4)` before an investigation. `kernel.perf_event_paranoid=1` permits those events on Syd1; changing it requires the machine owner's approval. Missing tools, failed commands, missing instruction counts, empty samples and unparseable samples raise errors. Unknown samples remain in the total; limiting displayed rows never renormalizes their shares. Report a tooling gap immediately with its owner and next check. Syd1 selects the installed perf 6.11.11 through `~/.local/bin/perf`; its HWE tools package is marked manually installed. Distro perf 7.0.14/libdw applied an incorrect 0x1000 ELF load bias to native BPL frames. Readable symbols alone did not establish valid callers. Matching `libc6-dbg` resolves libc internals. The selected perf and the shared `c++filt` step have been verified on native and formatter call chains without a BPL rebuild. +Linux allocation stacks: install `heaptrack` without GUI recommends (`sudo apt-get install --no-install-recommends heaptrack`). No Rust rebuild is needed. Generate the existing worker `steps` request with `bpltests._command(case, 16, setup)`; use the same setup, explicit numeric mode and forced-call count for both pinned binaries. Run captures through `fastws.procs.bounded` with a 30 s limit and `worker.environment(compile="never")`. The underlying commands are: + +```sh +heaptrack --record-only -o capture /path/to/pinned/bpl --worker < request.jsonl +heaptrack_print -f capture.zst -d baseline.zst > diff.txt +c++filt --format=rust < diff.txt > diff.demangled.txt +``` + +`heaptrack_print -f capture.zst -H sizes.tsv` gives allocation-size/count pairs: sum size × count for total bytes, not peak memory. Its backtrace filter does not filter that histogram or the global summary. Verify totals against the existing allocation counter before attributing a regression; peak/RSS or sampled allocator time do not measure allocation churn. This workflow is verified on Syd1's Counted `kmeansi` case. + The helper enables Cranelift's existing perf-map output in its child and removes the map when that child stops. Generated kernels appear as `kernel`. Their samples have no caller-unwind metadata; native caller support does not imply unwinding through a JIT kernel. `bpl-tests NAME ARGUMENT...` runs one of these from the shell, printing its result: `check_file`, `check_page`, `time_cases`, `instructions`, `profile`, `profile_diff`, `call_profile`, `disasm`, `jit_disasm`, `speed_costs`, `thread_scaling`, `cargo_costs`, `pytest_costs`, `run_pytest`, `compare_costs`, `bisect`, `explain_diff`, `wasm_test`, `wasm_stack`, `target_builds`, `prerelease`, `module_edges` (references from the engine's modules into the language's, `meta/crate-split-2026-10-31.md`), `dead_functions` (functions whose name appears nowhere else in either repository). `bpl-tests time_cases '+/v' --threads 4`: on four threads. `time_cases`, `instructions`, `speed_costs` take positional cases; put them after `--` when one starts with `-` (`bpl-tests speed_costs -- '-.×m' '+/v'`). `bpl-tests NAME -h`: arguments. In a kernel they return Python objects (e.g. rows for `compare_costs`), except `wasm_test`, which prints each completed batch and returns nothing. @@ -196,7 +210,7 @@ Features: basedpl's crates pass on rustymath's default `native` feature (rustyma Dependencies: published ranges, with the lower bounds the APIs in use require. `Cargo.toml` names a published rustymath range; the `~/aai-ws` Cargo patch selects the sibling checkout locally; CI resolves published crates → release rustymath changes before basedpl CI relies on them. Don't edit fastws-generated Cargo patches, `.git/fastws-cargo-key` or workspace configuration; don't commit the workspace-generated `Cargo.lock`. `meta/`: ignored planning material, not contributor documentation. `[tool.uv] cache-keys` tracks packaging manifests, not Rust sources. -CI: `Test` runs correctness checks on pushes to main and pull requests. Native checks include editable installation, `cargo develop`, `cargo nextest run --cargo-profile release --profile untimed`, pytest, `bpl-nb` and `nbdev-test`; Python tests use the installed `release` build. Wasm checks build with `cargo wasm --strip` and run `node --test tests/wasm.mjs`. Native and wasm jobs use `ubicloud-standard-4-ubuntu-2604`. A lockfile is generated before restoring `Swatinem/rust-cache` so dependency versions participate in its key. The `untimed` profile in `.config/nextest.toml` leaves out timed tests; CI runs no timings or release-cost logging. Reference deadlines and execution watchdogs remain enabled. +CI: `Test` runs correctness checks on pushes to main and pull requests. Native checks include editable installation, `cargo develop`, `cargo nextest run --cargo-profile release -E 'not binary(speed)'`, pytest, `bpl-nb` and `nbdev-test`; Python tests use the installed `release` build. Wasm checks build with `cargo wasm --strip` and run `node --test tests/wasm.mjs`. Native and wasm jobs use `ubicloud-standard-4-ubuntu-2604`. A lockfile is generated before restoring `Swatinem/rust-cache` so dependency versions participate in its key. The nextest filter excludes the speed binary, including its timing and count tests. CI runs no timing calibration or release-cost logging. Reference deadlines and execution watchdogs remain enabled. `Release`: on `v*` tags, separately, on GitHub's runners. Builds dist wheels, sdist, standalone CLI artifacts and the Windows keyboard archive; publishes Python artifacts and the GitHub release. npm job: publishes the stripped browser package by trusted publishing; purges jsDelivr's package files (stale unversioned URLs). Runs no tests; doesn't wait for `Test`. @@ -206,6 +220,8 @@ CI: `Test` runs correctness checks on pushes to main and pull requests. Native c For shorter compile feedback, use `cargo check --all-targets --profile quick` or `cargo develop --profile quick`. The optional `quick` profile keeps O3 and incremental compilation but disables LTO. Its cache is separate in `target/quick/`. Default development remains `release`; use the existing release or dist profile for performance comparisons. +Syd1’s local shared-build Cargo correction, exact source/tool pin and rollback are in `meta/compile-time-next.md`. Its absolute host settings stay local; upstream Cargo remains installed. + The project Cargo configs enable cross-crate generic sharing with `-Zshare-generics=yes`. They set `RUSTC_BOOTSTRAP=1` for Cargo's compiler processes, without changing the shell or requiring nightly. The tested toolchain is rustc 1.99.1 (`58cf8f9c35b355199727627d07ae34a4e2846d67`), LLVM 23.1.1. Keep the existing Rust minimum and toolchain-selection policy. The unstable switch should be checked when upgrading the compiler. The `dist` profile owns authoritative x86 speed gates: O3, explicit cross-crate `lto = "thin"`, 16 codegen units, nonincremental. Routine `release`/`test` stays local ThinLTO. Run `cargo test --profile dist --test speed` in either repo; use `ship-rs-build --profile dist` for the Python wheel and run its existing speed test; build Node budgets with `cargo wasm --profile dist --out DIR`, then `speed_costs(wasm=DIR)`. Syd1 targets were recalibrated from actual BPL v0.1.82 (shipped rustymath v0.1.53) and standalone rustymath v0.1.54 with this profile and the existing budget multipliers; macOS targets and holds are unchanged. LTO changes runtime code as well as build time. Keep the profile, optimization level, features, target, compiler and LLVM version fixed within a comparison. Rustymath's DEV.md "Loops and codegen" covers runtime inlining and vectorization. @@ -284,12 +300,11 @@ A small repro must retain the concrete types, widths, generic caller chain, inli | `array.rs`, `array/storage.rs`, `array/text.rs` | values, layouts, compact and mixed storage, gathering, source formatting | | `agreement.rs`, `keyed.rs`, `selection.rs` | rustymath's broadcasting `Agreement` with basedpl's layouts and their renderer; reading paired items; named and keyed alignment; selection, writeback | | `primitive.rs`, `primitive/` | primitive metadata, ranks, axes, inverses, array operations. Structural glyphs read their arguments, take each result's layout and `Steps` from a plan in rustymath's `structure`, copy by it | -| `number.rs`, `element.rs`, `pervasive.rs`, `fusion.rs`, `explain.rs` | numeric domains; item operations; compact kernels; dispatch's gate (`fusion::gate`, the cost rule `waits`), nodes for copies, joins, selects and conversions, forcing (`resolved`); events `•explain` records; live nodes `•memory` counts | -| `graph.rs` | the one lazy graph: interned nodes (`graph::Node`: a step, operands read through rustymath `Mapping`s, a `Desc`), their work, forcing a node through a `program::Program` and keeping its result | -| `derivative.rs`, `linear.rs`, `optimize.rs` | `∂`: duals, tags, seeding, rustymath's partials through rows, operator rules, loops' JVP and transpose; the transpose and substitution passes over the graph's linear nodes; `⇄` through `rustymath::optimize` | +| `number.rs`, `element.rs`, `pervasive.rs`, `fusion.rs`, `explain.rs` | numeric domains, item operations and compact kernels; eligible pending calls/copies, mapped reads and forcing through `Program`; events reported by `•explain` | +| `derivative.rs`, `derivative/tape.rs`, `optimize.rs` | `∂`: tagged duals, seeding and rules using rustymath partials; linear calls recorded into a shared `Program`, its transpose and forward evaluation; `⇄` through `rustymath::optimize` | | `transform.rs` | core shared by running transformations: one stack of levels; routing to the newest level among a call's arguments; the read point for code that isn't a call | | `batch.rs` | batched maps: the batch carrier; `each_pair`'s try, calling the operand of an Each, a Rank or an outer product once on batches of cells while the `batch` pref is on (default); batch rules routing hands calls to. `tests/reference.rs` runs every reference case with it off and on; `bpl-tests check_page PAGE --unbatched` checks a page with it off | -| `plan.rs`, `program.rs`, `program/`, `jit/src/lib.rs` | plans giving a call's result kind and layout from its arguments' (`plan.rs`); `Program`, `Step`, `Desc`, `Key` and the program cache: `Programs` on a dfn's definition, `program::tacit` by structure for other functions (`program.rs`); real-run traces, recordings and planning runs (`program/trace.rs`); replay and step execution (`program/run.rs`); the rule table (`program/rules.rs`) and contraction's builders (`program/contraction.rs`); block stages running elementwise steps a block of items at a time (`program/blocks.rs`), on compiled kernels (`jit/src/lib.rs`) where compiling repays, else the interpreter's kernels; storage orders (`program/layout.rs`); contract and range checks in reference runs (`program/verify.rs`) | +| `plan.rs`, `program.rs`, `program/`, `jit/src/lib.rs` | call kind/layout plans; shared `Program`, `Node`, `Step` and plan cache; symbolic recording and function-owned outcomes (`program/trace.rs`); replay (`program/run.rs`); contraction recognition and storage ordering; cache-sized native/compiled block stages; reference contract verification | | `search.rs`, `number_theory.rs`, `polynomial.rs`, `distribution.rs` | array-level adapters to rustymath | | `system.rs`, `data.rs`, `files.rs`, `date.rs`, `regex.rs` | system-function metadata, native functions, options, import rules; paths and the filesystem; `•date`; `•r`, `•replace` | | `csv.rs`, `json.rs`, `xml.rs`, `image.rs`, `plot.rs`, `canvas.rs`, `display.rs` | formats, rich values, rendering | @@ -393,7 +408,7 @@ Form rewriter (`eval/rewrite.rs`): the one home of fast combinations. Joining compact arrays widens Boolean → integer → float → complex; float widths join at the wider. Item-by-item construction keeps exact and approximate kinds apart, staying mixed when needed; only approximate real and complex items widen together. A write into an existing array follows the items rule (`Widening::Items`), as a literal of the same items would: exact and approximate numbers stay apart, as do Booleans and other numbers. Rearranging or combining mixed storage finishes in `Gather::finish`: compact where one dtype holds the items under the items rule; its scan compares each item's variant with the first's → a copy that stays mixed pays little. An in-place write into mixed storage keeps it mixed (a scan per write makes a loop of writes quadratic). Fresh computations choose storage from their result path. Results computed one cell at a time (Each, Rank, Key, power histories, polynomial and number-theory functions) assemble as items via `Value::assemble`; mix and catenation join arrays. Keep whole-array joining and element assembly distinct. -`element::Converts`: the one rule converting a compact item to another numeric type; size marks, `•float`, and programs' `Convert` steps in replays and block stages use it. Float target: reads the item as rustymath's `Source` does. Integer target: Boolean → 0 or 1; integer wraps to the width; float → floor within the float width's tolerance, then saturates at the target's range. `Value::converted`: plain values. `derivative::converted`: carried values, through the carrier (`Carrier::cast`); on a pending array, one more pending call (`fusion::convert`). +Rustymath's `convert::Conversion` owns compact numeric conversion. Size marks and `•float` use its `Buffer::{floored,cast_results}` entries; programs' `Convert` steps use it directly in replays and block stages. Float targets read through rustymath's `Source`. Integer targets take Boolean as 0 or 1, wrap integers to the width, and floor floats within the input width's tolerance before saturating at the target's range. Scalar `Number::to_fixed` uses rustymath's `convert::{floored,saturated}`; BPL keeps exact fractions' whole-number wrapping and exact floor. BPL chooses ordinary storage widths through `array::ordinary_width_for`, retaining each caller's empty, wrapping and known-range rules. `Value::converted` retains layout and fixed/weak metadata for plain values. `derivative::converted` handles carried values through `Carrier::cast`; a pending array adds a pending `fusion::convert` call. Bare numbers, empty literals, NaN: the parsed float width. Top-level `evaluate_code` validates the whole syntax, runs leading literal-record `•prefs` statements, then parses the rest at the new width; a syntax error prevents header execution. The default width also covers imported numbers, random data, and float results from exact operands. Time, date and metadata values: f64. Formatting marks non-default widths; serialization marks every float (width-independent readback). @@ -404,10 +419,10 @@ Floats: IEEE NaN, infinity, signed zero. Comparison: NaN unequal; match and sear Static kinds (`•prefs ["kinds":"static"]`): a result's kind follows from its arguments' kinds alone (`meta/kinds.md`). - `Numeric` carries `kinds` and `int`. Until the `float` pref sets a width, `Numeric::set` gives new floats the kinds' width: 32 bits static, 64 dynamic (`Kinds::float_width`). Whether the pref set one: `settings::float_set`, outside `Numeric` (every call builds and copies a `Numeric`; a Boolean field there cost about 14 instructions per primitive call, 34 per dfn call). -- A pervasive call on arrays plans before computing: `plan::pervasive` builds a `plan::Pervasive` from operand kinds (`plan::Kind`: type, fixed width, weakness) and settings, reading no items. It holds the settings with the operands' fixed width (`Numeric::for_kinds`), whether 16-bit floats compute in 32 bits, the computing kind (`computes_in`: rustymath's `DType::common` after `Numeric::kind`), its kernel table (`computes`) and, under static kinds, every result's kind (`Pervasive::result`). Eager dispatch, fusion's deferral (`Pervasive::fuses`) and typed programs read the same plan. +- A pervasive call on arrays plans before computing: `plan::pervasive` builds a `plan::Pervasive` from operand kinds (`plan::Kind`: type, fixed width, weakness), available scalar constants, ranges and settings, without reading array items. It holds the settings with the operands' fixed width (`Numeric::for_kinds`), whether 16-bit floats compute in 32 bits, the computing kind (`computes_in`: rustymath's `DType::common` after `Numeric::kind`), its kernel table (`computes`) and the observed scalar facts. Its `result_desc` owns the settled result kind and range. Eager dispatch, fusion's deferral (`Pervasive::fuses`), typed publication and verification consume this plan rather than reconstruct its result from a normalized operand. - Products and folds plan likewise: `plan::Product` holds `g`'s pervasive plan and the settings `f` folds its results under (`Product::folds`); `plan::Fold` a fold's or scan's settings and operand kinds. Eager products, folds and scans read them, as typed programs do. - Comparisons, `⌈ ⌊` and bitwise functions compute at their arguments' storage width, where results are the same at every width (`pervasive::first_kind`). A comparison of integers with a whole float standing for an integer computes on integers via `Pervasive::computes`; so do a residue by such a float and a residue scan from one, whose results then convert to floats (`plan::whole_float`). -- Weak (`Number::is_weak`): literals, values computed only from literals, Python's scalar arguments. Before a call, `Primitive::weakened` converts a weak number beside a strong one to the width rustymath's `DType::common_weak` gives, or to a fixed integer's width where it fits. Weak integers have no fixed width; NEP 50 decides theirs. +- Weak (`Number::is_weak`): literals, values computed only from literals, Python's scalar arguments. Before a call, `Primitive::weakened` converts a weak number beside a strong one to the width rustymath's `DType::common_weak` gives, or to a fixed integer's width where it fits. A known weak integer retains its actual minimum-width class in `plan::Kind`; a computed weak integer's provisional dtype does not prove that class. Shared promotion/join keeps this uncertainty unknown until the value is known, rather than promising that it fits a fixed width. This is a planning fact, not a caller admission rule. - Loops keep the failed results `keeps` names: floats' NaNs, wrapped integers of objects reporting `WRAPS`. Kernel tables choose float `⌊ ⌈ ×` and wrapping negation from `run.numeric()`. `Stored::build_at` keeps complex results complex. - `Primitive::number` gives every pervasive result on numbers and each step of a fold of numbers; under static kinds, `statically` converts it to the plan's kind. A static call on ordinary integers that an integer kernel takes runs it at the `int` width, as fixed integers run at theirs (`Primitive::integer_kernel`). Under dynamic kinds a call on two numbers stays outside plans: its result type depends on their values. - `Number::wrapped`, `Value::wrapped`: wrap an exact integer result to the integer width: the fixed width `Numeric::for_kinds` finds among the operands, or `int` under static kinds. Under static kinds a strong ordinary integer is a fixed integer of the width it computes at, `int` joined with its storage width (`plan::Kind::stored`) → every static call on integer arrays computes under a fixed width. Display marks a fixed integer of `int` width with plain `ₓ` (`Mark::integer`). @@ -451,9 +466,9 @@ A failed compact kernel leaves placeholder results and reruns the whole call on Fusion (`fusion.rs`; its module doc owns eligibility rules and limits: don't copy its thresholds here): -- Calls a block stage can run (`plan::Pervasive::fuses`) defer: `Primitive::pervasive_mapped` returns a pending array (`Data::Node` holding a `fusion::Pending`): a typed `Program` of its one step, plan fixed at call time, plus the arrays and atoms it reads, each pending one in its own value (`fusion::held`). Pending arrays survive assignment and link to the pending arrays they read. Node-making code stays out of line from dispatch: inlined, it grows the frame kept on the stack at every recursion level. -- A read forces via `ArrayData::storage`: gathers in the programs of pending arrays it reads in order (`Pending::gathered`), runs the result through its plan's units. `program::planned` plans each program structure once, from a copy with inputs for held arrays, reusing the plan for later forces of the same chain on new arrays. Shape, kind and prototype readers (`Value::len`, `Value::environment`) don't force. `Value::force` forces, as timing does. -- Fusion and dfn traces both add nodes via `program::add`, which returns an earlier node making the same value: a held atom of the same identity, the same held array, or the same step on the same values. +- Calls a block stage can run (`plan::Pervasive::fuses`) defer: `Primitive::pervasive_mapped` builds a typed `Program` and returns a pending array (`Data::Node` holding a `fusion::Pending`). Its plan is fixed at call time; each pending operand has its own held value (`fusion::held`). Every eligible deferred call uses `pending_program`, the shared owner of step-budget and input-retention accounting. There is no separate single-call package or retention path. Pending arrays survive assignment and link to the pending arrays they read. Node-making code stays out of line from dispatch: inlined, it grows the frame kept on the stack at every recursion level. +- A read forces via `ArrayData::storage`: gathers in the programs of pending arrays it reads in order (`Pending::gathered`), then runs the result through the shared executor. `run::lone` runs a single normalized Map with direct copy reads through the same `Primitive::planned` native call, without block-by-block re-entry. Explicit `Compiles::Always` requests for copy-bearing Maps stay with Blocks. Other programs use their plan's units. `program::planned` plans each program structure once, from a copy with inputs for held arrays, reusing the plan for later forces of the same chain on new arrays. Shape, kind and prototype readers (`Value::len`, `Value::environment`) don't force. `Value::force` forces, as timing does. +- Fusion and dfn traces both add nodes via `program::add`, which returns an earlier node making the same value: a held atom of the same identity, the same held array, or the same step on the same values. Tracing collects symbolic inputs under its own policy; sharing `Program` does not make every ordinary call a trace. - A pending array read in order that a name or another array also holds joins as an output (`Program::outputs`), computed in the same pass; a block stage stores its items with the result's. Other pending arrays the program reads but doesn't compute as steps compute first, alone (`Pending::gathered`): those not readable in order, and held copies. A fold of a held pending result stores that result as an output of the fold's pass, once for all readers (`fusion::fold`); a pending copy folds so, held or not. - A block stage runs elementwise steps block by block, in parts (`blocks::Part`): each run of steps lowering to machine operations that give their kernels' exact results (`blocks::lowered_op`) through one compiled kernel (`jit/src/lib.rs`); other steps through the same pervasive kernels (`pervasive::node`). A part whose kernel can't take a run's items (e.g. an input a broadcast reads as one item) runs those kernels for that run. A stage compiles all parts or none: where the same kernel compiled or ran before, or where one run of its items repays compiling (`blocks::Blocks::compiles`, `repays`). `settings::Compiles` can compile every stage or none: the reference harness; timings via `BASEDPL_COMPILE`. Deferral uses the result-count threshold `Sizes::min_items` and the call's eligibility (`fusion::defers`). - Each step writes a block to a buffer later steps reuse once its readers ran. A comparison read only by later steps of the stage writes floats 1 and 0 there (`Dyad::compared`, rustymath's `tolerant::Indicator`). `⍚` and `@[p]` run cases that can't fail on every item, selecting by a Boolean mask (`selected`, `eval/operators/agenda.rs`); `plan::select` gives its type and the maps its mask and cases read through; `fusion::select` defers it where the mask is such a comparison, else runs it at once via rustymath's `operation::choose` (`fusion::chosen`). A one-step stage runs only where it reads through a copy, recording no `fused` row. @@ -482,10 +497,10 @@ Fusion (`fusion.rs`; its module doc owns eligibility rules and limits: don't cop Typed programs (`program.rs`, `program/`), run by pending work and `∂`: - `∂` of a dfn runs one program holding its whole call, value and derivative (`derivative::differentiate`). The first call with given argument types, layouts and settings traces it (`program/trace.rs`): the dfn runs on traced values (type and layout, no items); each call plans its result (`plan`) and adds a step via `program::add`. Where the trace stops (`nbs/autodiff.qmd`, "What stops a trace"), the call runs in the interpreter. -- `Programs` keeps each trace's outcome, program or stop, on a dfn's `Definition` or another function's structure (`program::Tacit`, at most `STRUCTURES`). An outcome serves later calls with the same argument signature whose free values still hold what the trace read (`Entry::reread`) → cycling free values replay. Free values changing every call stop tracing after `CHANGES` traces in a row; a function keeps at most `KEPT` outcomes. -- `program::call` replays the program on the call's values; `program/run.rs` replays step by step, the reference compiled code matches. Fusion's and traced programs share this code. +- `Programs` keeps each trace's outcome, program or stop, on a dfn's `Definition` or another function's structure (`program::Tacit`, at most `STRUCTURES`). An outcome serves later calls with the same argument signature whose free values still hold what the trace read (`Entry::reread`) → cycling free values replay. Observed capture equality includes the canonical kind-class, not just numeric equality: a changed integer/float or weak-width class can change later promotion. `MAX_UNREUSED_BUILDS` consecutive unreused recording attempts stop further tracing; any valid cache hit resets the newest matching signature's counter. A function keeps at most `KEPT` outcomes. +- `program::call` replays the program on the call's values; `program/run.rs` replays step by step, the reference compiled code matches. Deferred and traced programs share this code. `BodyCaller` in `eval/operators.rs` serves intermediate Power calls for a single finite count, `each_pair`'s per-cell fallback for Each, Rank and outer products, and `fold_array`'s generic fold/scan fallback. Eligible dense-array bodies reuse recordings under the existing exact guards. Valid hits apply immediately; new builds wait for consecutive matching keys within the operator invocation. Unsupported or ineligible bodies use ordinary calls. Power's final iteration keeps normal `call_with` binding; count arrays, histories and until forms are unchanged. Producer-reduction goals remain open in the active plan. - Index notation and its gradients: `Step::Gather` selects by computed positions; `Step::At` writes or accumulates where a map or selector says; `Step::OverlapAdd` adds a windows copy's cotangent back to its source; `Step::Picks` marks each lane's `⌈`/`⌊` fold pick. Level 2: rustymath's selection planning (`structure::joined`) reads an index array with affine positions through strides, not an offset table (`Table::affine`). -- `program/contraction.rs`: a `+` fold of a `×` whose operands read sources through affine maps runs as one product (`contracted`); a fold of a copy repeating items as a fold of its source (`fold_of_copy`). `program/layout.rs`: stores maps in the order operands and readers need (`layout::ordered`). `blocks::plan`: finds runs of elementwise steps for block stages. `program/verify.rs`: checks each program's contracts before it runs, in every reference case and page (`settings::verifies`). +- `program/contraction.rs`: a `+` fold of a `×` whose operands read sources through affine maps runs as one product (`contracted`); a fold of a copy repeating items as a fold of its source (`fold_of_copy`). `program/layout.rs`: stores maps in the order operands and readers need (`layout::ordered`). `blocks::plan`: finds runs of elementwise steps for block stages. `program/verify.rs` checks each program's contracts before it runs, in every reference case and page (`settings::verifies`). For Map, `plan::check_map` checks the compatible available operand facts before settling the declared plan's result. Normalization can hide a previously observed scalar constant; verification retains that declared observation and its derived computing kind, without inferring constants from opaque data. - `•explain` lists programs a call built or replayed, and stopped traces (`programs`, `stopped`, `declined`). `["sites":$t]•explain` fills `site` on `forced` and `stopped` rows: the innermost basedpl functions that computed the items or stopped the trace (`explain::site`, from a Rust backtrace, skipping the event's own bookkeeping, `BOOKKEEPING`). Use it to place a forced read or a trace stop, rather than a temporary backtrace. Batched maps (`batch.rs`): @@ -600,6 +615,7 @@ CSV columns, JSON arrays and Python lists share `data::imported`: a non-fill fin - Every reference case gets a fresh session; documentation examples share one per BPL code block, starting from default settings. Each page starts with no kept plans or compiled kernels, as a new process does (`program::forget_plans`, `jit::forget_kernels`) → an example showing block counts or compiling gives the same output alone (`check_page`) or after other pages in a `reference_cases` child. - A reference file's leading `•prefs` line sets all its cases' settings. Optional `⍝⍝ setup`: shared definitions, initialized separately for each run and its expectation. A section's inline `•prefs` suffix runs before setup. +- `testkit` starts `.bpl` reference runs and speed/count helpers with Mathish off. Files ending `_mathish.bpl` turn it on before their file/section preferences. Explicit preferences still apply last. Python and WASM measurement helpers use the same boundary. Documentation pages and notebooks keep the language default. - Each case runs with the `batch` pref on, then off, then with fusion's small sizes and every available block stage compiled, then the same with compilation disabled, and as the body of a dfn called twice. The batch-off run doesn't apply where the batch-on run never read the pref (`settings::batch_reads`), and the compilation-disabled run doesn't apply where the compiled run compiled nothing (`hooks::compiled_runs`): each would repeat the run before it. The second dfn call checks replay's value, storage kind, error and output. Cases that cannot be dfn bodies omit that run. `compiled` skips the compilation-disabled run. Shared setups that reference `test_compiled` receive that fixture, true in the forced-compiled run. Other reference sessions do not gain the name. Reporting tests use it to require actual `"compiled"` execution without changing the scheduler's mode. Capture each workload separately, including a warmed read that reuses code. `compiles` reports code generation, not execution. - Every run checks each program before it runs (`program::verify`): each step reads values before its own; each read lays out as many items as its node holds; each node's description is what the shared planners give its step on its operands. A breach panics with the program's listing. - Run the complete reference cohort with: from basedpl.bpltests import check_file; report = check_file("tests/reference"). The report contains every parsed case's applicable modes, actual failures, incomplete checks and coverage totals. Healthy single-native-thread workers are reused; fixture thread preferences override that default. A watchdog replaces its worker and continues unattempted independent modes and queued cases, never retries the stopped mode. Fresh continuations are labelled; replay needs a successful normal warmup, and cross-mode storage comparison stays blocked without all standard outcomes. report["complete"] means no expected check is missing, not that every result passed. diff --git a/nbs/target.qmd b/TARGET.md similarity index 99% rename from nbs/target.qmd rename to TARGET.md index ef3e9cbf..55b10daa 100644 --- a/nbs/target.qmd +++ b/TARGET.md @@ -1,5 +1,7 @@ # Target: the finished core +> Historical design from the one-graph experiment, retained for rationale and comparison rather than current requirements. [Computational foundations](nbs/foundations.qmd) describes the implemented eager/Program engine. + Measure for every step, diff and review in the one-graph release. Existing code supplies behavior and reusable algorithms, not a requirement to retain its architecture. Reuse sound algorithms in their single intended homes. This file owns the current design; `archive/one-graph-2026-10-31.md` keeps reasons, evidence and history. Goals, required behavior, acceptance criteria, budgets and explicit process rules are constraints. Parts, interfaces, algorithms and ownership describe the current working design. Continuously improve that design as implementation reveals simpler solutions, updating this document and affected consumers together. Ask Jeremy only when proceeding requires changing a constraint, information only he holds, or a substantive trade-off that cannot be resolved within the agreed priorities. A departure from the working design alone is not a blocker. diff --git a/cli/src/worker.rs b/cli/src/worker.rs index d79f71a9..3cfb407a 100644 --- a/cli/src/worker.rs +++ b/cli/src/worker.rs @@ -25,11 +25,9 @@ pub(crate) fn run(output: &mut impl Write) -> io::Result<()> { let session = Session::new(); for (request, interrupt) in receive { let id = request.as_ref().ok().and_then(|r| r["id"].as_u64()); - let result = request.and_then(|r| reply(&session, &r, interrupt)).unwrap_or_else(|message| Reply::Json(basedpl::protocol::request_error(&message))); + let result = request.and_then(|r| reply(&session, &r, interrupt, output)).unwrap_or_else(|message| Reply::Json(basedpl::protocol::request_error(&message))); if let Some(id) = id { active.lock().unwrap().remove(&id); } - serde_json::to_writer(&mut *output, &Line { id, result })?; - writeln!(output)?; - output.flush()?; + write_reply(output, id, result)?; } Ok(()) } @@ -56,13 +54,24 @@ impl serde::Serialize for Line { } } +fn write_reply(output: &mut impl Write, id: Option, result: Reply) -> io::Result<()> { + serde_json::to_writer(&mut *output, &Line { id, result })?; + writeln!(output)?; + output.flush() +} + /// The result of one request, or why it can't run. -fn reply(session: &Session, request: &Value, interrupt: InterruptHandle) -> Result { +fn reply(session: &Session, request: &Value, interrupt: InterruptHandle, output: &mut impl Write) -> Result { if request.get("id").is_some_and(|id| !id.is_u64()) { return Err("id must be a nonnegative integer".into()); } let options = EvalOptions { interrupt, ..basedpl::protocol::options(request)? }; if let Some(case) = request.get("case") { return Ok(Reply::Json(basedpl_testkit::reference::check(case, &options.interrupt))); } if let Some(spec) = request.get("timing") { return basedpl_testkit::timing::request(spec, &options.interrupt).map(Reply::Json); } if let Some(spec) = request.get("steps") { return basedpl_testkit::timing::steps_request(spec, &options.interrupt).map(Reply::Json); } + if let Some(spec) = request.get("profile") { + return basedpl_testkit::timing::profile_request(spec, &options.interrupt, || + write_reply(output, request["id"].as_u64(), Reply::Json(serde_json::json!({"ready":true,"error":null}))) + .map_err(|error| error.to_string())).map(Reply::Json); + } if let Some(spec) = request.get("costs") { return basedpl_testkit::timing::file_request(spec, &options.interrupt).map(Reply::Json); } if let Some(spec) = request.get("reference") { return basedpl_testkit::reference::file_request(spec, &options.interrupt).map(Reply::Json); } if let Some(spec) = request.get("page") { return basedpl_testkit::reference::page_request(spec, &options.interrupt, options.timeout).map(Reply::Json); } diff --git a/nbs/foundations.qmd b/nbs/foundations.qmd index 8e45dbd8..06310dd9 100644 --- a/nbs/foundations.qmd +++ b/nbs/foundations.qmd @@ -1,160 +1,111 @@ --- title: "Computational foundations" -description: "What BPL borrows from tinygrad, JAX, Dex and Futhark, and what owning the language makes possible" +description: "BPL's eager evaluator, deferred programs, batching and autodiff, and what owning the language makes possible" --- ## How the core fits together -Numerical calls from primitives, tacit functions and dfns build one lazy graph. An operation's row defines its language rules, input roles and result description. Rustymath owns the mathematical formulas, partial derivatives and shared numerical kernels. Each graph node retains the call's current inputs and settings. Captured data are call-time inputs rather than values hidden in cached code. +BPL evaluates functions through shared primitive calls. Basedpl owns language rules, scalar domains, function values and transformations. Rustymath owns numerical algorithms, array geometry and mathematical facts, including partial derivatives. A primitive's plan selects computing kinds, agreement and numerical settings before its kernel runs. Where input kinds determine the result kind, the plan records it too. Calls whose results need actual values run eagerly. Eligible array calls build a typed `Program` and return a pending value that retains it and its inputs. -A read computes the graph it needs through one program planner and cache. Rewrites and scheduling arrange operations into native kernel calls or fused block stages. Block stages can run without compilation or use JIT code when compiling repays its cost. A function recording reuses the same program on new inputs. Reusable plans keep computation structure and refill descriptions and read maps at the current lengths. +Reading a pending value gathers compatible programs. The planner groups elementwise calls and supported reductions into cache-sized block stages. A stage uses rustymath kernels or JIT-compiled arithmetic according to its execution policy. Deferred work and traced recordings use the same `Program`, steps and executor. Tracing is a separate collection policy on symbolic inputs, not an extra pass over every ordinary function call. Eligible ordinary Power bodies also reuse compatible recordings. -Differentiation and batching transform ordinary calls into this same graph. Differentiation uses real floats, promoting exact differentiated inputs through the normal float-width rules. Forward derivatives use the shared mathematical partials; reverse derivatives transpose ordinary linear operations, accumulating where several results read one input. Power and custom folds/scans expand actual function calls rather than introduce another numerical executor. Reads that decide program structure and ordered effects form explicit boundaries. This is the foundation under construction; [the target](target.qmd) defines its required behavior and costs. +Autodiff and batching intercept primitive calls through tagged transformation levels. Forward AD propagates primal values and tangents. Reverse AD records linear calls into a `Program` through its tape driver, then transposes those calls. Batching calls a function on many cells together where its operations support that transformation. Unsupported recording or batching returns to ordinary evaluation. ## Why owning the language helps -Python array libraries already run numerical kernels in native code. BPL's opportunity is in the work around those kernels. A BPL call needs no Python operator overload, array-object protocol or Python-to-native crossing in its core. We can give dispatch one entry, make its inputs small and measure the cost of every step. This matters most for short expressions, loops and the many small calls used to construct a larger calculation. +BPL controls parsing, function structure, numeric domains and array execution together. A glyph call, a tacit function and a dfn can reach the same primitive without a Python operator-overload protocol in between. Python is another frontend to these function values and arrays. Its bindings should call the BPL operation rather than implement a second selection, promotion or numerical rule. -BPL also knows how a function was made. A primitive, a train and an operator applied to its operands have structural representations. We can reuse a recorded calculation for closures of the same definition while passing their different captured arrays as inputs. The cache need not depend on the identity of a newly allocated callable. Captured data are snapshots from the current call, never stale data kept inside compiled code. JAX documents the recompilation cost of repeatedly constructing temporary functions. [JAX's function caching](https://github.com/jax-ml/jax/blob/main/docs/jit-compilation.md#jit-and-caching). +Function values describe how they were built. A train or bound function retains its operands; a dfn retains its definition and lexical environment. A recording can distinguish that structure from the current arrays it reads. Reusing construction need not mean retaining old captured data inside compiled code. Existing replay validates captures and binds the inputs it recorded. Changed assumptions require another recording or ordinary execution. [JAX's function caching](https://github.com/jax-ml/jax/blob/main/docs/jit-compilation.md#jit-and-caching) illustrates why callable identity and captured data need deliberate ownership. -BPL can also choose numerical domains that fit its execution model. Ordinary `ₓ` integers use machine arithmetic, including floor division, with dynamic widening within machine widths and wrapping at the maximum width. Unbounded integers and rationals belong to the explicit `ᵣ` domain. An ordinary integer calculation never needs to move to host BigInt arithmetic merely because its result grew or division produced a fraction. Dynamic kinds still support value-dependent real/complex results and within-machine promotion. This distinction removes automatic unbounded-promotion machinery without removing dynamic types. [Numbers](numbers.qmd) gives the language rules. - - -An ordinary weak literal retained as a keyed constant contributes its minimum width when promotion selects the fixed-integer domain. `1ₓ₁+1ₓ` stays byte-sized; `1ₓ₁+256ₓ` widens instead of failing admission. Names, captured data and formal arguments use their declared kind bounds, never observed extrema, even when the caller writes a literal. Pure ordinary static arrays keep their ordinary width. - -Lazy arrays preserve the calculation that produces them across function calls. A transpose, a selection or a window can remain a description of reads until its consumer needs items. Both the BPL frontend and rustymath's numerical kernels can use that description. A single runtime can then coordinate graph construction, transformations, planning and code reuse instead of translating between independently maintained execution routes. +The language can also keep numerical domains useful and simple. Ordinary `ₓ` integers use machine arithmetic and integer floor division. Dynamic results widen within machine widths, then wrap at the maximum width. Unbounded integers and fractions use the explicit `ᵣ` domain, retained even at denominator 1. Ordinary arithmetic does not promote to BigInt because a result grew or division produced a fraction. This removes an execution-domain change without removing dynamic kinds. [Numbers](numbers.qmd) owns the full rules, including weak literals and symbolic infinities. +Array operators are the performance target. Each, Rank, Power, broadcasts, folds, scans, Agenda and meaningful differentiated uses expose repeated or whole-array work to the runtime. Scalar interactive latency, Fibonacci and guarded-dfn throughput are not reasons to complicate the foundation. Removing repeated interpretation or setup inside an analytical operator matters; shortening an unrelated top-level call does not establish that benefit. ## Known kinds and fast forms -Fast calculation uses static kinds or explicit approximate forms whose result kinds follow from the arguments without reading array items. `√ₐ`, `*ₐ` and `⍟ₐ` have this contract, as do `○ₐ` and `○⁻ₐ` with a constant code. The [approximate forms](numbers.qmd#approximate) select real float results at the arguments' promoted width. Outside the real domain they return the real part of the principal value. +Static kinds determine result kinds from argument kinds. Explicit approximate forms provide fast real-valued operations where an unmarked dynamic operation can choose real or complex results from its data. `√ₐ`, `*ₐ`, `⍟ₐ`, `○ₐ` and `○⁻ₐ` select the real part of the principal value at the promoted float width. [Numbers](numbers.qmd) describes their domains and conversions. -Known-complex arithmetic retains its declared complex kind without scanning the result's imaginary parts. [Numbers](numbers.qmd#float-widths) describes real-valued functions and the explicit real-part conversion. +The value-dependent dynamic forms of these operations have no speed, fusion or recording requirement. They remain useful for interactive analysis. An already known domain can make an individual call eligible, but the runtime needs no extra value-specialization framework to accelerate the excluded cases. Dfn guards that read traced values also run correctly outside the tracing contract. Ordinary dynamic work otherwise aims to cost at most four times the corresponding static calculation. -Symbolic `∞` and `¯∞` have singleton logical kinds backed by ordinary float storage. Order-only operations can preserve an exact operand or decide a comparison from those kinds without reading array items. The existing kind key distinguishes both signs from finite inputs and ordinary floating infinities. [Numbers](numbers.qmd#infinities-and-nan) gives the language rules. +Ordinary dynamic integer widening can likewise prevent a result kind being fixed before execution. A fixed-width or static calculation can use a typed fused stage. An eager call can rerun a kernel at a wider width when needed. Both use the same language kind rules and rustymath's arithmetic. A compiled stage's declared input and output types still have to agree with its actual buffers; shared construction alone does not enforce that contract. -The value-dependent dynamic forms of `√`, `*`, `⍟` and `○` have no speed, fusion or recording requirement. They remain useful for interactive work, including results that become complex according to input values. Use the explicit fast forms for these calculations. Dfn guards on traced values also remain outside the fast contract. Ordinary dynamic interactive work, including tacit code and simple dfns, still aims to cost at most four times the same static calculation. +Known complex storage remains complex without scanning results for zero imaginary parts. Explicit real-part conversion is available when wanted. An outer conversion of an arbitrary function's result does not authorize replacing every complex intermediate with a real one. -The boundary follows whether ordinary planning needs numeric values. It is not a list of slow glyphs. Known kinds or proofs can already make a dynamic call fast. A body using an approximate operation can expose that operation normally. An outer result conversion does not permit changing every complex intermediate in an arbitrary body. No pre-kind IR or additional value-specialization machinery is needed to accelerate the excluded forms. +## Deferred execution and native kernels -## Calls build a graph; reads compute +A pending array retains a `Program`, its result description, captured numeric settings and the inputs needed to compute its items. Eligible deferred calls use `fusion::pending_program` for shared step-budget and input-retention accounting. Shape and prototype queries need no item read. Eligible calls can remain pending across assignment and function returns. Calls outside the deferred subset execute normally. -A graph records operations and their dependencies. A pending array holds its producer node instead of an intermediate buffer. For example, suppose `x` is a real float vector: +For a suitable real float vector `x`, `loss←+/⊼ ⋄ y←loss x` squares the items and sums them. A pending square can feed the reduction without storing a whole vector of squares. Where another value also needs that vector, the same pass can retain it as an output. A small selection can compute only selected work when its mapping and producer support that read. Other readers use the pending value's cached stored result. The selection relationship and the computed buffer are different facts. -````bpl -loss←+/⊼ -y←loss x -```` +Forcing gathers pending programs. A single step over input or held values can run through the normal step executor directly. Block scheduling prepares input geometry for grouped stages. A stage with one native operation and no intermediates calls its kernel once per assigned range. Multi-operation native stages keep intermediates in cache-sized blocks. An always-compile request can select compiled execution even for one operation. Native parts call rustymath kernels. Compiled parts run arithmetic steps in one generated loop and can keep intermediates in registers. The JIT improves some fused chains, not every numerical operation. Matrix products and substantial power/root algorithms already have whole-array native implementations. -[`⊼`](glyphs/wedge-bar.qmd) squares each item. [`+/`](glyphs/slash.qmd) sums the squares. Calling `loss` builds those operations without first storing a vector of squares. Displaying `y` reads its value. The runtime then plans the reachable graph and computes the total, with the square inside the reduction where the execution rules support it. +Structural operations describe how a result reads its sources through rustymath mappings. Consumers can read transposes, windows and selections through those maps instead of first making dense copies. Correct geometry alone does not ensure efficient execution: preserve mapped reads until the bulk executor chooses their traversal rather than splitting them early. Reading in place is not always cheapest; a copied or converted chunk can enable a faster kernel. -We borrow lazy graph construction and node interning from tinygrad. Interning gives structurally identical live nodes one representation. tinygrad separates Tensor construction, scheduling and `realize`, and replaces realized producers with storage. Its compiler caches generated programs by a structural key. BPL uses the same separation to retain producer information before a read and release it after storing the result. [tinygrad's `UOpMetaClass`](https://github.com/tinygrad/tinygrad/blob/master/tinygrad/uop/ops.py), [Tensor realization](https://github.com/tinygrad/tinygrad/blob/master/tinygrad/tensor.py), [compilation and execution](https://github.com/tinygrad/tinygrad/blob/master/tinygrad/engine/realize.py). +The planner already groups some elementwise producers with a terminal fold. Geometry and stage limits determine whether the compiled loop accumulates directly or writes a cache-sized block for rustymath to fold. This does not imply every indexed reduction is one accumulator loop, or that a full broadcast grid is always materialized. [Futhark's fusion and views](https://github.com/diku-dk/futhark/blob/master/docs/performance.rst) supplies the producer-consumer model. BPL's named index arrays preserve axes for agreement and contraction recognition. -A plan decides operation order, which intermediate results need storage and which operations run together. Fusion means computing a producer's items inside its consumer rather than storing and rereading an array between them. Futhark performs producer-consumer and shared-input fusion over a dependency graph, even when the source expressions are separated by names or functions. BPL's graph gives its rewrite rules that same view. [Futhark's fusion and views](https://github.com/diku-dk/futhark/blob/master/docs/performance.rst). +[tinygrad's realization and execution](https://github.com/tinygrad/tinygrad/blob/master/tinygrad/engine/realize.py) and [TinyJit](https://github.com/tinygrad/tinygrad/blob/master/tinygrad/engine/jit.py) are references for keeping construction, execution and code reuse distinct. BPL retains an eager base rather than requiring every call to enter a lazy graph. -A trace records the graph built by an actual run. A replay binds new inputs to the recorded calculation and runs its plan. tinygrad's TinyJit demonstrates recording executed work and replaying it without rerunning the Python function. BPL records the graph that ordinary calls already build. A trace is therefore a way to reuse construction work, not another interpretation of numerical operations. Ordinary graph reads and recorded calls use the same planning pipeline. [TinyJit capture and replay](https://github.com/tinygrad/tinygrad/blob/master/tinygrad/engine/jit.py). +## Recordings and replay validity +A traced value describes a kind and layout without providing its array items. Supported calls add typed steps. A successful recording executes through the same planner and executor used by deferred work. A later compatible call binds current inputs without interpreting the function body again. Recording outcomes belong to the function's program cache; compiled machine code belongs to the JIT code table. -## Function structure and operation facts +Built recordings currently require exact argument and captured layouts, alongside numeric and execution-size settings, kinds and other recorded assumptions. A length change retraces. Stopped outcomes can reuse length classes `0`, `1` and `other`; that does not mean a built program can run at any length in the class. Kernels already take concrete lengths at runtime. Reusing a program at different lengths also requires its descriptions, maps, dimensions and result layouts to bind current lengths. Guarding a length is not rebinding it. -With the same arguments and numeric context, `(1ₓ₁+)`, `1ₓ₁↣+` and `{1ₓ₁+⍵}` build the same addition with a literal left operand. Different function templates and capture layouts do not require different numerical rules. Literal nodes retain their values in the graph. Arguments and captured data enter as inputs. Promotion, derivatives and kernels belong to the resulting operation. +Opaque data inputs supply kind bounds rather than narrower numeric contracts inferred from observed extrema. Keyed literals and facts derived from operations can support proofs inside a recording. A pervasive plan owns observed scalar constants and the result kind and range derived from them. Stored descriptions include their dtype bounds; mathematical range predictions can exceed a provisional type to support checked widening. Extracting a representative scalar preserves the application’s original array/atom and strong-kind context. Verification checks the declared plan: normalization hiding an observed constant does not erase that proof, and exposing another constant does not require a new specialization. A shape query can be represented as an operation, but a count that changes how many body calls are unrolled changes the recorded structure. Runtime kernel lengths and structural iteration counts are different responsibilities. -Every numeric token is interpreted when its containing statement executes under the active numeric settings. A bound operand is a value evaluated during construction. A dfn's body statement executes when called. Changing preferences between those events can produce different literal types. This is one statement-execution rule. Free names read at a call and values stored in a bound function also have genuinely different binding times. +A known weak integer keeps the minimum width that holds its value. A computed weak integer's provisional dtype is not proof of that width; promotion keeps the uncertainty instead of guessing that a narrow fixed type will fit. Replay likewise compares an observed capture's kind-class as well as its numeric value: equal numbers can imply different integer/float promotion. These facts belong to shared numeric planning and capture validation, not checks added by each caller. -Fold, product and marked-function consumers use one typed-body query. A declared primitive supplies its operation directly. For another function, the query reuses effect-safe planning and the existing recording/cache. It accepts an original recording that returns one native primitive call with unchanged output metadata, ordered formal inputs, identity read mappings and matching settings. Conversions, observed data-dependent control, effects, writes and custom derivative rules prevent substitution. +Recording stops before effects, random draws, unsupported observations and value-dependent kinds it cannot describe. The whole call then runs normally. Stopping before visible work avoids rollback or replaying effects. Reading a guard predicate from a traced argument is one such observation. Reads of ordinary eager values remain normal execution. [Autodiff](autodiff.qmd) owns the supported operations and trace-stop list. -An admitted body reuses the operation's existing numerical facts, including its reduction identity: `{⍺+⍵}/⍬` returns 0 like `+/⍬`. The query proves the operation for the current arity and cell descriptions, not general mathematical equivalence. Unsupported bodies retain ordinary execution and generic empty folds still require an identity. There is no second body IR, capability cache or numerical evaluator. +Eligible ordinary dense-array bodies can reuse compatible recordings in Power with a single finite count, per-cell map fallbacks and generic fold/scan fallbacks. Each, Rank and outer products try batching first. Replay binds current inputs and captures under the existing exact guards. Valid cache hits apply immediately. A miss waits for matching keys on consecutive calls within that operator invocation before building. Unsupported or ineligible bodies use ordinary calls. Power's count arrays, histories and until forms keep their existing call paths. Deferred calls and the tracer emit the same `Program` steps but collect concrete pending inputs and symbolic traced inputs under different policies. Sharing the representation does not require tracing every top-level call. Further consolidation has to remove responsibilities without losing first-call fusion, selective reads or safe replay. ## Transformations keep whole-array work -Batching applies one function to many cells by transforming its operations into array operations. A cell is the part of an array that the function receives at each position. BPL's Each, Rank and related operators use one batching mechanism instead of separate evaluators for each operator. - -Records and ordinary nested arrays preserve their structure while their numeric leaves use the same graph. Uniform results can become dense arrays. Ragged cells retain the released mixed-array behavior. A native segmented representation with shared leaves, offsets and shapes could accelerate those workloads, but would require new storage, metadata and transformation contracts. That capability is deferred until its benefit can be measured against the simpler nested representation. - -Stored host results use stored-value construction. Only compatible deferred native items need graph collection. A pending value changes its layout through a graph view, never a stored-only relayout. For a batched call, the outer frame axes identify the separate calls. Combining homogeneous nested items into a native array preserves those axes separately from each child's axes. Missing outer frame axes become leading unit-axis views; child axes must not take their place. - -JAX's Autodidax tutorial supplies the model for composing these transformations. In that presentation every primitive call goes through `bind`, which selects the relevant transformation interpreter from its arguments. A transformation can call primitives through the same entry. Older transformed values can be lifted into a newer transformation without losing their earlier meaning. BPL calls these nested interpreters levels. Its derivative and batching levels follow those data dependencies. [JAX's interpreter stack, lifting and batching](https://github.com/jax-ml/jax/blob/main/docs/autodidax.md#part-1-transformations-as-interpreters-standard-evaluation-jvp-and-vmap). - -For the example above: - -````bpl -grad←loss∂ -grad x -```` - -[`∂`](glyphs/partial.qmd) gives the gradient of a function with one number as its result. Here the gradient is twice each item of `x`. The original calculation is the primal computation. Forward differentiation propagates a tangent, the change in a result along a chosen input direction. Linearization separates that primal calculation from a calculation linear in the tangent. Reverse differentiation transposes the ordinary linear graph steps to propagate result sensitivities back to the inputs. Coefficients retain the intermediate values from the actual forward call; a recorded Description-only coefficient captures primal geometry, while merged or numerical uses retain their Dual and Batch values. BPL uses one graph and one set of mathematical partials for both modes, rather than a separate AD evaluator. [JAX's linearization and transposition](https://github.com/jax-ml/jax/blob/main/docs/autodidax.md#part-4-linearize-and-vjp-and-grad). - -Dex and Futhark show why preserving array structure through differentiation matters. Dex linearizes `for` bodies and transposes their structured loops. Its transpose uses indexed accumulations to send sensitivities back to selected array positions. Futhark differentiates maps, reductions and scans as bulk array operations, then allows fusion to run again. BPL follows that principle. The reverse of a repeated read accumulates at the source positions, without constructing a one-hot array or invoking an interpreter for every item. [Dex linearization](https://github.com/google-research/dex-lang/blob/main/src/lib/Linearize.hs), [Dex transposition](https://github.com/google-research/dex-lang/blob/main/src/lib/Transpose.hs), [Futhark's array differentiation](https://github.com/diku-dk/futhark/blob/master/src/Futhark/AD/Rev/SOAC.hs). - -Dex also shows the value of writing a formula for one item while retaining its array structure. BPL's [index notation](index-notation.qmd) follows that idea through named axes. Index formulas and whole-array spellings build operations that the same contraction and read-mapping rules can recognize. BPL uses arrays of named indices rather than Dex's statically typed index sets. [Dex's table comprehensions and typed indexing](https://github.com/google-research/dex-lang/blob/main/examples/tutorial.dx). - -The reverse derivative of a structural operation can often read from the sensitivity array instead of scattering into a new one. tinygrad's gradient rules reverse padding with a shrink and a shrink with padding. BPL similarly prefers read-based transposes when the mapping permits them, retaining accumulation where positions repeat. [tinygrad's movement gradients](https://github.com/tinygrad/tinygrad/blob/master/tinygrad/mixin/gradient.py). - -The transformed operations still enter the ordinary graph. Rewrite rules and fusion therefore apply to the gradient as well as the original function. Scalar mathematical facts have one home, including formulas, partial derivatives and range information. A rule can recognize a contraction or a stable mathematical form after batching and differentiation have exposed it. - -Agenda and conditional updates first choose masked execution or compaction using cached case descriptions. They then call the original functions and build ordinary graph operations before differentiation or batching. Masked cases gate active inputs and tangent directions before the call, protecting derivatives from inactive infinite slopes. Compaction gathers each case's selected cells, calls the function once on that whole group, and scatters its results. It does not implicitly map the function per cell. Both forms use the ordinary graph transpose, including read-back accumulation, while actual function calls preserve custom derivative rules and the forward computation's intermediate values. - -## Iteration starts with unrolled graphs - -Power (`⍣`) and generic folds/scans repeatedly call a function. The foundation reuses the existing host iteration algorithms and sends each actual body call through the ordinary function and graph machinery. Under differentiation those calls build an unrolled linear graph. Known reduction operations keep their dedicated numerical algorithms. Exposing those operations through equivalent function bodies is the shared-boundary work described above. Effects, control flow and unknown algebra still require ordered calls. Each normally batches cells rather than interpreting a loop over every numeric item. JAX similarly traces Python loops by unrolling them, while `lax.scan` explicitly requests a compact loop representation. [JAX's loop transformations](https://github.com/jax-ml/jax/blob/main/jax/_src/lax/control_flow/loops.py). - -Unrolling makes graph size grow with the executed work. The linear graph already identifies the operations taken and retains their required coefficients. It needs no separate carry/residual/history-count packet or reverse-loop interpreter. A count that decides how many calls are expanded is structural and keys its exact value. Data-dependent control follows the normal recording-stop boundary while the executed computation and its derivative remain correct. This differs from an array length that can refill an unchanged plan. - -Compact loop AD could keep graph construction proportional to body size and make long iteration easier to compile. Checkpointing could trade recomputation for less saved data. Those are later capabilities to add when a workload demonstrates their benefit. Dex and Futhark supply useful designs for them; they are not prerequisites for consolidating BPL's existing AD. The foundation retains nested AD, iteration histories, predicates, convergence and inverse iteration without introducing a compact loop IR. [Dex's residual handling](https://github.com/google-research/dex-lang/blob/main/src/lib/Linearize.hs), [Futhark's reverse loops](https://github.com/diku-dk/futhark/blob/master/src/Futhark/AD/Rev/Loop.hs). - -## Reuse structure as lengths change - -An array's exact length often changes the amount of work without changing the computation. A kernel can take that length as a runtime argument. Futhark's compiled libraries accept array dimensions at runtime. tinygrad also represents bounded symbolic sizes separately from their current values. BPL's kernels use runtime counts without requiring a symbolic shape language throughout the runtime. [Futhark's array API](https://github.com/diku-dk/futhark/blob/master/docs/c-api.rst), [tinygrad's variables](https://github.com/tinygrad/tinygrad/blob/master/tinygrad/uop/ops.py). - -BPL keys reusable plans by structure and the facts their validity needs. The length component distinguishes empty, unit and longer axes. Unit axes change broadcasting. Ranks, kinds, axis equalities and structural parameters also matter. Two longer vectors of different lengths can reuse a plan when those other facts agree. - -Execution computes a graph key once for cache lookup and insertion and propagates checked errors for invalid structural parameters. Description and AD walks do not compute graph keys. - -A reusable plan takes only intrinsic kind bounds from an opaque data input, even when its caller knows a narrower range. Forced graphs and recordings use this same rule. Literals retain their values in the key. Operations inside the graph can prove narrower ranges, such as nonnegativity after real-floating magnitude, and refill derives index/count bounds from current lengths. There are no numeric contracts on particular input values to guard, invalidate or retrace. A proof that is unavailable leaves the ordinary kind read, bounds check or numerical repair in place. The ordinary dynamic cost targets still apply outside the value-dependent forms described in [Known kinds and fast forms](#known-kinds-and-fast-forms). - -Refill updates concrete result descriptions and read mappings for the new lengths. It preserves the plan's rewrites, operation order and stage structure. Preparation uses actual counts for the few choices where size changes the preferred execution, including whether compiling is worthwhile, which direction to read a contraction and whether to recompute an intermediate. +Each and Rank try to apply a function once to a batch of cells. A cell is the part of an argument supplied to one mapped call. A successful batch transforms the body's operations into operations on whole arguments. Nested maps can combine frames; folds across many rows can call a dfn once per column rather than once per numeric item. -Futhark demonstrates choosing implementations at runtime from size checks. Its multicore scheduler selects between parallel and sequential nested work, and its GPU compiler generates versions for different amounts of parallelism. BPL borrows size-sensitive choice without adopting an ahead-of-time catalogue of versions. A just-in-time compiler emits a worthwhile structural version when execution first needs it and keeps that code in one cache. A new length does not automatically require another compilation. BPL uses no size buckets or padding to a bucket. [Futhark's runtime selection](https://github.com/diku-dk/futhark/blob/master/docs/performance.rst). +Ordinary nested and ragged arrays keep their existing representation. Uniform whole-array work can batch; an unsupported layout or result takes the normal per-cell path. Per-cell program replay and one-pass ragged batching are different optimizations. Neither requires a segmented storage format merely to preserve existing values. -Filtering and finding unique items determine a result's length from data. BPL schedules only the prefix before an unobserved count. After reading it, execution looks up the downstream suffix in the same cache under the count-extended key. A hit refills that suffix at actual lengths; a miss schedules it. Completed effects and draws stay in the same run state. A changed structural fact uses the normal cache-miss path. The read is an execution boundary, not a reason to reject the expression. JAX's jitted `unique` instead requires a static output size, with truncation or padding to that size. This distinction matters for interactive data analysis. [JAX's data-dependent output sizes](https://github.com/jax-ml/jax/blob/main/jax/_src/numpy/setops.py). +Transformation tags select the newest active level among a call's arguments. A batch can contain a dual, and a derivative can encounter earlier derivative levels. Calls made by a rule enter normal dispatch again, preserving these nesting relationships. Each level has a scoped lifetime: normal returns and errors perform its semantic exits; a panic restores the outer transformation state without running language work. Code reading actual items asks the transformation boundary to observe them. Item kernels should consume the buffer, slice or storage view they operate on rather than require unrelated callers to pass a proof wrapper. -## Effects and errors belong in the model +[JAX's interpreter stack, lifting and batching](https://github.com/jax-ml/jax/blob/main/docs/autodidax.md#part-1-transformations-as-interpreters-standard-evaluation-jvp-and-vmap) provides the model for composing transformations around primitive calls. BPL controls its evaluator and function values, including ordinary fallback when a transformation stops. -Lazy execution must preserve visible actions. BPL represents output and other effects as ordered nodes. A token is a dependency with no numerical meaning that keeps one effect after the previous effect. This follows JAX's ordered-effects design. An effect reads only its own operands, rather than forcing every pending array in the session. [JAX's effect sequencing](https://github.com/jax-ml/jax/blob/main/docs/jep/10657-sequencing-effects.md). +## Differentiation and iteration -A draw captures its generator and reserves a stream when its whole-array node executes. Reading that result again reuses its samples; replaying the program executes a fresh draw. Capturing inputs does not advance a counter. Independent lazy draws can receive streams in read order. Counter-based item positions keep a batched draw nonoverlapping without serializing its lanes. JAX's random design supplies the model for those streams. BPL manages the streams inside its runtime rather than requiring JAX's explicit user-level key splitting. [JAX's PRNG design](https://github.com/jax-ml/jax/blob/main/docs/jep/263-prng.md). +For one differentiated number, forward mode runs the function with a seeded tangent. For more, the AD driver linearizes the executed calculation once. A tape value represents a linear dependency without numerical tangent items. The tape uses the shared `Program` and `Tracer` with a tape role. Reverse mode transposes it from result sensitivities; forward evaluation of the tape is also available. The Jacobian uses whichever direction needs fewer runs. -Static graph-numeric execution has a non-raising policy for data errors. An invalid numeric index reads the fill, including on an empty axis, and an invalid numeric write skips that position. Dynamic execution retains checked behaviour. A potentially fallible dynamic branch gathers only its live cells before running them, a process called compaction. There is no speculative whole-read retry. Numerical execution records checked data errors without raising them. A host materialization/readback checks that slot before exposing storage; effects, error guards, display and statement ends are also checkpoints. Shape, kind, name and host errors remain checked. These choices avoid maintaining error-replay machinery alongside the graph executor. +Derivative rules obtain pervasive partials from rustymath's mathematical objects and apply them through primitive calls. The tape/transposition driver remains a separate AD responsibility, not a second scalar arithmetic evaluator. Transposed operations can defer and fuse normally. Coefficients retain values needed from the actual primal computation. Structural reads transpose to read mappings where possible and to accumulation where several reads used the same source position. They need no one-hot array per selected item. +[JAX's linearization and transposition](https://github.com/jax-ml/jax/blob/main/docs/autodidax.md#part-4-linearize-and-vjp-and-grad), [Dex's linearization](https://github.com/google-research/dex-lang/blob/main/src/lib/Linearize.hs) and [transposition](https://github.com/google-research/dex-lang/blob/main/src/lib/Transpose.hs), and [Futhark's bulk-array differentiation](https://github.com/diku-dk/futhark/blob/master/src/Futhark/AD/Rev/SOAC.hs) guide this choice. [tinygrad's movement gradients](https://github.com/tinygrad/tinygrad/blob/master/tinygrad/mixin/gradient.py) illustrate read-based transposes such as reversing a pad with a shrink. -Choose a useful result where the semantics and implementation permit one. Extra checks whose purpose is to reject a reasonable computation need justification. A wider integer result can be useful where a narrow type cannot hold an operand. Error behavior is a language choice rather than a requirement inherited from an old implementation. Memory safety, structural validity and checked host operations still require their own checks. +Power and generic folds/scans use ordinary body calls in their existing iteration algorithms. Differentiation follows the calls that execute. Recording those calls unrolls the computation and can make the program grow with the count. Unrolling itself does not imply cubic execution; repeated whole-program work can cause that avoidable cost. -Integer-producing real rounding already uses saturating conversion in native execution and the JIT: positive infinity gives the maximum integer, negative infinity gives the minimum, and NaN gives zero. Dynamic unmarked `⌊` and `⌈` produce integers. Static unmarked forms retain floats; explicit `⌊ₓ` and `⌈ₓ` produce integers in both modes. Exact-domain and complex rounding keep their existing domains. No extra rounding algorithm or error predicate is needed for these exceptional real inputs. +Known algebraic folds keep rustymath's bulk algorithms and reassociation freedoms. Generic bodies retain their language-defined ordering and effects. An equivalent primitive body can use the primitive route where existing admission rules establish that equivalence. Last-bit agreement between routes is not a reason to duplicate numerical algorithms. -Indexed reads and writes share one selection layout. Repeating a source key drops the affected result axis's keys, as [Index](glyphs/squad.qmd) describes. A write preserves the original array's keys without promising which replacement wins at a repeated position. There is no read/write planning mode or selection context to preserve through recording and replay. +Compact loops, loop-specific AD and checkpointing are possible later additions, not prerequisites for correct eager iteration. [JAX's loop transformations](https://github.com/jax-ml/jax/blob/main/jax/_src/lax/control_flow/loops.py) shows the distinction between unrolling calls and representing a loop as one operation. Add such machinery only for a demonstrated workload benefit. -## Share numerical algorithms at the right size +## Math ownership, reads and errors -A mathematical object owns the common computation, its derivative and range facts, and any repair needed for rare inputs. A formula over `Ops` describes arithmetic without choosing scalar, SIMD or generated execution. An optional repair predicate is another such formula, producing one Boolean per item. It is distinct from a comparison whose result happens to be Boolean. Range proofs can remove the repair test when the same predicate is always false. Otherwise consumers see the corrected result, never an uncorrected intermediate. +Rustymath owns formulas, range facts, derivative facts, repair algorithms and array kernels. Basedpl owns language domains and promotion, function structure, transformation drivers, evaluation and frontend behavior. Shared array agreement and read mappings belong in rustymath; attaching language meaning to keys, fills, options and assignment belongs in basedpl. Co-design the boundary rather than add adapters around incompatible accounts of the same fact. -Native execution reuses rustymath's optimized drivers and SIMD operations. RM and BPL share interfaces designed for both, without a compatibility layer around an old engine. Shared formulas remain monomorphic in hot loops; rare repair and error reporting use the existing two-pass pattern. A substantial iterative algorithm can remain one whole-array Call through the common graph runner. The foundation needs neither a new SIMD engine nor a general numerical-loop IR or blanket vector-call ABI. +Rustymath serves as BPL's math core; its independently useful core has a thin Python API. Change the core and bindings together without preserving old Rust/Python signatures or adding compatibility layers. Python compatibility does not constrain co-design. Discuss a dramatic loss of the Python API's independent usefulness before proceeding. +A formula over `Ops` describes arithmetic for native and generated execution. Specialized native slice forms can implement a substantial algorithm more efficiently. Power and root retain their table-and-repair algorithms together with cheap constant-exponent and exact cases. A native algorithm need not be translated into the JIT before programs can call it. Native execution capability and derivative capability are separate facts. -Native execution capability and derivative capability are separate facts. A native kernel can lack a specialized derivative. Its derivative can still use the same generic function body through ordinary graph operations. +Lowering retains immutable read recipes. Basedpl binds values and slots, schedules stages and packs the JIT ABI. Rustymath's `Read` and `Binding` read current storage and chunk windows; `Walked` constructs cycle backings or mapped buffers before parallel execution. Native maps, explicit conversions and elementwise selection (`Choose`) share operand reads and the output writer. Conversion scratch is reused across blocks within one execution. Row probes query existing geometry without general view/export normalization. Compiled input preparation preserves each bound array's actual storage dtype and declines an incompatible buffer. Semantic conversion, including float-to-integer flooring and tolerance, stays separate from exact input casts. -We prefer simpler, faster algorithms to legacy machinery for unnecessary edge-case accuracy, while protecting common numerical uses and exact results. That can mean composing existing operations or repairing rare items instead of translating an elaborate algorithm into every backend. The [target](target.qmd#numerical-algorithms) defines numerical bodies, type coverage, repair ordering and accuracy requirements. +Pending items are observed at the storage/read boundary. A read can force pending work under the settings captured by its calls. Language shape, kind, name and host-operation checks remain with their owners. Recording stops are internal control flow, distinct from user errors. Glyph pages define individual operations' results and errors; this page adds no new exceptional-input guarantees. -## One home for each decision +Default fills, explicit fills, an empty result's prototype and write destinations are different facts. They can share lower-level operations without becoming interchangeable. Reads and writes should consume established selection geometry instead of reconstructing it from a stored result. A copied value does not by itself retain the relationship needed for assignment. -BPL supplies language rules and function structure. rustymath supplies numerical operations and shared array-reading machinery. Transforms rebuild operations through the same dispatch entry. The owner resolves each fact once; consumers use that result rather than infer it again from a value's representation. A mathematical fact, read mapping, iteration algorithm or cache policy keeps the same owner across frontends and derivatives. +Keep hot numerical bodies inlinable, and dispatch once per block or whole call where appropriate. Retain a simple native path for the browser, which has no BPL JIT. Share the mathematical algorithm instead of creating a BPL-specific numerical implementation or a blanket vector-call ABI. -Graph construction uses logical values and descriptions. The runtime read boundary observes actual items. Item-processing algorithms use the existing storage view, buffer or slice that they operate on, while the original value retains graph and selection identity. Static enforcement belongs at that boundary. A proof wrapper passed through unrelated code would add another obligation to callers. Use a narrow checked API or ratchet where a type would cause that spread. +## Development and measurements -Default fills, explicitly chosen fills, inference of an empty result's prototype and logical write destinations are different responsibilities. Shared lower-level rules do not make those facts interchangeable. The target records each construction boundary and the old entry points removed when it changes. +[`DEV.md`](https://github.com/AnswerDotAI/basedpl/blob/main/DEV.md) documents build profiles, commands and contributor contracts. -The same graph should remain usable by a later GPU or external backend without adding a second account of what a BPL expression means. +Measure completed analytical operators, including final forcing and work repeated inside their loops. Report first recording/compilation separately. Use calibrated tools or measured slopes to separate fixed outer setup, never an estimated blanket subtraction. Compare matching numeric modes, input layouts, build profiles and hardware. +Main `.bpl` reference tests and substantive speed/count tests run with Mathish off. Dedicated `*_mathish.bpl` files preserve deliberate Mathish stories and a few cheap regression canaries. This is a test policy, not a change to the user-facing Mathish default. Pages and notebooks exercise the language-default mode. Mathish is useful optional behavior, not a performance target requiring more machinery. -## Development and performance builds +Give each fact one home and each job one mechanism. A type is useful when it narrows what an actual consumer accepts; passing it through unrelated code adds an obligation. Prefer a small substantive test or ratchet when static enforcement would spread through the codebase. Before adding substantial machinery, identify owners, affected callers, state and code it removes. -Development and correctness-test helpers use the optimized, incremental `quick` profile with LTO off. Request `--profile lto` for local ThinLTO when checking performance budgets. Cross-crate generic sharing reuses rustymath's instantiated Rust implementations in BPL instead of generating them again. The build configuration enables its unstable compiler switch. Separate Cargo artifact and incremental caches remain available when switching profiles. The [development guide](https://github.com/AnswerDotAI/basedpl/blob/main/DEV.md#codegen-and-rebuilds) owns the tested compiler configuration and commands. diff --git a/nbs/llms.txt b/nbs/llms.txt index e1123b72..b98b83a0 100644 --- a/nbs/llms.txt +++ b/nbs/llms.txt @@ -2,7 +2,7 @@ > BPL: Based-array Programming Language. APL-derived; J, BQN ideas. Base: Dyalog `⎕ML≥2` (`↑` first/take, `⊃` mix/pick) except below. Most APL idioms don't apply: write BPL. -Entry: Dyalog diff, example, defining page. Not the reference: read the page before relying on an entry. Links relative to https://answerdotai.github.io/basedpl/. Session: `]help ⍳` shows a glyph's or system fn's page. +Entry: Dyalog diff, example, defining page. Index, not reference: read a feature's full page before using, reviewing or describing it; entry or grep hit insufficient. Links relative to https://answerdotai.github.io/basedpl/. Session: `]help ⍳` shows a glyph's or system fn's page. ## Terms diff --git a/nbs/performance.qmd b/nbs/performance.qmd index 2bc13b50..a22d0fb6 100644 --- a/nbs/performance.qmd +++ b/nbs/performance.qmd @@ -73,7 +73,7 @@ w←1000↕¿1000000⍴0 ## Compiled loops -A group of 3,145,728 items or more compiles its loop into machine code. The compiled loop keeps intermediate values in registers. A smaller group compiles too where the process has already compiled its loop. A compile takes about a millisecond. `•explain` records each run of compiled code as a `"compiled"` route: +By default, a group of more than one operation compiles its loop into machine code at 3,145,728 items or more. The compiled loop keeps intermediate values in registers. A smaller group compiles too where the process has already compiled its loop. A single operation normally runs its native kernel unless compilation is explicitly forced. A compile takes about a millisecond. `•explain` records each run of compiled code as a `"compiled"` route: ```bpl y←¿3145728⍴0 @@ -88,7 +88,7 @@ Each, Rank, stencils and outer products call a dfn once, on all their cells toge ## The cost of a call -Each call pays a fixed cost to interpret it, whatever the size of its arguments. A call of `{⍵}` costs about 1,500 machine instructions. Each call of a recursive Fibonacci function costs about 4,600. A loop over the items of a million-item array pays that cost a million times. Code that works on whole arrays pays it once for each call. Compiling doesn't remove this cost. Only the loops inside a group compile. +An unrecorded call pays a fixed cost to interpret it, whatever the size of its arguments. A call of `{⍵}` costs about 1,500 machine instructions. Each call of a recursive Fibonacci function costs about 4,600. A loop over the items of a million-item array pays that cost a million times. Code that works on whole arrays pays it once for each call. Compiling doesn't remove this cost. Only the loops inside a group compile. ## Names and writes @@ -131,6 +131,6 @@ Large calls split across the machine's cores. A call splits only when it has eno The plan includes: - One pass for several reductions of the same data. Today each reduction, as in `+/v ÷ ⌈/v`, makes its own pass. -- A tracing point for whole training steps, such as `⍣`. A repeated step's forward pass, gradient and update would then run as one program. +- A tracing point that combines a whole training step's forward pass, gradient and update into one program. Reuse of eligible dense-array bodies in Power with a single finite count, map fallbacks and generic folds/scans does not trace a whole training step. - Programs for maps whose cells take different branches, so that `∂` can trace them. - The GPU, through WebGPU, with 16-bit brain floats (bf16), and compiled code in the browser. diff --git a/nbs/study/01_basics.ipynb b/nbs/study/01_basics.ipynb index 462e0773..cd9b232a 100644 --- a/nbs/study/01_basics.ipynb +++ b/nbs/study/01_basics.ipynb @@ -2618,7 +2618,7 @@ { "data": { "text/plain": [ - "1 1 3 1 5" + "0 4 1 5 2" ] }, "metadata": {}, @@ -2638,7 +2638,7 @@ { "data": { "text/plain": [ - "0.2138630234 0.0605094191" + "0.07522840832 0.1445897454" ] }, "metadata": {}, @@ -2674,7 +2674,7 @@ { "data": { "text/plain": [ - "38 32 37 21 2 22 14 43 51 29 10 20 42" + "36 39 29 37 24 3 49 38 50 33 28 6 41" ] }, "metadata": {}, diff --git a/nbs/system/explain.qmd b/nbs/system/explain.qmd index 057d31f2..49c687ef 100644 --- a/nbs/system/explain.qmd +++ b/nbs/system/explain.qmd @@ -49,10 +49,10 @@ - `"compiled"`: a group that would compile ran on its usual kernels instead. The reason names a call whose kernel has no machine code, or is `"a kernel that doesn't compile"`. - A fast combination's name, as in `"partial grade"`, with `"arguments that don't suit it"`: the general path ran instead. - `"batching"`: a map didn't try a batched call, because `"the batch pref is off"`, because of `"a label level, or a ∂ call inside a batched run"`, or because of `"an argument too large to lay over the frame"` or `"an argument that isn't compact"`. - - `"program"`: a call could not attempt a program. `"a call under another level"` means `∂` ran inside another level, such as a batched map. `"an argument that programs can't hold"` means a program cannot take one of its arguments. + - `"program"`: an ordinary body call or `∂` did not use a program. `"a call under another level"` means the call ran inside another transformation, such as a batched map. `"an argument that programs can't hold"` means a program cannot take one of its arguments. Other reasons include `"an effect"`, `"a write"`, `"a random draw"`, `"a body outside dense elementwise reuse"` and `"a free value that changes on every call"`. - `"in place"`: a kernel copied an argument's items into a buffer instead of reading them where they are, because of `"repeats of each item"`, `"a cycle that wraps"`, `"steps other than a forward run"` or `"a repeated item"`. - `"contraction"`: a fold of elementwise products could not run as a matrix product. -- `programs` has a row for each [`∂`](../glyphs/partial.qmd) that ran from a program, which holds the whole call, its value and its derivative together. `event` is `"built"` where the call's trace built the program first, and `"replayed"` where the call found one. `steps` counts the program's steps. A replay runs the program's steps as ordinary calls, which fuse and compile as other calls do, so `fused`, `compiles` and `routes` report that work. `listing` shows the program, one line for each value. It shows a size mark in the dfn as a `convert` step. A call whose trace stopped has a `stopped` row instead. +- `programs` has a row for each call that built or replayed a program. These include [`∂`](../glyphs/partial.qmd) calls and eligible ordinary dense-array bodies in [Power](../glyphs/star-diaeresis.qmd) with a single finite count, Each/Rank/outer-product fallbacks and generic folds/scans. A derivative program holds the primal and derivative work; an ordinary-body program holds that body's work. `event` is `"built"` for a newly recorded program and `"replayed"` for a compatible cached program. `steps` counts its steps. Both use the shared planner and executor, whose work appears in `fused`, `compiles` and `routes`. `listing` shows the program, one line for each value, including size marks as `convert` steps. A stopped `∂` trace has a `stopped` row instead; an ordinary-body trace stop reports a `"program"` decline. Valid cached hits apply immediately. Only a cache miss waits for consecutive matching invocation keys. This waiting is silent. Calls excluded before admission run ordinarily without attempting a recording or reporting a program decline. Each event record is a table of columns, as `•csv` returns. `(•explain t).routes.route` lists successful routes. `.fast.rule` lists fast combinations. `•prefs ["fusion":$t]` writes each forcing call and assignment as it happens. Event recording is disabled outside a capture. diff --git a/nbs/web.ipynb b/nbs/web.ipynb index 2b62a35b..bfa45f70 100644 --- a/nbs/web.ipynb +++ b/nbs/web.ipynb @@ -293,7 +293,7 @@ " const { source, options, data } = JSON.parse(json, view);\n", " (0, eval)(source)(canvas.getContext('2d'), data, options);\n", "}\n", - ")(document.currentScript.previousElementSibling, \"{\\\"source\\\":\\\"line\\\",\\\"options\\\":{},\\\"data\\\":{\\\"shape\\\":[600.0,2.0],\\\"data\\\":{\\\"$typed\\\":\\\"Float64Array\\\",\\\"offset\\\":0,\\\"length\\\":1200}}}\", Uint8Array.from(atob('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'), c => c.charCodeAt(0)))" + ")(document.currentScript.previousElementSibling, \"{\\\"source\\\":\\\"line\\\",\\\"options\\\":{},\\\"data\\\":{\\\"shape\\\":[600.0,2.0],\\\"data\\\":{\\\"$typed\\\":\\\"Float64Array\\\",\\\"offset\\\":0,\\\"length\\\":1200}}}\", Uint8Array.from(atob('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'), c => c.charCodeAt(0)))" ], "text/plain": [ "canvas" @@ -440,7 +440,7 @@ " const { source, options, data } = JSON.parse(json, view);\n", " (0, eval)(source)(canvas.getContext('2d'), data, options);\n", "}\n", - ")(document.currentScript.previousElementSibling, \"{\\\"source\\\":\\\"(ctx, p, o) => {\\\\n let m = 0;\\\\n const tick = () => {\\\\n m = m > 1.5 * p.shape[0] ? 0 : m + o.step;\\\\n ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);\\\\n line(ctx, {data: p.data.subarray(0, 2 * m)});\\\\n if (ctx.canvas.isConnected) requestAnimationFrame(tick);\\\\n };\\\\n tick();\\\\n}\\\",\\\"options\\\":{\\\"step\\\":10.0},\\\"data\\\":{\\\"shape\\\":[2001.0,2.0],\\\"data\\\":{\\\"$typed\\\":\\\"Float64Array\\\",\\\"offset\\\":0,\\\"length\\\":4002}}}\", Uint8Array.from(atob('AAAAAAAwa0AAAAAAAMBSQAa7M1KsL2tANuWqpA/MUkC6OiVRsS5rQKRdZEcb2FJAxnfWFQ8ta0DeBNTmHuRSQAQ78snFKmtAM3PTghbwUkCiXcen1SdrQAwKBx3++1JAnCNC+j4ka0BehHa50QdTQICx4xwCIGtALzclX40TU0B0nrd7HxtrQFbuqRgtH1NAtKNHk5cVa0CaUcb0rCpTQB9rjfBqD2tAXb79Bgk2U0BwfuIwmghrQB+CK2g9QVNAGlnuASYBa0BAYhg3RkxTQPCekiEP+WpAVl0PmR9XU0D/edVdVvBqQMaScbrFYVNAJiLKlPzmakAhPUnPNGxTQE6Rd7QC3WpALKzbE2l2U0BIZ726adJqQFYrOs1egFNAjAA3tTLHakDEwdFJEopTQF/DHMFeu2pA+7j54X+TU0AMpyML765qQKLXgPijnFNAFflaz+ShakC2PTn7eqVTQHxkCFlBlGpA9M+CYwGuU0B/P4ICBoZqQD4g1LYztlNAPCQINTR3akARwUGHDr5TQBHZmWjNZ2pAP/IDdI7FU0Ccjcwj01dqQGeV+imwzFNAenGe+0ZHakDEWC9kcNNTQCqqSJMqNmpARwdW7MvZU0CTrQ+cfyRqQAvtSpu/31NA6wYS1UcSakCNP49ZSOVTQOCLFQuF/2lAM3nDH2PqU0A2CFMYOexpQBCYH/cM71NA/GVA5GXYaUD7L+n5QvNTQABZWWMNxGlAakDnUwL3U0D+k+aWMa9pQL6+00JI+lNAco7DjNSZaUD2xcoWEv1TQAviIl/4g2lAFl23Ml3/U0DaRlE0n21pQNbFvQwnAVRAqzV3PstWaUB4RaMubQJUQOE3Wbt+P2lA/lkzNi0DVECd7Bb0uydpQEdOotVkA1RA6sroPIUPaUDyH+3TEQNUQOCo3PTc9mhAOao2DTICVEDiD5GFxd1oQFQJInPDAFRAKmbvYkHEaEA9KioNxP5TQAr25ApTqmhAM3v2+DH8U0Bk2xoF/Y9oQHCxrGoL+VNA99+s4kF1aEA4mT+tTvVTQFZP3z0kWmhAfuW6IvrwU0BZy9O5pj5oQPD0i0QM7FNAGyo9AswiaEB5gsejg+ZTQJ5mEsuWBmhAxjds6V7gU0BSrUDQCepnQLcYotac2VNA0Y1c1SfNZ0D/vvZEPNJTQE5bUqXzr2dAq12WJjzKU0AoxRUScJJnQLeEgYabwVNAYrFQ9J90Z0Aknb+IWbhTQJNiESuGVmdAjRaOanWuU0A383ebJThnQJI/jILuo1NAMDBjMIEZZ0DXwuNAxJhTQG7cHNqb+mZA1sJtL/aMU0DCZwWOeNtmQByP1PGDgFNA7iI/Rhq8ZkAC7rFFbXNTQBL7WAGEnGZAY/apArJlU0C1xvjBuHxmQC51ghpSV1NAji6FjrtcZkA/2zaZTUhTQG08z3CPPGZARrAIpaQ4U0CCmrt1NxxmQPeHjH5XKFNAco3rrLb7ZUAyd7OAZhdTQIezZSgQ22VAQgfRINIFU0Bvkz78RrplQMqlne6a81JA+gRBPl6ZZUBfkDWUweBSQDx+lgVZeGVAOzsU1kbNUkCOT29qOldlQAYzDJMruVJA59iqhQU2ZUD7eTvEcKRSQPLCf3C9FGVAPWH8fBePUkBlRiREZfNkQJjf0uogeVJA+It2GQDSZEBgZ1ZVjmJSQHQvpQiRsGRAkT0YHmFLUkBC8NcoG49kQLZUhsCaM1JAw5nYj6FtZECxrsrRPBtSQO4tvFEnTGRAxEinAEkCUkBeXIyArypkQNSVThXB6FFAL1DxKz0JZEAoizjxps5RQNLe22DT52NAeET0jvyzUUASIjApdcZjQH5E9gHEmFFAVIdwiyWlY0ChV2N2/3xRQDteaYrng2NA2x7YMLFgUUCW8NwkvmJjQEhJLY7bQ1FAjC0wVaxBY0BMgzgDgSZRQOvxFxG1IGNArSKKHKQIUUBH90ZJ2/9iQE2XJ35H6lBArnMc6SHfYkDGqELjbctQQINzU9aLvmJAYIntHRqsUED39rLwG55iQGHHyxZPjFBAldy+EdV9YkAnJsDMD2xQQDGiaQy6XWJAs2iXVF9LUEBcBceszT1iQOAXsNhAKlBAh4y/txIeYkDSTp+YtwhQQMABxeqL/mFA4jCl0Y3NT0Dt54f7O99hQMrSXWTkiE9AO/OtlyXAYUAihFnjeUNPQGCNiWRLoWFAdgnjb1X9TkAhbtL+r4JhQFMYzFF+tk5AelBf+lVkYUAOH5X2+25OQJjM4OE/RmFANsGP8NUmTkC2Xp02cChhQIUj/PUT3k1AuKIucOkKYUD0IiHgvZRNQFfMP/yt7WBA/pJfqttKTUBtY00+wNBgQASgQHF1AE1A0ktmjyK0YEAmc39xk7VMQCAg7j3Xl2BA1jUOBz5qTEBe5mCN4HtgQIKTFqx9HkxAqCUYtkBgYEDr2PX3WtJLQIFjEeX5RGBAqNA0nt6FS0B0D7Y7DipgQIJ9e20ROUtAZuOkz38PYEBI04BO/OtKQNd9+VSh6l9AyI/2QqieSkAsB1KTBbdfQK9VcWQeUUpAU/BePDCEX0DaK03jZwNKQMZw/xglUl9A8IOOBY61SUCcbYjY5yBfQKvrvyWaZ0lA5KtpEHzwXkCAjMyxlRlJQNs71jvlwF5ADJ7XKYrLSEDkJnC7JpJeQLTvEB+BfUhAfWn31ENkXkCmroYyhC9IQElC/LI/N15Ab430E53hR0C33pRkHQteQA5zkIDVk0dAaG0W3d/fXUBe19RBN0ZHQBKe0fOJtV1AdPNILMz4RkA4ltJjHoxdQGzsRh6eq0ZAgGGky59jXUCeH8D+tl5GQCzkF60QPF1AjLf/uyASRkDGVQ5tcxVdQNywa0rlxUVAXklHU8rvXEACd0SjDnpFQM5IMooXy1xAqEBjw6YuRUBkB8QeXadcQKdT96m340RAbDBPAJ2EXEDPWEJXS5lEQFrVYADZYlxA+udTy2tPREDYfqDSEkJcQMp0xAQjBkRA1eOzDEwiXECSxG//er1DQNJIJiaGA1xAPhYvs311Q0CiiVN4wuVbQMskkxI1LkNACM9WPgLJW0AnLZ4Jq+dCQHHx/JRGrVtARiB+fOmhQkBKibp6kJJbQBoqR0b6XEJAWq2lz+B4W0Bstq435xhCQKZec1U4YFtADhzHFbrVQUBsoXivl0hbQFsXvJh8k0FAzUGvYv8xW0BrPJBqOFJBQNBCvtVvHFtApYnbJfcRQUBq9QVR6QdbQABDi1TC0kBAULWv/mv0WkBKPqNuo5RAQGdIwer34VpAc8gA2aNXQECb3TMDjdBaQBRMH+TMG0BAIacOGCvAWkAEwb2VT8I/QP4LhdvRsFpA/NKZYntPP0D+axjigKJaQKbc3UYv3z5AC3G9ojeVWkCmnx8ofXE+QDboBHf1iFpAfAcft3YGPkCMHEibuX1aQHT4am0tnj1AFK3YLoNzWkDowguLsjg9QFTXMzRRalpAvIYzFBfWPEDNLjmRImJaQCLS9M5rdjxA8LlkD/ZaWkCsxf9AwRk8QEFsDFzKVFpA0gdmrSfAO0A49aAInk9aQBbQZRKvaTtA5trxim9LWkAyUjwnZxY7QPLWdD09SFpA+M//WV/GOkBRbJBfBUZaQKCYgc2meTpAeKvpFcZEWkAaOjhXTDA6QJAatWp9RFpALikyfV7qOUC8tgpOKUVaQHAjEXTrpzlANQM9lsdGWkBEjQ4dAWk5QH4aMwBWSVpATA0JBK0tOUDItcUv0kxaQAylm1389ThABR4fsDlRWkDqhT4F/ME4QAn6HfSJVlpAOuBxe7iROECZ7LpWwFxaQNzo8uM9ZThA+vRwG9pjWkD2UPsDmDw4QEKEqG7Ua1pAqGmLQNIXOEBjOSVmrHRaQNIsv5z39jdAbTV2AV9+WkAkYi64Eto3QHD5aCrpiFpA4hZYzS3BN0Djvn61R5RaQDSdGbBSrDdAUDtkYnegWkAoRjHMips3QJK/a9x0rVpA0AjNI9+ON0Dgogm7PLtaQMZGJU5YhjdAYOlSgsvJWkBS3SN2/oE3QOQVfqMd2VpA0rAXWdmBN0AWFmZ9L+laQKDedEXwhTdAJTgPXf35WkDav6EZSo43QMsYLn6DC1tATObQQu2aN0AzeLALvh1bQOA66LvfqzdAIuRHIKkwW0BuY3UMJ8E3QIgl9sZARFtAbJSvR8jaN0BaX5v7gFhbQNjwhgvI+DdAkcyFq2VtW0CemcF/Khs4QKIKA7bqgltApIsmVfNBOED73fLsC5lbQG5qtsQlbThArF1bFcWvW0D8UvKOxJw4QBpz/ucRx1tAhtEw+9HQOEDimvAR7t5bQFgSAddPCTlAVNMwNVX3W0DCZJx1P0Y5QGKlQelCEFxAbCVmr6GHOUBKNMO7silcQOQieuF2zTlAeUAOMaBDXECEjkntvhc6QLwJ0MQGXlxABolGOHlmOkAQ/abq4XhcQDZZnqukuTpA8hnADi2UXECkWAK0PxE7QFr7dJbjr1xA+qB/QUhtO0AXcOrgAMxcQJyCZce7zTtAno6vR4DoXEAuyzo8lzI8QN0vXR9dBV1AcOHBGdebPEALvTW4kiJdQCy5C113CT1AIDzFXhxAXUAUoZmGc3s9QJKGgVz1XV1ApumNmsbxPUBMlmr4GHxdQIBj6yBrbD5ARtSqd4KaXUBWsOMlW+s+QMJUNx4tuV1AeGE0OpBuP0C17G8vFNhdQHzbknMD9j9AUAy/7jL3XUAne5O21kBAQEhKOaCEFl5A0CYKJMOIQEDsizyJBDZeQMEaCdbC0kBA2bUO8a1VXkCZIQlk0R5BQFbReyF8dV5AW616KepsQUBgknNnapVeQMaWJ0YIvUFAriqmE3S1XkC7C5yeJg9CQMRVIHuU1V5AJ6GW3D9jQkCiiub3xvVeQPp8f29OuUJAdz+P6QYWX0A4i+aMTBFDQPIr3LVPNl9Av7AIMTRrQ0D6dlLJnFZfQPDsWx//xkNAyrzRl+l2X0BlWyLjpiREQFHaKZ0xl19ANwUE0CSEREA8a69dcLdfQNVwrwJy5URAAOjOZqHXX0D73oBhh0hFQGxRnk/A919AkCEwnV2tRUDXq7Zc5AtgQJP5hDHtE0ZASfUpKNsbYEA45xFmLnxGQK2Q32XCK2BA2Vf1ThnmRkA+NH73lztgQCsboc2lUUdA93HhxFlLYECyCaiRy75HQHJ6XbwFW2BA3MSRGYItSEAF6QHTmWpgQGd4tLPAnUhA25DbBBR6YEC0hBR/fg9JQMRCNVVyiWBAHvhJbLKCSUDCg9fOsphgQN68az5T90lAYSxHhNOnYED4YACMV21KQALpApDStmBA+Fv0v7XkSkCck78UrsVgQHy2lRpkXUtAVl+jPWTUYEBK9pSyWNdLQLXOfz7z4mBALDELdolSTEA1bQpUWfFgQKIohSvszkxAPUUUxJT/YEDdThNzdkxNQKQLwN2jDWFABpZexx3LTUAJ+rb5hBthQCHpwX7XSk5AhlFcejYpYUArLmjMmMtOQGd//8u2NmFAILBuwVZNT0Cq3QxlBERhQKrOC04G0E9AZgk9xh1RYUDM6FwhzilQQDXJwnoBXmFAgt6yqAZsUEAJfncYrmphQDMBUQenrlBACBkGQCJ3YUDDdW55qfFQQB+RFJ1cg2FA+EzNLQg1UUBK02vmW49hQIk7WUa9eFFAtSge3h6bYUB698fYwrxRQA8OrFGkpmFAWSg77xIBUkCPdyca67FhQALX44inRVJAdH1VHPK8YUDdSaeaeopSQPlszki4x2FASjrFD4bPUkDFORycPNJhQG1Pf8rDFFNAWUzXHn7cYUDvysGkLVpTQPipweV75mFAl1PNcL2fU0DPcuARNfBhQO7J4fls5VNAYbOT0Kj5YUCIEuoENitUQHGGrFvWAmJAEMIoURJxVEDGhIH5vAtiQEiW5Zj7tlRAeIAB/VsUYkDjpxuS6/xUQJ6JxMWyHGJAYkAo79tCVUBqOhvAwCRiQH8/el/GiFVAAkkcZYUsYkA8+0GQpM5VQKBdsDoANGJAAoYhLXAUVkCdK5zTMDtiQLRF3eAiWlZAdcyIzxZCYkAhxwxWtp9WQORbCtuxSGJAjbjLNyTlVkCG1KSvAU9iQMX2ajJmKldAiizPEwZVYkBflyH0dW9XQGiy9Nq+WmJAgNu9LU20V0CYqXTlK2BiQP71VZPl+FdAmyegIE1lYkAikPjcOD1YQNYxtoYiamJAvPdcx0CBWED0HN4erG5iQCPikhT3xFhA1i4g/elyYkDkrrGMVQhZQCqEXELcdmJArBWH/lVLWUARO0Acg3piQEAqRUDyjVlAguQ4xd59YkBsoi8wJNBZQCI+ZoTvgGJA7EhIteURWkDANoqttYNiQB6J+r8wU1pAnz/3oDGGYkDQ/sVK/5NaQBXtfMtjiGJAVvbnWkvUWkA06VKmTIpiQG7IAwEPFFtAYjsCt+yLYkAu/slZRFNbQAHoTI9EjWJA7iiejuWRW0CC6xPNVI5iQL9aO9bsz1tAYZQ7Gh6PYkC7K1d1VA1cQNk/jiyhj2JAwzhDvxZKXEBAfZ3F3o9iQLcIjRYuhlxAKZyhstePYkD4RJztlMFcQLypV8yMj2JAkTJPx0X8XEC+4d32/o5iQDJZlTc7Nl1AG5iOIS+OYkD6RQjkb29dQNqf2UYejWJAk1eChN6nXUCjNBxszYtiQBqCs+OB311AL2x3oT2KYkCk+LPfVBZeQCE1pQFwiGJAgquUalJMXkD36MuxZYZiQLiJ7Yp1gV5ACXdQ4R+EYkAQdWlcubVeQIgup8mfgWJAz9dPEBnpXkDSLSOu5n5iQCTMC+6PG19Ac33E2/V7YkDxxLBTGU1fQHXdBKnOeGJAbqh8trB9X0CqS6N1cnViQPxMV6NRrV9A6UluquJxYkBASU+/99tfQETrDLkgbmJAWgQKZM8EYEB4r8YbLmpiQACLNkohG2BA5TNKVQxmYkCdRleK7zBgQKfBcvC8YWJAEFSVKThGYEBnwAyAQV1iQHgrUDr5WmBAwxWZnptYYkBPbFPcMG9gQDN5D+7MU2JA7iILPd2CYEB+xJ8X105iQD+Btpf8lWBA80hyy7tJYkCnBJk1jahgQMgxZ8B8RGJARgMqbo26YED2+9SzGz9iQA+bQqf7y2BAQwxGaZo5YkAs/UlV1txgQAZsNar6M2JAmxBg+xvtYECQtcpFPi5iQMRmhivL/GBABDmVEGcoYkCGfceG4gthQKtiRuR2ImJAzklcvWAaYUDKa2ufbxxiQJAG0I5EKGFAQV8mJVMWYkC2QyLKjDVhQBR75lwjEGJAPDHnTThCYUBU+B8y4gliQIwiZghGTmFAyEIDlJEDYkCgRrb3tFlhQMypM3Uz/WFAgJHZKYRkYUD9k33LyfZhQAvU1byybmFAZT+Mj1bwYUAe/8veP3hhQKgXn7zb6WFAXo8NziqBYUAcrD5QW+NhQDcfMNlyiWFAek/xSdfcYUDUHB9fF5FhQAlr76pR1mFABqIrzxeYYUAaj9d1zM9hQEZsGqlznmFAvUpirknJYUBU8y99KqRhQKLUFVnLwmFABp067DupYUATj/l6U7xhQD4Nm6enrWFADHFJGeS1YUAJkUpxbbFhQGNeKTl/r2FAXKTfG420YUAAeVjfJqlhQOyRkIoGt2FAPHXkD92iYUDuLDSx2bhhQED73M2jnGFA1KRAlAa6YUCUHwcbfZZhQDBzyEiNumFAsPyQ92qQYUBXZHX0bbphQKF3xWFvimFAWruBzai5YUC5OcBVjIRhQF5yrxo+uGFAJ+khzcN+YUBomD0zLrZhQG2qxL4XeWFAIM7bfnmzYUCO9HAeinNhQAHjm3UgsGFAwcGS3BxuYUDwlOGfI6xhQHom7+XRaGFAOXRQloOnYUCMWFojq2NhQEjutwFBomFA+S9ueapeYUCVgf2aXJxhQDErQcjRWWFAhh0FK9eVYUAwAB7rIlVhQECxl4qxjmFAE8Q7uJ9QYUCO7Eei7IZhQH6ydgBKTGFAODZVaol+YUA1ngmPI0hhQILbjOqIdWFAJRRIKS5EYUB2fCk67GthQAg6WY5rQGFAOrmwf7RhYUDIcfN23TxhQIAkz/DiVmFAicoYlYU5YUCQfzLSeEthQE1I1JNlNmFAn0Rid3c/YUD3CvgWfzNhQEqElkLgMmFAVF3cutMwYUAvG42ktCVhQMm0HxRlLmFABkVdHPYXYUADqmevNCxhQI2SSTemCWFAF/MiEUQqYUAISJCQxvpgQCVnTLWUKGFAGCo60VjrYEC3Ey8PKCdhQPC+569e22BApWsrif8lYUAoCp3w2cpgQGqXfYQcJWFAhcmLZMy5YEB97gRZgCRhQFY53Ok3qGBARaEMVSwkYUAMZ3RrHpZgQPCaFb0hJGFADBm+4IGDYEB9oqHLYSRhQNVSa01kcGBA88H/sO0kYUCcfDnBx1xgQLH6GZPGJWFA6zWzV65IYECgTUSN7SZhQLna8DcaNGBAxR0NsGMoYUDFwleUDR9gQKvzDgEqKmFAC0NYqooJYEDRp8N6QSxhQM7yVIQn519APPpYDKsuYUD81BNdVrpfQPOchplnMWFAfvTfmKaMX0A5t2X6dzRhQAePmgUdXl9A4uZJ+9w3YUB45oOEvi5fQDPFm1yXO2FAELipCZD+XkBi9LTSpz9hQL2tU5uWzV5ApLq9BQ9EYUDK221R15teQJUvjJHNSGFArlzxVFdpXkBwAIUF5E1hQLQdS98bNl5Aks995FJTYUC07sA5KgJeQEUzoaQaWWFAtOfUvIfNXUDrV1SvO19hQKA3p885mF1ALkkeYbZlYUDOblbnRWJdQL7lkAmLbGFAB1lehrErXUAGgTPruXNhQCF69TuC9FxA0zVvO0N7YUBaQWmjvbxcQOvsfCIng2FAngZ5Y2mEXEBAGlW7ZYthQBrmry2LS1xALDOhE/+TYUDSjb29KBJcQAHgrivznGFAxhHO2EfYW0D26mT2QaZhQHja4EzunVtAOO45WeuvYUCUwh7wIWNbQMbCLCzvuWFA3ncvoOgnW0Bosb45TcRhQFo0jkFI7FpA+GbvPgXPYUDS492+RrBaQOOsOusW2mFAuso8COpzWkCU5pfggeVhQBDDlxI4N1pAVVR7s0XxYUACJP3WNvpZQNAb2eph/WFAFGnvUey8WUBeFioA1gliQKOtt4Jef1lAzGRyX6EWYkC0Ebhqk0FZQEvISWfDI2JArRy+DJEDWUDov+VoOzFiQOwyVWxdxVhApGklqAg/YkDsMhmN/oZYQBgmn1sqTWJADU8JcnpIWEBr/a+sn1tiQDs42xzXCVhABcSMt2dqYkCWrk6NGstXQEv9VIuBeWJAVIyBwEqMV0BZeicq7IhiQPVfRLBtTVdAsLI4iaaYYkCWqW9SiQ5XQEzU6pCvqGJAZtA5mKPPVkCniOccBrliQPfjjW3CkFZArWs7/KjJYkA2PmO461FWQLkxc/GW2mJAhBkWWCUTVkBFebqyzutiQEQvwSR11FVA4ET86U79YkBmcpju4JVVQJwZBTUWD2NA9/lEfW5XVUBHvqYlIyFjQL4uQo8jGVVAIZfdQXQzY0B6TzzZBdtUQNuZ9wMIRmNAFl9wBRudVEBf1bza3FhjQG2PDbNoX1RAoIiZKfFrY0ClO5h19CFUQJvDyUhDf2NAnYRO1MPkU0BZjYaF0ZJjQAKijkncp1NAvYk0IpqmY0Da+T5CQ2tTQJYZlFabumNAuhA4Hf4uU0BK7/JP085jQD9ksCoS81JARBJfMUDjY0A+QaqrhLdSQAlL2xPg92NA4qZj0Vp8UkDY8ZQGsQxkQJxHyLyZQVJAWhkbD7EhZECQuOV9RgdSQMMOlyneNmRAEeBhE2bNUUDQKAZJNkxkQPSy82n9k1FAit50V7dhZEDSUN5bEVtRQOAeOzZfd2RAg45usKYiUUC44Tm+K41kQF7+ehvC6lBAMusZwBqjZEC0hOY8aLNQQIO5iwQquWRAVocloJ18UECnlYhMV89kQJDGxbtmRlBALr+UUaDlZEAJ7fjwxxBQQA6rAsYC/GRAIslDFou3T0BxTTdVfBJlQIf5yX7HTk9AEmbvowopZUBN4XVYTedOQP3GhVCrP2VA5a4NsiSBTkD/jDrzW1ZlQNwZXmtVHE5AOEF7HhptZUDdOXI057hNQPfaK1/jg2VAPnvRjOFWTUAdmfA8tZplQJ7Gw8JL9kxA7Kl4Oo2xZUAK8ZvyLJdMQESYydVoyGVAFIkIBow5TED3dIuIRd9lQNgUa7Nv3UtAGrNVyCD2ZUAc0zV93oJLQMGt/Ab4DGZAOBJQsd4pS0DJzd+yyCNmQL0tgWh20kpAHEY4N5A6ZkAQQ+KFq3xKQLtcaPxLUWZAHq1WtoMoSkDpNkto+WdmQDBVC3AE1klAsh6F3pV+ZkAQ5/zxMoVJQN831MAelWZAnvWEQxQ2SUB9mmFvkatmQL4b7jOt6EhADMkSSevBZkDuJA9aAp1IQCp426sp2GZAJUjtExhTSEDenQ/1Se5mQCJ+ZYbyCkhAPL61gUkEZ0Ah/NyclcRHQGJr2a4lGmdAYtr4CAWAR0CC7t3Z2y9nQPbtXEJEPUdA2Q/RYGlFZ0De2nGGVvxGQGPzvaLLWmdAhmQy2D69RkAAAAAAAHBnQAAAAAAAgEZAwsaV2gOFZ0Dnq36LnERGQETgc5bUmWdAxA94zRYLRkCytdeZb65nQLDkxd1w00VANiuaTdLCZ0Dkp0OZrJ1FQK8hgh361mdAgJbHoctpRUBSxZZ45OpnQMTxIl7PN0VAIJ9x0Y7+Z0ADiSn6uAdFQNpekJ72EWhADofAZonZREBvU6ZaGSVoQF1+9FlBrURAtIfthPQ3aEC5rxZP4YJEQEN5d6GFSmhASYXhhmlaRECFX305ylxoQME7pAfaM0RA4/iv279uaEDcsXWdMg9EQCTUhhxkgGhAA1Zu2nLsQ0AkDI+WtJFoQNIo6Raay0NA8Wu56q6iaEAjystxp6xDQLnxp8BQs2hA94XV0JmPQ0CipvrGl8NoQDlV9eBvdENAIMKbs4HTaECPxaYWKFtDQB8PC0QM42hAy7pVrsBDQ0CYiag9NfJoQJL7yKw3LkNARiv+bfoAaUDZeZPfihpDQBTeCKtZD2lAY0eM3bcIQ0A5i4DTUB1pQDQlTQe8+EJA6T0fz90qaUAvnLeHlOpCQKJQ547+N2lAp4yAVD7eQkA8nGgNsURpQGkhwi6200JAFKAET/NQaUAnEpSj+MpCQJ2aMWLDXGlAgSCqDALEQkD5ibxfH2hpQA26+JDOvkJAJQwKawVzaUDzqF8lWrtCQJUWVrJzfWlAFLxajaC5QkAnffJuaIdpQOROuFuduUJAdkCE5eGQaUBVmFXzS7tCQMWbP2bemWlArqfgh6e+QkDoyiJNXKJpQF/1nx6rw0JAnoAvAlqqaUALbT+PUcpCQA0Go/nVsWlAqNWihJXSQkA0/Cy0zrhpQKR7vX1x3EJAUrgkv0K/aUCWAG/O3+dCQGE0vbQwxWlAJTNloNr0QkACjTc8l8ppQGPRAvRbA0NAWAYUCnXPaUBJF0uhXRNDQHeRQeDI02lAF/rRWNkkQ0BIzEuOkddpQD7xsKTIN0NA8HWH8c3aaUCxLIDpJExDQOtRPfV83WlA2RdUZ+dhQ0BJdNOSnd9pQBEIvzoJeUNAoPH00S7haUAV9dZdg5FDQIvtt8gv4mlABRk/qU6rQ0CyAsKbn+JpQKhVNdVjxkNAe/9qfn3iaUCFO6N6u+JDQN7y3bLI4WlAq48yFE4AREDwhDiKgOBpQNUrZf8TH0RA5paoZKTeaUABFbB9BT9EQJ8miLEz3GlAgKOZtRpgREDvcXfvLdlpQF2X2rNLgkRADFZ1rJLVaUAC9IFskKVEQM3n9YVh0WlAA30bvODJRECWQfcomsxpQE2u2Gg070RAA4QUUjzHaUAdCbwjgxVFQJoGl81HwWlARY/GicQ8RUAOtoV3vLppQPhFJyXwZEVAyp2yO5qzaUDfmGxu/Y1FQMKaxhXhq2lAG3a3zeO3RUCjNUsRkaNpQOf775ua4kVA6aKySaqaaUD+j/sjGQ5GQFnnXeoskWlAEjn0o1Y6RkDcH6EuGYdpQBQSYU5KZ0ZAsOvFYW98aUCsr29L65RGQFf4C98vcWlAKE8uujDDRkCzrqcRW2VpQCSnxrER8kZANwG/dPFYaUARMblChSFHQPpaZJPzS2lAtcMYeIJRR0ARsI8IYj5pQPxVxlgAgkdAf68Vfz0waUDMwazo9bJHQHoXnbGGIWlAfF78KVrkR0DiLJJqPhJpQL5KZh4kFkhAH1cYhGUCaUBWPVfISkhIQIXh+ef88WhAErUxLMV6SEAm5JWPBeFoQFlfh1GKrUhAn1XMg4DPaEB8jVFEkeBIQPJH6NxuvWhAgpEoFtETSUDSUojC0apoQK/Zed9AR0lAkS2Fa6qXaECdpLzA13pJQLZ71h36g2hAbCWl44yuSUDzznUuwm9oQJ7xVXxX4klAguE/AQRbaEDYkY/KLhZKQJEM1AjBRWhAgg7eGgpKSkAM/nHG+i9oQCZSxMfgfUpAmrHVybIZaED6O+U6qrFKQNKwEbHqAmhAEjwq7l3lSkDznmcopOtnQFhU52zzGEtAOxQf6uDTZ0CAWfxUYkxLQOXNWr6iu2dAbU7zV6J/S0AuN+x666JnQNq2GzyrsktAylAlA72JZ0BwvKLddOVLQMz7qEcZcGdAPAOoL/cXTEBYrTlGAlZnQMoKTz0qSkxAA5GGCXo7Z0CM+MwqBnxMQGMf96iCIGdAKqpyNoOtTED9LnVIHgVnQLTtsrmZ3kxAq4U1GE/pZkC1viQqQg9NQNPwflQXzWZAmmeBGnU/TUAQ629FebBmQCBnnjsrb01A8NXCPneTZkCW+mJdXZ5NQKTOkJ8TdmZAvS25bwTNTUDKJBPSUFhmQAtSeoMZ+01AnHpjSzE6ZkBHv1bLlShOQJeVOou3G2ZA4MC4nHJVTkCL560b5vxlQLiSonCpgU5Ar9frkL/dZUDmUYfkM61OQETT9ohGvmVAaMYeuwvYTkBvLl+rfZ5lQFnqM90qAk9As9z7qGd+ZUDEFW5aiytPQAIKojsHXmVAB7UUaidUT0CCnNslXz1lQItxzWv5e09Af6ecMnIcZUBLtVTo+6JPQJDY9zRD+2RAe3EwkinJT0Ch59EH1dlkQFASXUZ97k9AmBKUjSq4ZEBDRXoGeQlQQC+u3a9GlmRAgrDnjEEbUECW1DRfLHRkQMbWj+aVLFBAqDu2kt5RZEAn+ALacz1QQO47xEdgL2RAHNmPRNlNUECsEbWBtAxkQDTuhxrEXVBABmKASd7pY0DRWoBnMm1QQLYNbK3gxmNAwrqPTiJ8UEDWWrjAvqNjQF6viAqSilBAIYBLm3uAY0CbKDHuf5hQQCibXFkaXWNAfmN2ZOqlUECoGx4bnjljQG2XnfDPslBAHK5nBAoWY0B0THEuL79QQIKvXzxh8mJA9lRr0gbLUEDNNCTtps5iQJRl26lV1lBA7K9zQ96qYkDJRgqbGuFQQPU8VW4Kh2JA0JtZpVTrUECZocCeLmNiQCQ7YOEC9VBAgAlGB04/YkAjFQOBJP5QQIuJtdtrG2JAAqaKz7gGUUD7dMZQi/dhQHPwtDG/DlFAV46+m6/TYUAR/8MlNxZRQAsfGfLbr2FA3eqIQyAdUUAOAC6JE4xhQLFlazx6I1FA1J3YlVloYUD0x23bRClRQH0CH0yxRGFAPaEtBYAuUUCN8NjeHSFhQA/M4LcrM1FAXxhXf6L9YEBxBU8LSDdRQF9zCl1C2mBANQnIMNU6UUA5zyulALdgQJ80FnPTPVFALZNjguCTYEBTsW02Q0BRQJXKcRzlcGBA4SpY+CRCUUBIftaXEU5gQM8RnU95Q1FA0Gd6FWkrYEBpbybsQERRQHQIWLLuCGBA/03ilnxEUUCEXEoOS81fQMi5oDEtRFFANuP5TyGJX0DmXu62U0NRQJpqFEhmRV9ArMnrOfFBUUAUZmUKIAJfQHNOIeYGQFFAtz2Fn1S/XkD9nk//lT1RQLLVMQQKfV5AmxM94Z86UUBWa6goRjteQMSuf/8lN1FAN9oA8A76XUBn4kPlKTNRQLNdii9quV1AQR4QNa0uUUB44SmuXXldQGovhaixKVFAwvS5I+85XUBEeRsQOSRRQJJybDgk+1xAtBDdUkUeUUDx8S2EAr1cQKDCHG7YF1FA0BALjo9/XEByDyp19BBRQBarl8vQQlxAXCUCkZsJUUD0EFigywZcQNrj/f/PAVFABE0sXYXLW0AJ83wVlPlQQM6MvT8DkVtA6/mNOerwUECGvO1xSldbQA3/k+jU51BAi2ZJCWAeW0Bd/+iyVt5QQEvoewZJ5lpATMd9PHLUUEB8DMZUCq9aQMwadzwqylBAgxp3yah4WkDxOMh8gb9QQMJraCMpQ1pAhcjK2Xq0UEBdlXsKkA5aQOg61EEZqVBAeTYbD+LaWUA3s8i0X51QQKp5vqkjqFlA5H+rQ1GRUEC0V286WXZZQAs1LRDxhFBAqqpTCIdFWUCKdjdMQnhQQNoeOUGxFVlAyYB2OUhrUED4DiT52+ZYQIJ/4CgGXlBAXlnhKQu5WEBWwjp6f1BQQI86m7JCjFhACd+cm7dCUEDoOXFXhmBYQG7R8QiyNFBArzQTwdk1WEDHKXdLciZQQLmSX3xADFhA5Vk6+fsXUEDQsAT6veNXQFQylLRSCVBAIosljlW8V0D2QEVX9PRPQKqyAXAKlldAjn6BL+zWT0C+l6C533BXQHmX+nmUuE9AkDOAZ9hMV0AtHx/T9JlPQMIZR1j3KVdAPqeV6hR7T0Bo+3lMPwhXQHfYIIL8W09A4qM05rLnVkDi5IBsszxPQGR35qhUyFZAvndSjEEdT0CIexL5JqpWQC5J69Ku/U5AbfASHCyNVkAeezQ/A95OQOKB4DdmcVZAcOWC3Ea+TkDGFt1S11ZWQAF4bcGBnk5Aa0SiU4E9VkCe2KEOvH5OQOpr0wBmJVZANmS37f1eTkA8hvMAhw5WQB+6AJBPP05AuqM+2uX4VUBe+FstuR9OQIIjh/KD5FVA6tABA0MATkAephaPYtFVQNCeU1L14E1AF7+S1IK/VUBoo6hf2MFNQB9o5cblrlVARZMacfSiTUAyOClJjJ9VQHWaUc1RhE1AyGCZHXeRVUCOAFC6+GVNQHpyheWmhFVAUZQ9fPFHTUAa6kghHHlVQMAGM1REKk1AAIdGMNduVUAsXgV/+QxNQGpq51DYZVVAMqkRNBnwTEDc/52gH15VQEkZCaSr00xAgq3sG61XVUC3rb33uLdMQHpLcJ6AUlVANJbvTkmcTEAtYO7imU5VQKB1G79kgUxAwh9ng/hLVUAErElSE2dMQNYrK/mbSlVAbNDeBV1NTECVEPWcg0pVQB6BbclJNExAnHsGp65LVUCOsol94RtMQLMoSS8cTlVAbKOd8isETED6f3Mty1FVQHWdwOcw7UtAouAwebpWVUCLqY8J+NZLQP6STcroXFVAhl0I8YjBS0AWXOa4VGRVQIjqZSLrrEtArKybvfxsVUAYkQAMJplLQO1kyDHfdlVA1KEvBUGGS0AIJrxP+oFVQPYuLk1DdEtA5Cn5MkyOVUAVlQIKNGNLQGuZddjSm1VAfv5oRxpTS0CwWOAejKpVQCIHwfX8Q0tAt0DpxnW6VUCSo/7o4jVLQEa9jHONy1VAzm+e19IoS0CFxWKq0N1VQMCInVnTHEtAWCXx0zzxVUC+EnXn6hFLQCgNATzPBVZATpAZ2R8IS0BW3fcRhRtWQPwp/mR4/0pAWCIzaVsyVkCoCRyf+vdKQKu1ZzlPSlZAROn9d6zxSkBl+ANfXWNWQCD2z7uT7EpALBqVm4J9VkDpJ3QRtuhKQBlhL5a7mFZAKSub+RjmSkB8ZNnbBLVWQOb94c3B5EpA/C3631rSVkD4W/S/teRKQO4yyvy58FZAlxm02PnlSkBCGMdzHhBXQPyHZfeS6EpADjIqboQwV0BpAeHQhexKQBSxYf3nUVdANLfJ7tbxSkAdbowbRXRXQP7dya6K+EpAukT4q5eXV0BBUdRBpQBLQHztonvbu1dA18hrqyoKS0C4R71BDOFXQA6578AeFUtAxAIxoCUHWEA29e4ohSFLQEeWKCQjLlhAKC2AWmEvS0BfeJlGAFZYQFxboJy2PktAsIHQbLh+WEAJO5cFiE9LQMBsAOlGqFhAPtphethhS0A9YNL6ptJYQN1dI66qdUtA0nH4z9P9WEAcCpwhAYtLQNAPwoTIKVlAfaOmIt6hS0BfP7IkgFZZQBY6vMtDuktA3JwXq/WDWUBYcH4DNNRLQMALpgMksllAvk5IfLDvS0BAAxILBuFZQCqzxbO6DExA1mStj5YQWkA6bJHyUytMQNHIBVLQQFpAHg3aS31LTEACL4QFrnFaQDmJDZ03bUxANv4NUSqjWkD8oYuNg5BMQFhAp88/1VpAUDRfjmG1TECsBhYR6QdbQGhv/tnR20xAXOKGmiA7W0Dq/hF01ANNQGJdMufgbltAGDJDKWktTUC2XwNpJKNbQFInEY+PWE1A+Gs+ieXXW0DPA60DR4VNQGSgKakeDVxA3z3drY6zTUAgZrUiykJcQAf/53xl401ArbslSeJ4XECrooQoyhROQDQFvGlhr1xANlXUMLtHTkBYTmHMQeZcQIvWYd42fE5ArOhQtH0dXUA8YShCO7JOQKJSw2APVV1AtLihNcbpTkAUUZkN8YxdQL9c21rVIk9AiCgH9BzFXUAO5JIcZl1PQFbfP0uN/V1AgXlZrnWZT0CmdSBJPDZeQPh7vgwB109AqP3aIiRvXkDPncB+AgtQQKh/oQ0/qF5As+jkBz8rUEBOlVA/h+FeQIiFNE40TFBAXacZ7/YaX0C8sg9j4G1QQJi5LFaIVF9AdIf1PEGQUED+r2GwNY5fQEZ0rrdUs1BAkfngPPnHX0DEP3qUGNdQQK7FZZ/mAGBARnlBeor7UEBUDPH+1R1gQIxdyvWnIFFAtLmV48c6YEDOJvF5bkZRQJ8AzXa5V2BAmcLjX9tsUUCVGFjlp3RgQMfmYOfrk1FABu+RX5CRYEByfvo2nbtRQE5FwBlwrmBA3GdbXOzjUUCdMWRMRMtgQC97kEzWDFJAA/mJNAroYEDY0VTkVzZSQJQ5GBS/BGFAV0Zh6G1gUkB5Wh4yYCFhQL0ivwUVi1JAIDki2+o9YUAB9R3SSbZSQP0JbWFcWmFAEH8szAjiUkDyY1cdsnZhQKS49FtODlNAu2yUbemSYUBu1zrTFjtTQHkefLf/rmFAKlTfbV5oU0AmnFRn8sphQCbfQ1IhllNAvI2a8L7mYUCtOrORW8RTQBZ6SM5iAmJANe3LKAnzU0AqFx2D2x1iQGDA7f8lIlRApYfgmSY5YkAC/6nrrVFUQAJ+qKVBVGJAeWU2rZyBVEAFPRtCKm9iQJ+24vLtsVRAsW2xE96JYkCG55BYneJUQOPC9sdapGJAqNMvaKYTVUAoY8kVnr5iQDVrOJoERVVARxGYvaXYYkCrTC1Ws3ZVQB8Mn4lv8mJAQ7oc862oVUDwniNO+QtjQCvcJLjv2lVA31qu6UAlY0BmP/rccw1WQCTzQ0VEPmNAn4FwijVAVkAltZxUAVdjQGUZBdsvc1ZAIpZaFnZvY0BqKmzbXaZWQALQPZSgh2NAZlQfi7rZVkCfB1jjfp9jQKRs7txADVdAB/Y9JA+3Y0A6D5K360BXQLmPN4NPzmNAHPg/9rV0V0C0o244PuVjQKEQQWmaqFdAee0biNn7Y0B+IInWk9xXQK2UssIfEmRAdA9Q+pwQWECZFQpFDyhkQJClrIewRFhAZo2GeKY9ZEB4tjApyXhYQE5mP9PjUmRAkqaGgeGsWECJXiTYxWdkQOU0ECz04FhAruYgF0t8ZEBNeIa9+xRZQADTPS1ykGRAkPuaxPJIWUBtXMHEOaRkQErlmcrTfFlAZG1NlaC3ZEDHFw1UmbBZQJs3/GOlymRAbTVg4T3kWUBDD3sDR91kQNh0he+7F1pAbYkjVITvZEA2Mpv4DUtaQGvaEkRcAWVA/yeSdC5+WkAhcj/PzRJlQB081NkXsVpAENSM/9cjZUCezOudxONaQPio3ex5NGVAsGcrNi8WW0CXCCS9skRlQHjaVRhSSFtAu/lvpIFUZUA8gka7J3pbQJwn/OTlY2VAYMuZl6qrW0BIyzjP3nJlQJXJVSjV3FtAT8jUwWuBZUDK1JLroQ1cQBD8xCmMj2VAqhUkYws+XEBav0mCP51lQD3uPxUMblxAEpryVIWqZUCAKCiNnp1cQPcooDldt2VAotfRW73MXEDxNYTWxsNlQLLWjBhj+1xAJwMg4MHPZUBC0KphiildQNXJQBlO22VAobol3S1XXUDHbfpSa+ZlQEq0RTlIhF1AcGagbBnxZUCOK0Yt1LBdQHjfvFNY+2VAYj76eczcXUDKEQYEKAVmQJU8cOorCF5AkNdRh4gOZkCQOZRU7TJeQKp8h/V5F2ZA4ZjRmQtdXkBSzo90/B9mQO+Bs6eBhl5AEWxDOBAoZkDsJoR4Sq9eQBpdV4K1L2ZAn8vqE2HXXkCP7Eei7DZmQNZ3iI/A/l5AOdBB9bU9ZkBNQ5MPZCVfQNecCeYRRGZAeCZwx0ZLX0CEi+HsAEpmQDw+S/pjcF9APpRtj4NPZkAqb677tpRfQD3hlWCaVGZAw1UWMDu4X0CDn2cARllmQDhxhQ3s2l9A3jD0G4ddZkB6dhUcxfxfQBLELm1eYWZAtlxD++AOYEDlV8i6zGRmQEzNZiXvHmBA+C0K2NJnZkC9807tii5gQMyyrqRxamZAMA76PbI9YED/37gMqmxmQE87zgxjTGBAsh5KCH1uZkAT49tZm1pgQG6vdpvrb2ZADPceMFloYEDMnRjW9nBmQO8Bv6WadWBAr0Wh059xZkBz/k3cXYJgQJBw6brncWZAkO0FAaGOYEAyEQC+z3FmQH0kBU1immBASKT3GVlxZkCySokFoKVgQBY9shaFcGZAzgApfFiwYECjRKwGVW9mQMooDA+KumBA+vLFRsptZkCbyCIpM8RgQGGJCz7ma2ZAVIBaQlLNYEA7VXxdqmlmQOyM0t/l1WBA7YLQHxhnZkAfUQ6U7N1gQKHIPQkxZGZAHV4m/2TlYECr8Dqn9mBmQJ71985N7GBAjEtCkGpdZkB9/1K/pfJgQNARk2OOWWZA3mwmmmv4YEDOvfHIY1VmQFEDqzee/WBA7GVncOxQZkBYiox+PAJhQKMfABIqTGZALFYRZEUGYUAwdIhtHkdmQIYqQOy3CWFAQu9JSstBZkCscAQqkwxhQPHQxnYyPGZA9btQP9YOYUCP63TIVTZmQFuYP12AEGFAi7Z3GzcwZkDPnjLEkBFhQIyfWVLYKWZAN8vvwwYSYUDTosRVOyNmQEwQvbvhEWFAfDQ6FGIcZkCVJXoaIRFhQKiDyoFOFWZAOIy4XsQPYUBqIcuXAg5mQDfH0RbLDWFAnBSNVIAGZkBQxPvgNAthQKRkErvJ/mVAjnJbawEIYUBuJMPS4PZlQPyDFXQwBGFArAcip8fuZUBVWF3Jwf9gQOOLgEeA5mVAPQ6CSbX6YEBMv7LGDN5lQHW5+eIK9WBAU6/COm/VZUAavGqUwu5gQJOIo7ypzGVArUGzbNznYEBkcuRnvsNlQH7a7opY4GBA3S9jWq+6ZUC3NnoeN9hgQEyQ/rN+sWVA7v/0ZnjPYEA+uUiWLqhlQBzRQbQcxmBA+1M5JMGeZUBATIRmJLxgQMqo34E4lWVA5E0d7o+xYEChsRTUlotlQJI+pctfpmBAxy4tQN6BZUB8guSPlJpgQPzGq+sQeGVAGwjK2y6OYECyPfP7MG5lQIf2X2AvgWBA0Mj4lUBkZUCCfL7elnNgQIuQ9t1BWmVAosH8J2ZlYEDfYh73NlBlQAb7Hx2eVmBA4KRMAyJGZUBOpQivP0dgQL+LuyIFPGVA0eVd3ks3YEBEp7Zz4jFlQFQVd7vDJmBAxcdOErwnZUB4dkNmqBVgQDNJDhiUHWVAthowDvsDYEAkza2bbBNlQDDxF+R5419AUG3JsEcJZUCqbNS/3r1fQGdwlmcn/2RAWFgFaieXX0BvipnMDfVkQEZDIbtWb19Ae7Jd6PzqZED6k1+pb0ZfQOKWK7/24GRAXAZvSHUcX0BUucFQ/dZkQOS3KMlq8V5AJTwNmBLNZEAGyj95U8VeQLJp44o4w2RAoqTtwjKYXkAo/7sZcblkQCHhmiwMal5AgkNsL76vZEB66YRY4zpeQHnz4rAhpmRAoVNgBLwKXkBUC+V8nZxkQCAG+Aia2V1AD3fLazOTZEBOL8lZgaddQGOxQU/liWRAfhucBHZ0XUAGWgXytIBkQPb1GTF8QF1Ae8umF6R3ZECegF8gmAtdQGG4Sny0bmRAQc+MLM7VXECO2GzU52VkQJgTUsgin1xAV62jzD9dZECqhnl+mmdcQEZlZQm+VGRAQH5u8TkvXEBS5s0mZExkQLi7wdoF9ltAVwZmuDNEZEDEAqsKA7xbQOT460guPGRA5AaIZzaBW0CS+RxaVTRkQLW+WO2kRVtAyDmAZKosZEANLDmtUwlbQLYYM9cuJWRAIKnYzEfMWkA5rLYX5B1kQNrJ7oWGjlpA+qC+gcsWZEDI4q0lFVBaQDV4AWfmD2RA1UQzDPkQWkAZKQoPNglkQLo/9as30VlA1isLt7sCZECc+y6J1pBZQP7zspF4/GNAXztKOdtPWUDO3wHHbfZjQHcZR2JLDllAwaEhdJzwY0Cl0CG6LMxYQKEpPqsF62NASKM2BoWJWED/EWBzquVjQLnyoxpaRlhAB5dIyIvgY0DEmqrZsQJYQA8bT5qq22NAlqEMM5K+V0CMPUDOB9djQOhRaiMBeldAZYg+PaTSY0A40J2zBDVXQMm3pLSAzmNAnD8V+KLvVkBDoOn1ncpjQEiJKxDiqVZAr7aFtvzGY0AC238lyGNWQFU92p+dw2NA9+5La1sdVkCGGRpPgcBjQAszuR2i1lVAdVQ0Vai9Y0BZ4TSBoo9VQDpKwDYTu2NATiDD4WJIVUByietrwrhjQEY9UZLpAFVAy2VpYLa2Y0DNFgfsPLlUQJU/ZHPvtGNAjcqXTWNxVEBKgnD3bbNjQOW8kRpjKVRAlVuBMjKyY0BwDK66QuFTQG4s3108sWNAjYkfmQiZU0BjtR+mjLBjQDtD4SO7UFNAIP8fKyOwY0CMwQTLYAhTQAAAAAAAsGNAAAAAAADAUkAg/x8rI7BjQHo++zSfd1JAY7UfpoywY0DLvB7cRC9SQG4s3108sWNAeXbgZvfmUUCVW4EyMrJjQJbzUUW9nlFASYJw922zY0AiQ27lnFZRQJU/ZHPvtGNAejVospwOUUDKZWlgtrZjQDnp+BPDxlBAconra8K4Y0DAwq5tFn9QQDpKwDYTu2NAud88Hp03UEB1VDRVqL1jQFo9lv264E9AhhkaT4HAY0D2mY3Eu1JPQFU92p+dw2NAHSJoKUnFTkCutoW2/MZjQAlKALVvOE5AQqDp9Z3KY0B87ajfO6xNQMm3pLSAzmNA1IDVD7ogTUBkiD49pNJjQJxfxJj2lUxAjD1AzgfXY0A7XCu5/QtMQA8bT5qq22NA4LzmmduCS0AGl0jIi+BjQITKqkyc+kpA/xFgc6rlY0CaGrjKS3NKQKApPqsF62NAfLmS8/XsSUDBoSF0nPBjQMJevIumZ0lAzd8Bx232Y0AezXE7aeNIQP7zspF4/GNATolrjUlgSEDVKwu3uwJkQNQIou1S3kdAGSkKDzYJZECYgBWokF1HQDR4AWfmD2RAYXaZ5w3eRkD6oL6ByxZkQHo6pLTVX0ZAOKy2F+QdZEBXbCL08uJFQLYYM9cuJWRAyq1OZnBnRUDHOYBkqixkQPGnjaVY7URAkvkcWlU0ZEChgk4ltnREQOT460guPGRAQvLvMJP9Q0BWBma4M0RkQIP6qer5h0NAUubNJmRMZECbiHxK9BNDQEVlZQm+VGRAiQMjHYyhQkBWraPMP11kQLjyDAPLMEJAjdhs1OdlZEDb2FtvusFBQGC4Sny0bmRAiWHmpmNUQUB6y6YXpHdkQMv+QL/P6EBABVoF8rSAZEAcFMydB39AQGOxQU/liWRADMnH9hMXQEAOd8trM5NkQNhC25j6YT9AVAvlfJ2cZECQ5x/cl5k+QHjz4rAhpmRAjLF+7g/VPUCCQ2wvvq9kQCha7J1yFD1AKP+7GXG5ZECKe5RNz1c8QLFp44o4w2RAhG1J9DSfO0AkPA2YEs1kQPLXABuy6jpAVLnBUP3WZECCIF3bVDo6QOCWK7/24GRAnuZD3iqOOUB6sl3o/OpkQCiwgVpB5jhAboqZzA31ZED08noTpUI4QGZwlmcn/2RAqp7qV2KjN0BPbcmwRwllQGZNrgCFCDdAJM2tm2wTZUBMO6BvGHI2QDJJDhiUHWVAYCp/jifgNUDEx04SvCdlQE5M5M28UjVAQ6e2c+IxZUBkVUck4sk0QL6LuyIFPGVAgtEQDaFFNEDfpEwDIkZlQJ7VuocCxjNA3mIe9zZQZUDcJwAXD0szQIqQ9t1BWmVA+PIZwM7UMkDQyPiVQGRlQPgbDApJYzJAsj3z+zBuZUDQSwD9hPYxQPvGq+sQeGVAMr+vIYmOMUDGLi1A3oFlQCbs24BbKzFAoLEU1JaLZUB0C9aiAc0wQMmo34E4lWVA6JAVj4BzMED6UzkkwZ5lQAye3cvcHjBAPblIli6oZUBM7uK7NJ4vQEuQ/rN+sWVALAGwkHkIL0DdL2Nar7plQJiUXBiOfC5AY3LkZ77DZUAsWBJRd/otQJKIo7ypzGVAQOXLNDmCLUBSr8I6b9VlQHg+VLnWEy1ATL+yxgzeZUC8aGTQUa8sQOOLgEeA5mVAPBzfZ6tULECsByKnx+5lQLh6KmrjAyxAbiTD0uD2ZUBEwKe++LwrQKNkErvJ/mVAMNdISul/K0CcFI1UgAZmQAC7Q/CxTCtAaiHLlwIOZkCUjOOSTiMrQKiDyoFOFWZAfDx3FLoDK0B7NDoUYhxmQKymXVju7SpA0qLEVTsjZkBA+y5E5OEqQIufWVLYKWZAkEwDwZPfKkCLtncbNzBmQBAT1rzz5ipAj+t0yFU2ZkBMegYs+vcqQPHQxnYyPGZArED0CpwSK0BB70lKy0FmQDz1uF/NNitAMHSIbR5HZkCkV/07gWQrQKMfABIqTGZAOJ3qvqmbK0DsZWdw7FBmQIBaNxc43CtAzr3xyGNVZkDkyk+FHCYsQNARk2OOWWZAEDKZXUZ5LECMS0KQal1mQCwI0Aqk1SxAq/A6p/ZgZkAgpoAQIzstQKHIPQkxZGZAHB6aDbCpLUDtgtAfGGdmQAzuGr82IS5AO1V8XappZkAwMdcCoqEuQGGJCz7ma2ZAuPpX2tsqL0D68sVGym1mQER2023NvC9Ao0SsBlVvZkCsuZ6HryswQBY9shaFcGZAhPm3Hjx9MEBIpPcZWXFmQGiqtdP/0jBAMhEAvs9xZkAQ3NaX7SwxQJBw6brncWZAeJPQ9/eKMUCvRaHTn3FmQGIMkB0R7TFAzJ0Y1vZwZkCA8AfSKlMyQG6vdpvrb2ZAmEcIfza9MkCyHkoIfW5mQFznIDElKzNA/9+4DKpsZkB6JY6Z55wzQMyyrqRxamZAdo4vEG4SNED5LQrY0mdmQBJiiJWoizRA5VfIusxkZkCSlcnUhgg1QBLELm1eYWZARBrlJfiINUDfMPQbh11mQAomqo/rDDZAg59nAEZZZkASO+rJT5Q2QD3hlWCaVGZA6qimPxMfN0A/lG2Pg09mQExDRhEkrTdAhIvh7ABKZkAEB9MWcD44QNicCeYRRGZAFGY/4uTSOEA60EH1tT1mQMLyssFvajlAjuxHouw2ZkCcIN7B/QQ6QBtdV4K1L2ZAdtFUsHuiOkASbEM4EChmQEJk7x3WQjtAU86PdPwfZkA4+DFh+eU7QKt8h/V5F2ZAcJy5mNGLPECR11GHiA5mQLAZr61KND1AyxEGBCgFZkCeDT9WUN89QHjfvFNY+2VAaAYXGM6MPkBxZqBsGfFlQLZR50qvPD9AyG36UmvmZUDELuka3+4/QNbJQBlO22VAtoq0RaRRQEAoAyDgwc9lQHNfqjzrrEBA8jWE1sbDZUCUUubOOQlBQPgooDldt2VAs1BcSIVmQUATmvJUhaplQPqur+XCxEFAW79Jgj+dZUB/I4DV5yNCQBH8xCmMj2VApNS3OemDQkBQyNTBa4FlQGRW2ii85EJAS8s4z95yZUC8bFSvVUZDQJ0n/OTlY2VANmnM0KqoQ0C4+W+kgVRlQJP7comwC0RAmAgkvbJEZUAHS1TPW29EQPqo3ex5NGVAmDCpk6HTREAS1Iz/1yNlQLpmKMR2OEVAJnI/z80SZUCth1dM0J1FQG3aEkRcAWVA+K/bFqMDRkBviSNUhO9kQIybyQ7kaUZARQ97A0fdZEBJFvUgiNBGQJ03/GOlymRAHJU/PYQ3R0BrbU2VoLdkQFPQ5VfNnkdAbFzBxDmkZEB0NcxqWAZIQP7SPS1ykGRA6gjKdhpuSECv5iAXS3xkQFwP84QI1khAi14k2MVnZEAslt+nFz5JQFNmP9PjUmRAwLLy/DymSUBrjYZ4pj1kQPSSnq1tDkpAmBUKRQ8oZEDptKbwnnZKQK+UssIfEmRADuFfC8beSkB77RuI2ftjQPq+7VLYRktAtqNuOD7lY0C03n0ty65LQMGPN4NPzmNAqQ+AE5QWTEAG9j0kD7djQJXh25AofkxAnQdY436fY0DAJiNGfuVMQATQPZSgh2NAK1fB6YpMTUAklloWdm9jQCKrJ0lEs01AK7WcVAFXY0AazfVJoBlOQCbzQ0VEPmNAufwe65R/TkDbWq7pQCVjQEKBC0YY5U5A8p4jTvkLY0CiR7aPIEpPQCEMn4lv8mJAb4vGGaSuT0BJEZi9pdhiQFCz0qlMCVBALmPJFZ6+YkC+lMdl+zpQQOXC9sdapGJAVCzQl1lsUECzbbET3oliQHUYb6dinVBACD0bQipvYkBdSR0NEs5QQAR+qKVBVGJAg5rJUmP+UECnh+CZJjliQPoAVhRSLlFAMRcdg9sdYkCUPxIA2l1RQBh6SM5iAmJAxxI01/aMUUC+jZrwvuZhQE/FTG6ku1FAKJxUZ/LKYUDXILyt3ulRQHsefLf/rmFA0qsgkqEXUkDDbJRt6ZJhQIcoxSzpRFJA72NXHbJ2YUBfRwuksXFSQAAKbWFcWmFA7IDTM/edUkAiOSLb6j1hQPwK4i22yVJAfFoeMmAhYUBA3UD66vRSQJ05GBS/BGFAm7meF5IfU0AA+Yk0CuhgQCsuqxuoSVNAmzFkTETLYEDUhG+zKXNTQFFFwBlwrmBAIZikoxOcU0AJ75FfkJFgQIuBBclixFNAlxhY5ad0YEA2GZ8YFOxTQKEAzXa5V2BAZD0coCQTVECwuZXjxzpgQDnZDoaROVRAVgzx/tUdYEBxojUKWF9UQLDFZZ/mAGBAt4a+hXWEVECX+eA8+cdfQDnAhWvnqFRADbBhsDWOX0Cyi1FIq8xUQJK5LFaIVF9AjngKw77vVEBYpxnv9hpfQEdN8JwfElVAUpVQP4fhXkB2esuxyzNVQK1/oQ0/qF5AShcb+MBUVUCu/doiJG9eQC5iP4H9dFVAtHUgSTw2XkD9waB5f5RVQFrfP0uN/V1APUPTKEWzVUCNKAf0HMVdQPaNtvFM0VVAGFGZDfGMXUCeUZJSle5VQKZSw2APVV1AoyMv5RwLVkC56FC0fR1dQFvP617iJlZAUk5hzEHmXEC8FM+Q5EFWQC8FvGlhr1xAZtWVZyJcVkCyuyVJ4nhcQKiuveuadVZAJGa1IspCXEB6AIxBTY5WQHSgKakeDVxAB2ERqTimVkAGbD6J5ddbQBN+KX5cvVZAsl8DaSSjW0BYbHc4uNNWQGZdMufgbltA8mZea0vpVkBh4oaaIDtbQIkA98UV/lZAsAYWEekHW0BKyAATFxJXQGRAp88/1VpA1GXQOE8lV0Aw/g1RKqNaQAcvOjm+N1dABi+EBa5xWkBiO3kxZElXQNbIBVLQQFpAcPkSWkFaV0DaZK2PlhBaQOJJtwZWaldATAMSCwbhWUBoJh2monlXQLwLpgMksllAotjbwSeIV0DXnBer9YNZQNTHQP7llVdAYj+yJIBWWUD04iEa3qJXQNQPwoTIKVlAQK6s7hCvV0DWcfjP0/1YQPH6MW9/uldASGDS+qbSWEAQUe6oKsVXQMRsAOlGqFhA4BLPwhPPV0C0gdBsuH5YQHtiNP072FdAYniZRgBWWEBS0q+xpOBXQEuWKCQjLlhAa+m/Uk/oV0DOAjGgJQdYQGSFiGs971dAtUe9QQzhV0B5I4ifcPVXQHjtonvbu1dAlBtKqur6V0C9RPirl5dXQF/XFV+t/1dAIG6MG0V0V0AAEZuougNYQB6xYf3nUVdAYSSbiBQHWEAWMipuhDBXQEx/jxe9CVhAPxjHcx4QV0ACPE2EtgtYQPIyyvy58FZANfOlEwMNWED/LfrfWtJWQAfSBSClDVhAgGTZ2wS1VkAOAQ8Znw1YQCBhL5a7mFZAbGoyg/MMWEAqGpWbgn1WQA/sRfekC1hAYvgDX11jVkDwBBgitglYQK21ZzlPSlZAXgsBxCkHWEBaIjNpWzJWQC37cbACBFhAXN33EYUbVkAE64DNQwBYQCoNATzPBVZA2jdzE/D7V0BWJfHTPPFVQKB2RYwK91dAhsViqtDdVUCgOzFTlvFXQEi9jHONy1VAGsgwlJbrV0C5QOnGdbpVQDiugIsO5VdAtFjgHoyqVUByfB+FAd5XQGyZddjSm1VAwoBL3HLWV0DmKfkyTI5VQHa1/vplzldACia8T/qBVUCG6GhZ3sVXQO5kyDHfdlVAFy9ofd+8V0CsrJu9/GxVQHW3//lss1dAGVzmuFRkVUC/Cs1uiqlXQP+STcroXFVAPtF7hzufV0Ci4DB5ulZVQDsrOPuDlFdA+39zLctRVUBGsR+MZ4lXQLQoSS8cTlVASy6xBup9V0CcewanrktVQLomO0EPcldAlBD1nINKVUBvP0kb22VXQNUrK/mbSlVAyJcQfVFZV0DCH2eD+EtVQP8p21Z2TFdALWDu4plOVUAyRXKgTT9XQHlLcJ6AUlVA6jSIWNsxV0CCrewbrVdVQCMpIYQjJFdA3P+doB9eVUBdc/stKhZXQGlq51DYZVVAZSv3ZfMHV0D+hkYw125VQOpQfUCD+VZAGOpIIRx5VUCgfObV3epWQHhyheWmhFVA3DXhQQfcVkDHYJkdd5FVQLP/16IDzVZALzgpSYyfVUDGMlcZ171WQBxo5cblrlVAWrZyx4WuVkASv5LUgr9VQE2uK9ATn1ZAGKYWj2LRVUCZMNZWhY9WQIAjh/KD5FVAiBd/ft5/VkC+oz7a5fhVQNMDUmkjcFZAN4bzAIcOVkDxov83WGBWQOpr0wBmJVZA6k0kCYFQVkBpRKJTgT1WQLYTr/ihQFZAwhbdUtdWVkAIREkfvzBWQOGB4DdmcVZARo2+kdwgVkBu8BIcLI1WQHbCZWD+EFZAinsS+SaqVkBqW4qWKAFWQGR35qhUyFZAJsTWOV/xVUDbozTmsudWQJCNv0mm4VVAa/t5TD8IV0DDk+++AdJVQMEZR1j3KVdAXiy1inXCVUCOM4Bn2ExXQGtwcJYFs1VAvJegud9wV0BCtALDtaNVQKCyAXAKlldAvUA/6ImUVUAaiyWOVbxXQIZfXdSFhVVA0rAE+r3jV0CuzWtLrXZVQLiSX3xADFhAGabFBgRoVUCmNBPB2TVYQDrWiLSNWVVA4jlxV4ZgWECQLg73TUtVQH86m7JCjFhA+yBjZEg9VUBfWeEpC7lYQKU9xYWAL1VA9A4k+dvmWEB/gB/X+SFVQNIeOUGxFVlANX+JxrcUVUCgqlMIh0VZQHeJyLO9B1VApFdvOll2WUD2ytLvDvtUQKh5vqkjqFlAGoBUvK7uVECBNhsP4tpZQMpMN0ug4lRAUpV7CpAOWkAZxSu+5tZUQMJraCMpQ1pAfzc1JoXLVECFGnfJqHhaQBDHN4N+wFRAcgzGVAqvWkA65YjD1bVUQEnoewZJ5lpAsziCw42rVECKZkkJYB5bQKYAF02poVRAiLztcUpXW0D0AGwXK5hUQMyMvT8DkVtAGQZyxhWPVED4TCxdhctbQPgMg+prhlRA+BBYoMsGXEAlHAIAMH5UQBirl8vQQlxApdr9bmR2VEDMEAuOj39cQI7w1YoLb1RA8PEthAK9XEBePeORJ2hUQINybDgk+1xATu8irbphVEC29Lkj7zldQLyG5O/GW1RAeuEprl15XUCX0HpXTlZUQLFdii9quV1AveHvylJRVEAx2gDwDvpdQJkdvBrWTFRATWuoKEY7XkA6UYAA2khUQKLVMQQKfV5AZuzCHmBFVEC4PYWfVL9eQABhsABqQlRAEmZlCiACX0CMsd4Z+T9UQJBqFEhmRV9AUjYUxg4+VEAo4/lPIYlfQBmhEUmsPFRAblxKDkvNX0A2Rl/O0jtUQHAIWLLuCGBAAbIdaYM7VEDVZ3oVaStgQJmQ2RO/O1RAQn7WlxFOYEAw7mKwhjxUQJTKcRzlcGBAINWnB9s9VEAuk2OC4JNgQK1Oksm8P1RAM88rpQC3YEBiy+mMLEJUQF5zCl1C2mBAyvY3zypFVEBeGFd/ov1gQI/6sPS3SFRAhvDY3h0hYUDwMx9I1ExUQHwCH0yxRGFAxF7S+n9RVEDNndiVWWhhQAo4kiS7VlRAEQAuiROMYUBPmpTDhVxUQAwfGfLbr2FAIxV3vN9iVEBUjr6br9NhQO8APNrIaVRA+nTGUIv3YUCMD0vOQHFUQIiJtdtrG2JA/Vl1MEd5VEB5CUYHTj9iQNzq/H7bgVRAnKHAni5jYkDdxJ8e/YpUQPQ8VW4Kh2JAMGSmWquUVEDpr3ND3qpiQDa59WTlnlRAyTQk7abOYkBqmiRWqqlUQHmvXzxh8mJAB6uULfm0VEAermcEChZjQIyzjtHQwFRAqBseG545Y0CTaGIPMM1UQCabXFkaXWNAgpyJmxXaVEAagEube4BjQGLXzhGA51RAzVq4wL6jY0CgUHf1bfVUQLINbK3gxmNAPEVwsd0DVUAMYoBJ3uljQDClf5jNElVApRG1gbQMZEDJEXjlOyJVQO47xEdgL2RA4yZwuyYyVUChO7aS3lFkQNYH/SWMQlVAj9Q0Xyx0ZEA1KXAZalNVQC2u3a9GlmRAfk8Yc75kVUCcEpSNKrhkQL+6hfmGdlVAmefRB9XZZEDUdtFcwYhVQI7Y9zRD+2RAQcfnNmubVUB5p5wychxlQFil1QuCrlVAe5zbJV89ZUA0RxlKA8JVQAQKojsHXmVAfKX1SuzVVUCy3PuoZ35lQBz1yFI66lVAby5fq32eZUDUCmaR6v5VQEPT9ohGvmVAypxwIvoTVkCo1+uQv91lQAlXvA1mKVZAjuetG+b8ZUCntq5HKz9WQJWVOou3G2ZAkJ+jsUZVVkCaemNLMTpmQFqgVBq1a1ZAySQT0lBYZkD71kI+c4JWQJzOkJ8TdmZAG2kjyH2ZVkDq1cI+d5NmQLGCTlHRsFZAEOtvRXmwZkBwzDBiashWQNDwflQXzWZANEy/ckXgVkClhTUYT+lmQKKg7epe+FZA+C51SB4FZ0AmiSYjsxBXQFsf96iCIGdA5arGZD4pV0AIkYYJejtnQLqDmer8QVdAVa05RgJWZ0Caeljh6lpXQMz7qEcZcGdAXv4raAR0V0DEUCUDvYlnQMShLpFFjVdAKjfseuuiZ0CLJPJhqqZXQOPNWr6iu2dAy1gG1C7AV0A+FB/q4NNnQELTgdXO2VdA7Z5nKKTrZ0DRVYxJhvNXQNCwEbHqAmhA9OHqCFENWECWsdXJshloQP5hjeIqJ1hACP5xxvovaEDk1h2cD0FYQJMM1AjBRWhAvfiQ8vpaWECC4T8BBFtoQBA3uJrodFhA8851LsJvaEAxB9VB1I5YQLZ71h36g2hAR20tjrmoWECMLYVrqpdoQK6toR+UwlhA01KIwtGqaEArE0OQX9xYQPRH6NxuvWhAPrfrdBf2WECeVcyDgM9oQD851123D1lAJOSVjwXhaEBVUDzXOilZQIHh+ef88WhAcCXnaZ1CWUAbVxiEZQJpQFJh1JvaW1lA5CySaj4SaUCj2szw7XRZQHgXnbGGIWlAxNAB69KNWUB8rxV/PTBpQBmfqQuFpllADrCPCGI+aUAD1ZzT/75ZQPZaZJPzS2lAIJ7zwz7XWUA5Ab908VhpQHhno14971lAsa6nEVtlaUBurBwn9wZaQFj4C98vcWlAZ9joomceWkCu68Vhb3xpQCgoSFqKNVpA3B+hLhmHaUDvds/YWkxaQFjnXeoskWlAc+MFrtRiWkDqorJJqpppQAA4Am7zeFpAoDVLEZGjaUAOAgiyso5aQMKaxhXhq2lA70QkGQ6kWkDKnbI7mrNpQI+zyUgBuVpADLaFd7y6aUD8XGzth81aQJsGl81HwWlAWrgcu53hWkADhBRSPMdpQHD7IW4+9VpAlkH3KJrMaUDSqJPLZQhbQMzn9YVh0WlAe0HyoQ8bW0ALVnWsktVpQAAGv8k3LVtA73F37y3ZaUBUtBIm2j5bQJ8miLEz3GlAPS4zpfJPW0DmlqhkpN5pQH71J0F9YFtA8IQ4ioDgaUAYak0AdnBbQN/y3bLI4WlAKLjm9dh/W0B6/2p+feJpQDxirkKijltAsgLCm5/iaUAuVWUVzpxbQIrtt8gv4mlAgnNgq1iqW0Cf8fTRLuFpQHaFFFE+t1tASHTTkp3faUD7e6Bie8NbQOtRPfV83WlAFPRVTAzPW0DwdYfxzdppQKbpP4vt2VtAR8xLjpHXaUBkh6etG+RbQHiRQeDI02lA8gKXU5PtW0BZBhQKdc9pQFp0Wi9R9ltABI03PJfKaUBJl/4FUv5bQGI0vbQwxWlAbmbNr5IFXEBTuCS/Qr9pQLZ/yBgQDFxANPwstM64aUAuQiFBxxFcQA4Go/nVsWlAKpWuPbUWXECggC8CWqppQH1JYDjXGlxA6MoiTVyiaUBSBbBwKh5cQMObP2bemWlAKqwPPKwgXEB2QITl4ZBpQNQzVQZaIlxAKn3ybmiHaUCI2CNSMSNcQJQWVrJzfWlA+KFSuS8jXEAmDAprBXNpQIgrUO1SIlxA/Im8Xx9oaUD6ooO3mCBcQJ6aMWLDXGlAvu+q+f4dXEAWoARP81BpQPH2Na6DGlxAPJxoDbFEaUBO757oJBZcQKRQ547+N2lArrm/1eAQXEDuPR/P3SppQOgxJLy1ClxAO4uA01AdaUBibVn8oQNcQBLeCKtZD2lAUNw5EaT7W0BHK/5t+gBpQBZDNpC68ltAnImoPTXyaEA6gpsp5OhbQB8PC0QM42hAmCLVqB/eW0AewpuzgdNoQDSdrPRr0ltAoqb6xpfDaEBmVYUPyMVbQLzxp8BQs2hABj2VFzO4W0Dxa7nqrqJoQOwaGkesqVtAJgyPlrSRaECUa4v0MppbQCnUhhxkgGhABNXIksaJW0Dk+K/bv25oQBYnRbFmeFtAhF99OcpcaEAf4i38EmZbQEV5d6GFSmhAWj2PPMtSW0C4h+2E9DdoQCyodFiPPltAdlOmWhklaEDawAVTXylbQNZekJ72EWhAdryfTDsTW0Agn3HRjv5nQHw764Ij/FpAVsWWeOTqZ0Aeh+5QGORaQLQhgh361mdAyDQcLxrLWkA4K5pN0sJnQBMsXrMpsVpAt7XXmW+uZ0CvDR2RR5ZaQEbgc5bUmWdAHvhDmXR6WkDFxpXaA4VnQBaqQLqxXVpAAAAAAABwZ0AAAAAAAEBaQGfzvaLLWmdAws3mk2AhWkDeD9FgaUVnQJYSx7zUAVpAhO7d2dsvZ0AEidHeXeFZQGBr2a4lGmdAz5KDe/2/WUA+vrWBSQRnQPWBkTG1nVlA4p0P9UnuZkD0QM28hnpZQCp426sp2GZA7FsJ9nNWWUAQyRJJ68FmQJRt+NJ+MVlAfZphb5GrZkAk8ghmqQtZQOE31MAelWZANIU93vXkWECwHoXelX5mQHOMAYdmvVhA6zZLaPlnZkBnVfrH/ZRYQMFcaPxLUWZAfqnUJL5rWEAfRjg3kDpmQH7eDj2qQVhAx83fssgjZkAeab/LxBZYQMGt/Ab4DGZA4vZXpxDrV0Afs1XIIPZlQH4WZcGQvldAAHWLiEXfZUCldUomSJFXQEiYydVoyGVAfrv7/DljV0DsqXg6jbFlQHkHsoZpNFdAH5nwPLWaZUCwHJ4e2gRXQP7aK1/jg2VAb0KXOY/UVkA6QXseGm1lQBjjxmWMo1ZABY0681tWZUAe81BK1XFWQP3GhVCrP2VAjCj5pm0/VkAWZu+jCillQGYPxVNZDFZAcU03VXwSZUA/A5tAnNhVQBKrAsYC/GRAeBvedDqkVUAzv5RRoOVkQAITBw84b1VAqJWITFfPZEBwOTpEmTlVQIK5iwQquWRAqHjaX2IDVUA06xnAGqNkQFN7GcOXzFRAvOE5viuNZECrAYXkPZVUQOAeOzZfd2RAe3GRT1ldVECP3nRXt2FkQDyvIaTuJFRAzygGSTZMZEAJTQyWAuxTQMUOlyneNmRA9B+e7JmyU0BYGRsPsSFkQGpHGoK5eFNA2fGUBrEMZEBluDdDZj5TQA1L2xPg92NAKlmcLqUDU0BEEl8xQONjQMK+VVR7yFJAR+/yT9POY0C3m0/V7YxSQJUZlFabumNARO/H4gFRUkDCiTQimqZjQDUGwb28FFJAYI2GhdGSY0AVXnG2I9hRQJ7DyUhDf2NAbnuxKzybUUCfiJkp8WtjQFnEZ4oLXlFAX9W82txYY0CVcPJMlyBRQOGZ9wMIRmNA/KCP+uTiUEAjl91BdDNjQI2wwyb6pFBAS76mJSMhY0BS0b1w3GZQQJwZBTUWD2NABwa7gpEoUEDkRPzpTv1iQFEbzyI+1E9ARnm6ss7rYkB9oX22FVdPQLoxc/GW2mJAAs3TT7XZTkCwazv8qMliQLGDOY8oXE5Ap4jnHAa5YkAWOOQke95NQEvU6pCvqGJALV+Mz7hgTUCwsjiJpphiQNSsIFvt4kxAW3onKuyIYkAsQHefJGVMQEr9VIuBeWJAVOf8fmrnS0AKxIy3Z2piQPKiYuXKaUtAa/2vrJ9bYkCCj0nGUexKQBkmn1sqTWJA8mHtGwtvSkCiaSWoCD9iQBuazeUC8klA6L/laDsxYkAumlUnRXVJQE7ISWfDI2JAvcaD5t34SEDMZHJfoRZiQJjcjyrZfEhAXRYqANYJYkClpJD6QgFIQNAb2eph/WFA1C0hXCeGR0BYVHuzRfFhQAy4BVKSC0dAmOaX4IHlYUABetDaj5FGQOSsOusW2mFAl2qG7ysYRkD3Zu8+Bc9hQE84RIJyn0VAZ7G+OU3EYUBPl+N8bydFQMrCLCzvuWFAZhChvy6wREA67jlZ669hQON6wh+8OURA+Opk9kGmYUAjSz5mI8RDQADgrivznGFActxjTnBPQ0AvM6ET/5NhQHXkhISu20JAQRpVu2WLYUDQM6Ck6WhCQOzsfCIng2FAzfINOS33QUDUNW87Q3thQGd9LbmEhkFABYEz67lzYUDCCxWI+xZBQL/lkAmLbGFA601D85yoQEAuSR5htmVhQGMiUzF0O0BA7FdUrztfYUCmIWPBGJ8/QEQzoaQaWWFALGGsDOHJPkCUz33kUlNhQFxF/BhX9z1AcQCFBeRNYUAiidOCkCc9QJUvjJHNSGFAWo06rKJaPECjur0FD0RhQMKQSLqikDtAYPS00qc/YUAGSbGSpck6QDXFm1yXO2FA6B9Z2b8FOkDj5kn73DdhQGBm8O0FRTlAObdl+nc0YUACxJXpi4c4QPKchplnMWFA9i2AnGXNN0A++lgMqy5hQCissIumFjdA06fDekEsYUD2NKzuYWM2QKvzDgEqKmFAuOc9raqzNUDEHQ2wYyhhQMjpQV2TBzVAnk1Eje0mYUA0KnlALl80QLL6GZPGJWFAwlBmQo26M0Dzwf+w7SRhQB4bNPbBGTNAfaKhy2EkYUB0aaWU3XwyQO+aFb0hJGFAnDcP+vDjMUBHoQxVLCRhQLzHXKQMTzFAf+4EWYAkYUBENR6xQL4wQGqXfYQcJWFA4LOh25wxMECjayuJ/yVhQMhdL/ZgUi9AtBMvDygnYUAIEYQBFUouQCdnTLWUKGFAUF5d7HJKLUAX8yIRRCphQHB/+/aWUyxAA6pnrzQsYUBU12aLnGUrQMi0HxRlLmFAnK8rOp6AKkBWXdy60zBhQCxNLre1pClA+Ar4Fn8zYUBYu5fW+9EoQE1I1JNlNmFAJLbbiYgIKECHyhiVhTlhQDQH2NxySCdAxnHzdt08YUAEuA3z0JEmQAg6WY5rQGFAeGz0BLjkJUAkFEgpLkRhQOg4aF08QSVANJ4JjyNIYUAASDJXcackQH2ydgBKTGFAaJysWmkXJEAVxDu4n1BhQDA3gds1kSNAMAAe6yJVYUAQ7IRW5xQjQDErQcjRWWFAqCeuT42iIkD6L255ql5hQKDmJ1A2OiJAilhaI6tjYUB4G4Hk79shQHom7+XRaGFAgLz4msaHIUDBwZLcHG5hQAix5gHGPSFAjfRwHopzYUD4z0Gm+P0gQGqqxL4XeWFAEB5DEmjIIEAo6SHNw35hQHh5JswcnSBAuznAVYyEYUAg2ghVHnwgQKF3xWFvimFAWErkJ3NlIECu/JD3apBhQJi6qbggWSBAkx8HG32WYUAIzXhzK1cgQEP73M2jnGFAwLL1u5ZfIEA8deQP3aJhQBgxvexkciBA/nhY3yapYUA44fZWl48gQF9eKTl/r2FAMLoFQi63IEAOcUkZ5LVhQGDvVuso6SBAEo/5elO8YUAILE+GhSUhQKHUFVnLwmFAkC9WPEFsIUC6SmKuSclhQMDKAC1YvSFAGI/XdczPYUCwO1luxRgiQAhr76pR1mFAkN9FDYN+IkB3T/FJ19xhQKAyDg6K7iJAGqw+UFvjYUCQDP5s0mgjQKcXn7zb6WFAOAonH1PtI0BnP4yPVvBhQAAOQBMCfCRA/JN9y8n2YUAgv6Iy1BQlQMupM3Uz/WFAAOhmYr23JUDJQgOUkQNiQCCWm4SwZCZAUvgfMuIJYkBI1515nxsnQBR75lwjEGJAHOyMIXvcJ0BCXyYlUxZiQKzE210zpyhAyWtrn28cYkDslv8St3spQKdiRuR2ImJA7GI7KvRZKkAFOZUQZyhiQJAniJPXQStAkbXKRT4uYkDgk5lHTTMsQAZsNar6M2JATPb+SUAuLUBBDEZpmjliQAAtYKuaMi5A9/vUsxs/YkCoTtaLRUAvQMsxZ8B8RGJA5uWvjpQrMED0SHLLu0liQM7aN1OWuzBAfcSfF9dOYkD09UtCG1AxQDB5D+7MU2JAbuimFxbpMUDEFZmem1hiQG6dZB15hjJAZsAMgEFdYkAOpH4tNigzQKfBcvC8YWJAdF9Vsz7OM0DjM0pVDGZiQADLRa2DeDRAdq/GGy5qYkAGqEuu9SY1QETrDLkgbmJACN2v34TZNUDnSW6q4nFiQMTawgIhkDZAqUujdXJ1YkAGzKJyuUo3QHXdBKnOeGJAXl4NJj0JOEB1fcTb9XtiQCLsPLGayzhA0i0jruZ+YkBIz9BHwJE5QIgup8mfgWJAxqDAvptbOkAHd1DhH4RiQKgrWo4aKTtA9ejLsWWGYkAq2UnUKfo7QCE1pQFwiGJA2FGtVbbOPEAwbHehPYpiQHQdMIGspj1AojQcbM2LYkB+9zFx+IE+QNif2UYejWJAfKH27YVgP0AdmI4hL45iQAR07zcgIUBAv+Hd9v6OYkCmTdWQiZNAQLypV8yMj2JA2pphcXQHQUAonKGy149iQPt1xyTWfEFAQX2dxd6PYkBo7uXSo/NBQNs/jiyhj2JAkY55gdJrQkBhlDsaHo9iQI6oURVX5UJAgusTzVSOYkB1SolTJmBDQAHoTI9EjWJADq7D4jTcQ0BjOwK37ItiQJkDbEx3WURANelSpkyKYkAGb/j94ddEQBXtfMtjiGJAVRMwSmlXRUCfP/egMYZiQE4CdGoB2EVAvzaKrbWDYkDM7QqAnllGQCM+ZoTvgGJADW5vlTTcRkCD5DjF3n1iQAS7oJ+3X0dAEjtAHIN6YkB4q3V/G+RHQCmEXELcdmJAutTxAlRpSEDXLiD96XJiQCeinOZU70hA9RzeHqxuYkCiO9rWEXZJQNYxtoYiamJAiBBGcX79SUCcJ6AgTWViQKjfDkaOhUpAlql05StgYkALFFTZNA5LQGmy9Nq+WmJA8EiEpGWXS0CKLM8TBlViQEvRvBcUIUxAh9SkrwFPYkBsEiqbM6tMQOVbCtuxSGJAyo5okLc1TUB1zIjPFkJiQLJx5lOTwE1AnCuc0zA7YkCmdEU+uktOQKFdsDoANGJA/PO8pR/XTkADSRxlhSxiQHUJfN+2Yk9AbTobwMAkYkDQgAtBc+5PQJ+JxMWyHGJAk7/XECQ9UEB4gAH9WxRiQCJY5G0Ug1BAxoSB+bwLYkC0aRpnBMlQQHOGrFvWAmJA5D3Xru0OUUBis5PQqPlhQHLtFfvJVFFA0XLgETXwYUAGNh4Gk5pRQPipweV75mFAaawyj0LgUUBaTNceftxhQAs1PlvSJVJAxDkcnDzSYUCasIA1PGtSQPtszki4x2FAq8U68HmwUkB4fVUc8rxhQA62WGWF9VJAj3cnGuuxYUD5KBx3WDpTQA4OrFGkpmFAsNfEEO1+U0C2KB7eHpthQIAIOCc9w1NATNNr5luPYUBqxKa5QgdUQB+RFJ1cg2FACLMy0vdKVEAKGQZAIndhQDaKkYZWjlRACH53GK5qYUDU/q74WNFUQDbJwnoBXmFAdiFNV/kTVUBlCT3GHVFhQDgXo94xVlVAq90MZQREYUCqGPrY/JdVQGl//8u2NmFA5qdIn1TZVUCHUVx6NilhQOToy5kzGlZABvq2+YQbYUB5C59AlFpWQKQLwN2jDWFA/bRQHHGaVkBARRTElP9gQIpYdsbE2VZAOm0KVFnxYECYaz3qiRhXQLfOfz7z4mBAYGf6RLtWV0BUX6M9ZNRgQN+EtaZTlFdAnZO/FK7FYEC+JLXyTdFXQAXpApDStmBA/tEFIKUNWEBhLEeE06dgQIHP/zlUSVhAxYPXzrKYYECGIcpgVoRYQMNCNVVyiWBA9APbyaa+WEDckNsEFHpgQKG9dcBA+FhABOkB05lqYEDWwyWmHzFZQHN6XbwFW2BAix038z5pWUD7ceHEWUtgQBj7KzeaoFlAPjR+95c7YEBpci8ZLddZQKuQ32XCK2BAHlSFWPMMWkBJ9Sko2xtgQF4M98zoQVpA2qu2XOQLYEAsgz1nCXZaQGxRnk/A919AN+9nMVGpWkAE6M5moddfQH6QP0+821pAO2uvXXC3X0CcR6j+Rg1bQFPaKZ0xl19AXv39l+09W0DSvNGX6XZfQEDSbo6sbVtA/HZSyZxWX0CICVJwgJxbQPgr3LVPNl9AnKd752XKW0CCP4/pBhZfQFK6jLlZ91tAporm98b1XkB8QUDIWCNcQMJVIHuU1V5Abq+0EWBOXECzKqYTdLVeQB/6sbBseFxAaZJzZ2qVXkCNNOzce6FcQFjReyF8dV5ATalC64rJXEDWtQ7xrVVeQDVv+02X8FxA7os8iQQ2XkCfcvuUnhZdQFBKOaCEFl5AlOz6bZ47XUBPDL/uMvddQGpCtqSUX11AuuxvLxTYXUAaSRsjf4JdQMBUNx4tuV1Ao+dy8VukXUBL1Kp3gppdQOkThzYpxV1ATJZq+Bh8XUAmJ8U35eRdQJSGgVz1XV1AkoVcWY4DXkAmPMVeHEBdQLaXWR4jIV5ADb01uJIiXUC1Eb0ooj1eQNgvXR9dBV1ArIePOQpZXkCcjq9HgOhcQDJN8TBac15AHHDq4ADMXEBUnyYOkYxeQGL7dJbjr1xAuxeg762kXkD2GcAOLZRcQNZp/xKwu15AEP2m6uF4XEC4aRjVltFeQL4J0MQGXlxAu13usWHmXkB/QA4xoENcQFucrUQQ+l5ATDTDu7IpXEBId6FHogxfQGilQelCEFxApXYmlBceX0Bc0zA1VfdbQMjmmCJwLl9A5ZrwEe7eW0Bnuz8KrD1fQBlz/ucRx1tAnsszgctLX0CuXVsVxa9bQEFrQ9zOWF9A/t3y7AuZW0BeZdKOtmRfQKEKA7bqgltAFF22KoNvX0COzIWrZW1bQJiZD2A1eV9AXF+b+4BYW0DKQx79zYFfQI0l9sZARFtA5xoU7k2JX0Ag5EcgqTBbQCKn4jy2j19ANXiwC74dW0BG8QURCJVfQNEYLn6DC1tAbMZLr0SZX0AoOA9d/flaQAuQl3ltnF9AFhZmfS/pWkBayKLug55fQOMVfqMd2VpAyhO6qYmfX0Bk6VKCy8laQKwId2KAn19A6KIJuzy7WkBRrnbsaZ5fQJK/a9x0rVpAzb0MN0icX0BNO2Rid6BaQHOu80wdmV9A5L5+tUeUWkCymPlT65RfQHT5aCrpiFpASvqpjLSPX0BwNXYBX35aQHpn9FF7iV9AZTklZqx0WkDNNNAYQoJfQEKEqG7Ua1pAlCXdbwt6X0D89HAb2mNaQMUrAf/ZcF9Amuy6VsBcWkDKRQOHsGZfQA76HfSJVlpA9ocj4ZFbX0AEHh+wOVFaQIdesP6AT19AyLXFL9JMWkC9FpnogEJfQH4aMwBWSVpArLz9vpQ0X0A4Az2Wx0ZaQLJcvLi/JV9AubYKTilFWkAmt/siBRZfQI4atWp9RFpAtHWzYGgFX0B4q+kVxkRaQHbxMers815AUmyQXwVGWkDYmZ9MluFeQPXWdD09SFpABwyAKWjOXkDj2vGKb0taQHHrMDZmul5AN/WgCJ5PWkD8i2Y7lKVeQEBsDFzKVFpAEn6mFPaPXkDyuWQP9lpaQJkOwK+PeV5A0i45kSJiWkB4y0IMZWJeQFDXMzRRalpAUB7zOnpKXkASrdgug3NaQEwPPV3TMV5AiRxIm7l9WkDuQaWkdBheQDvoBHf1iFpAHD44UmL+XUAIcb2iN5VaQBQY+LWg411A/GsY4oCiWkDZiEguNMhdQPwLhdvRsFpASotZJyGsXUAipw4YK8BaQMKPkBpsj11Ak90zA43QWkD8WfCNGXJdQGRIwer34VpAxpt/Ey5UXUBMta/+a/RaQOFgrkiuNV1AbPUFUekHW0B/XrrVnhZdQNFCvtVvHFtAMzsSbQT3XEDEQa9i/zFbQNTht8rj1lxAZ6F4r5dIW0BW9KGzQbZcQKhec1U4YFtA9nEc9SKVXEBaraXP4HhbQM6kKGSMc1xAQYm6epCSW0D6atzcglFcQG3x/JRGrVtA3u/AQQsvXEAMz1Y+AslbQGbpMHsqDFxAo4lTeMLlW0CabbZ25ehbQNFIJiaGA1xA53RoJkHFW0DS47MMTCJcQLUdSIBCoVtA1H6g0hJCXECfxZ197nxbQFLVYADZYlxADQxWGkpYW0BtME8AnYRcQJzTXlRaM1tAbAfEHl2nXEAoVgQrJA5bQMpIMooXy1xArF9OnqzoWkBYSUdTyu9cQIbEXa74wlpAulUObXMVXUCgJ8paDZ1aQDjkF60QPF1AMiQAou92WkB+YaTLn2NdQC7wn4CkUFpANJbSYx6MXUDNidzwMCpaQAqe0fOJtV1AUIbb6ZkDWkBobRbd399dQFSUFV/k3FlAqt6UZB0LXkCCxrc/FbZZQEZC/LI/N15ASbkFdjGPWUB1affUQ2ReQLSovOY9aFlA5yZwuyaSXkAliHdwP0FZQNk71jvlwF5AADEU6zoaWUDTq2kQfPBeQMu5GSc181hAl22I2OcgX0AuCiDtMsxYQMxw/xglUl9ABL44/TilWEBS8F48MIRfQBVqWQ5MflhAHQdSkwW3X0AyVcfNcFdYQNR9+VSh6l9AHbiE3qswWEBi46TPfw9gQGKWv9gBClhAdQ+2Ow4qYEA9QUJJd+NXQIBjEeX5RGBAsJflsBC9V0CoJRi2QGBgQIgTBYTSlldAW+ZgjeB7YEBCtvQpwXBXQBog7j3Xl2BAHuV4/OBKV0DSS2aPIrRgQG5GQEc2JVdAc2NNPsDQYED4r19Hxf9WQFbMP/yt7WBAgTbQKpLaVkC0oi5w6QphQIxu7w+htVZArl6dNnAoYUBI7gEF9pBWQJbM4OE/RmFAaR+4B5VsVkB6UF/6VWRhQHdwtQSCSFZAHm7S/q+CYUDa8xnXwCRWQFqNiWRLoWFATXsOSFUBVkA7862XJcBhQPA9Uw5D3lVA5eeH+zvfYUCkFtHNjbtVQL8BxeqL/mFAkGctFzmZVUCCjL+3Eh5iQDOxYGdId1VAXgXHrM09YkAd6E8nv1VVQC+iaQy6XWJAT5doq6A0VUCL3L4R1X1iQOLZPzPwE1VA9Pay8BueYkChODTpsPNUQIhzU9aLvmJAm3YS4uXTVECucxzpId9iQDlXvRyStFRAPvdGSdv/YkC6aNiBuJVUQOrxFxG1IGNAVd1141t3VECHLTBVrEFjQLh8x/x+WVRAmPDcJL5iY0C2ttJxJDxUQDleaYrng2NAJuEnz04fVEBVh3CLJaVjQF+onIkAA1RADiIwKXXGY0CFuwn+O+dTQMve22DT52NAjrsLcQPMU0AvUPErPQlkQNh0xw5ZsVNAVlyMgK8qZEAyarHqPpdTQO0tvFEnTGRAPbdY/7Z9U0C+mdiPoW1kQFNRNS7DZFNAOfDXKBuPZEBPq3k/ZUxTQHMvpQiRsGRAb8Ln4Z40U0D5i3YZANJkQKCYqapxHVNAY0YkRGXzZEBrIC0V3wZTQOzCf3C9FGVAx54Dg+jwUkDo2KqFBTZlQASGxDuP21JAh09vajpXZUAAzfNs1MZSQDx+lgVZeGVAxsTrKbmyUkD2BEE+XpllQKRvyms+n1JAcpM+/Ea6ZUA0WmIRZYxSQIWzZSgQ22VAvvgu3y16UkBpjeustvtlQNSITH+ZaFJAgJq7dTccZkAKeHOBqFdSQHE8z3CPPGZAuE/3WltHUkCPLoWOu1xmQMAkyWayN1JArsb4wbh8ZkDXin3lrShSQBL7WAGEnGZAnglW/U0aUkDpIj9GGrxmQP8RTrqSDFJAxWcFjnjbZkDjcCsOfP9RQG3cHNqb+mZAKD2S0AnzUUAoMGMwgRlnQC09HL8751FANfN3myU4Z0BvwHN9EdxRQI1iESuGVmdAdOlxlYrRUUBjsVD0n3RnQNpiQHemx1FAIsUVEnCSZ0BNe355ZL5RQE5bUqXzr2dAV6Jp2cO1UUDOjVzVJ81nQAJBCbvDrVFASq1A0AnqZ0BI510pY6ZRQJxmEsuWBmhAOMiTFqGfUUAcKj0CzCJoQIp9OFx8mVFAV8vTuaY+aEARC3S785NRQFFP3z0kWmhAghpF3QWPUUD436ziQXVoQMZmwFKxilFAXtsaBf2PaECUTlOV9IZRQAr25ApTqmhAz4QJB86DUUAnZu9iQcRoQMPV1fI7gVFA5A+RhcXdaECs9t2MPH9RQN+o3PTc9mhAw1XJ8s19UUDkyug8hQ9pQBHgEizufFFAlOwW9LsnaUC5sV0qm3xRQOU3Wbt+P2lAAqbMydJ8UUCsNXc+y1ZpQIa6XNGSfVFA1kZRNJ9taUAuOkLz2H5RQATiIl/4g2lA66JIzaKAUUBwjsOM1JlpQAo6NentglFAAJTmljGvaUBBQSy9t4VRQABZWWMNxGlAkr8YrP2IUUD2ZUDkZdhpQAfQFga9jFFANAhTGDnsaUDyZ+AI85BRQN2LFQuF/2lAy4Y84JyVUUDlBhLVRxJqQG7AcKa3mlFAkK0PnH8kakD4ErVkQKBRQCqqSJMqNmpAvPipEzSmUUB4cZ77RkdqQDun0JuPrFFAmI3MI9NXakCVagXWT7NRQBDZmWjNZ2pAvQ38i3G6UUA9JAg1NHdqQPU+vnjxwVFAfj+CAgaGakDD3ytJzMlRQHpkCFlBlGpACTB9nP7RUUAW+VrP5KFqQEfCxgSF2lFACqcjC++uakBhKH8HXONRQGDDHMFeu2pACEcGHoDsUUCKADe1MsdqQDw+Lrbt9VFASme9umnSakCp1MUyof9RQE6Rd7QC3WpA0FMk7JYJUkAjIsqU/OZqQN/CtjDLE1JA/HnVXVbwakA2bY5FOh5SQPKekiEP+WpArKLwZuAoUkAaWe4BJgFrQL6d58i5M1JAb37iMJoIa0DjfdSXwj5SQB1rjfBqD2tAoUEC+fZJUkC0o0eTlxVrQGWuOQtTVVJAdJ63ex8ba0CqEVbn0mBSQICx4xwCIGtAzcjaoHJsUkCbI0L6PiRrQKJ7iUYueFJAol3Hp9Una0D19fjiAYRSQAQ78snFKmtAyowsfemPUkDFd9YVDy1rQBr7Kxnhm1JAujolUbEua0Beopu45KdSQAa7M1KsL2tAzRpVW/CzUkAAAAAAADBrQAAAAAAAwFJA'), c => c.charCodeAt(0)))" + ")(document.currentScript.previousElementSibling, \"{\\\"source\\\":\\\"(ctx, p, o) => {\\\\n let m = 0;\\\\n const tick = () => {\\\\n m = m > 1.5 * p.shape[0] ? 0 : m + o.step;\\\\n ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);\\\\n line(ctx, {data: p.data.subarray(0, 2 * m)});\\\\n if (ctx.canvas.isConnected) requestAnimationFrame(tick);\\\\n };\\\\n tick();\\\\n}\\\",\\\"options\\\":{\\\"step\\\":10.0},\\\"data\\\":{\\\"shape\\\":[2001.0,2.0],\\\"data\\\":{\\\"$typed\\\":\\\"Float64Array\\\",\\\"offset\\\":0,\\\"length\\\":4002}}}\", Uint8Array.from(atob('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'), c => c.charCodeAt(0)))" ], "text/plain": [ "canvas" diff --git a/py/src/lib.rs b/py/src/lib.rs index dfc186a6..bcbc1a77 100644 --- a/py/src/lib.rs +++ b/py/src/lib.rs @@ -687,8 +687,9 @@ fn _captured_literal(value: &str) -> PyResult { /// Shared setup and records, including native budget-footer byte spans. #[pyfunction] -fn _reference_file(py: Python<'_>, text: &str) -> PyResult> { - let file = basedpl_testkit::reference::file("", text).map_err(PyValueError::new_err)?; +#[pyo3(signature = (text, name = ""))] +fn _reference_file(py: Python<'_>, text: &str, name: &str) -> PyResult> { + let file = basedpl_testkit::reference::file(name, text).map_err(PyValueError::new_err)?; python(py, &serde_json::json!({"setup":file.setup, "cases":file.cases})) } diff --git a/pyproject.toml b/pyproject.toml index 6837b1cd..ab51ebe3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ classifiers = [ ] [project.optional-dependencies] -dev = ["fastship>=0.0.11", "fastws-cli>=0.0.34", "maturin>=1.13,<2.0", "pytest", "pytest-timeout", "numpy", "pandas", "ipython>=9.17.1", "nbdev>=3.3.24", "conkernelclient>=0.0.23", "pillow", "rustymath>=0.1.49"] +dev = ["fastship>=0.0.11", "fastws-cli>=0.0.35", "maturin>=1.13,<2.0", "pytest", "pytest-timeout", "numpy", "pandas", "ipython>=9.17.1", "nbdev>=3.3.24", "conkernelclient>=0.0.23", "pillow", "rustymath>=0.1.49"] [project.scripts] basedpl-install = "basedpl.install:main" @@ -44,7 +44,7 @@ module-name = "basedpl._core" macos-deployment-target = "15.0" [tool.uv] -cache-keys = [{ file = "pyproject.toml" }, { file = "Cargo.toml" }, { file = "cli/Cargo.toml" }, { file = "image/Cargo.toml" }, { file = "jit/Cargo.toml" }, { file = "py/Cargo.toml" }] +cache-keys = [{ file = "pyproject.toml" }, { file = "Cargo.toml" }, { file = "cli/Cargo.toml" }, { file = "image/Cargo.toml" }, { file = "jit/Cargo.toml" }, { file = "py/Cargo.toml" }, { file = "testkit/Cargo.toml" }] [tool.fastws] dev-profile = "release" diff --git a/python/basedpl/bpltests.py b/python/basedpl/bpltests.py index 046f18f2..b08a12bc 100644 --- a/python/basedpl/bpltests.py +++ b/python/basedpl/bpltests.py @@ -1,5 +1,5 @@ "Read BPL test cases, convert reference inventories, activate reviewed cases, time BPL as the speed test does, and set the budgets of basedpl's and rustymath's speed tests." -import csv, difflib, json, math, os, re, selectors, statistics, subprocess, sys, tempfile, time, tomllib +import csv, difflib, json, math, os, re, selectors, shutil, statistics, subprocess, sys, tempfile, time, tomllib from concurrent.futures import ThreadPoolExecutor from collections import Counter, defaultdict from queue import Empty, Queue @@ -345,10 +345,13 @@ def reference_summary( kinds = defaultdict(list) for r in failing: if r['status'] == 'mismatch' and (k := _kind(r)) != 'other mismatch' and len(kinds[k]) < examples: kinds[k].append(r) + messages = Counter() + for r in failing: + messages.update({m['message'] for m in r.get('modes', ()) if m.get('message') and m['status'] not in ('pass', 'not applicable', 'unchecked')}) return ReferenceSummary( cases=len(results), obligations=sum(counted['obliged']), modes=modes, passing=Counter(m['mode'] for r in failing for m in r.get('modes', ()) if m['status'] == 'pass'), statuses=statuses, kinds=dict(kinds), - messages=Counter(r.get('message') or '' for r in failing if r['status'] == 'error'), files=Counter(r.get('file') for r in failing), + messages=messages, files=Counter(r.get('file') for r in failing), lost=[r for r in results if any(m['status'] == _LOST for m in r.get('modes', ()))], compared=baseline is not None) _ROOT = Path(__file__).parents[2] @@ -376,19 +379,19 @@ def __repr__(self): w = 13 - len(self.unit) return '\n'.join([head, *(''.join(f'{r[1]:>{w},.0f}{self.unit}' for r in rs) + f' {rs[-1][1]/rs[0][1]-1:>+7.1%} {_commented(rs[0][0])}' for rs in zip(*self))]) -def _section(section): +def speed_setup(section): "The setup of a speed file and the preferences of one of its sections, from `file:section` such as `models.bpl:static`." name, _, part = section.partition(':') - speed = _reference_file((_SPEED/name).read_text()) + speed = _reference_file((_SPEED/name).read_text(), name) header = next((c.get('prefs', '') for c in speed['cases'] if c['section'] == part), None) if header is None: raise ValueError(f'tests/speed/{name} has no section {part!r}') return speed['setup'], header -def _section_setup(section, setup): - "`setup`, or for a `section`, that section's preferences and then its speed file's setup, as one text." - if not section: return setup - file_setup, prefs = _section(section) - return f'{prefs}\n{file_setup}' +def _section_setup(section, setup, prefs): + "Setup and preferences, keeping preferences before source construction." + if not section: return setup, prefs + file_setup, header = speed_setup(section) + return file_setup, '\n'.join([prefs, header]) @delegates(environment) def time_cases( @@ -404,9 +407,7 @@ def time_cases( **kwargs ): "Time fully read calculations, taking the fastest of five warm runs per worker or the first run with `cold=True`, then the fastest over `rounds`. Several builds alternate order each round and return `ByBuild`." - if section: - setup, header = _section(section) - prefs = '\n'.join([prefs, header]) + setup, prefs = _section_setup(section, setup, prefs) setup = _reference_file((_SPEED/'arrays.bpl').read_text())['setup'] if setup is None else setup cases = listify(cases) limit = Limit.of(timeout) @@ -509,11 +510,11 @@ def _retired(command, bpl, settings, limit): # A case that names N outside its strings supplies its own repetition and reads. _NAMES_N, _QUOTED = re.compile(r'(? Result { if let Self::Number(n) = self { return Ok(Self::Number(n.to_fixed(width, numeric)?)); } if !matches!(self, Self::Array(_)) { return Err(Kind::Domain.into()); } if let Some(fixed) = self.as_fixed(width) { return Ok(fixed); } - let Some(buffer) = self.as_items().view().and_then(|v| with_width!(width, T => crate::element::converted::(v, numeric).map(T::buffer))) else { + let Some(view) = self.view().filter(|v| v.dtype().is_real()) else { let values = self.elements().map(|v| v.to_fixed(width, numeric)).collect::, _>>()?; return self.layout().collect(values, self.prototype().to_fixed(width, numeric)?); }; + let buffer = Buffer::floored(view, DType::Integer(width), numeric.tolerance_of(view.dtype()))?; Self::from_storage(self.layout().clone(), Storage::Compact(buffer).fixed(true)) } /// This value's numbers as integers of `width` (`to_fixed`), which are fixed where `numeric` has a fixed width and ordinary otherwise, as a call under `numeric` gives them. @@ -774,17 +775,16 @@ impl Value { } /// The width of the floats that hold the items, or of the parts of the complex numbers that do, when compact storage holds them or the value is such an atom. pub fn float_width(&self) -> Option { float_width_of(self.dtype()?) } - /// The numbers of this value as floats of `width`, each rounded once, with the arrays nested in it converted too. Complex numbers take parts of `width`, and 32-bit parts for 16 bits. Floats already of `width`, pending ones included, stay as they are, with no copy. Characters and functions are DOMAIN. + /// The numbers of this value at float width `width`, including nested arrays. Complex numbers take parts of `width`, and 32-bit parts for 16 bits. Floats already of `width`, pending ones included, stay as they are, with no copy. Characters and functions are DOMAIN. pub(crate) fn to_float_width(&self, width: FloatWidth) -> Result { if let Self::Number(n) = self { return n.to_float_width(width).map(Self::Number).map_err(|_| Kind::Domain.into()); } if !matches!(self, Self::Array(_)) { return Err(Kind::Domain.into()); } if self.dtype() == Some(DType::Float(width)) { return Ok(self.clone()); } match self.view() { - Some(View::Booleans(_) | View::Integers(_) | View::Floats(_)) => { - with_float_width!(width, F => F::build(self.layout().clone(), crate::element::read_as::(self).ok_or(Kind::Domain)?.into_owned())) - .ok_or_else(|| Kind::Domain.into()) + Some(view) if view.dtype() != DType::Character => { + let to = match view { View::Complexes(_) => DType::Complex(width.into()), _ => DType::Float(width) }; + Self::from_storage(self.layout().clone(), Storage::Compact(Buffer::cast_results(view, to, false)?)) } - Some(View::Complexes(z)) => Self::complex(self.layout().clone(), ComplexBuf::collect(width.into(), z.widened().iter().copied())), _ => { self.layout().collect(self.elements().map(|e| e.to_float_width(width)).collect::, _>>()?, self.prototype().to_float_width(width)?) } @@ -843,7 +843,7 @@ impl Value { pub fn widened_for_export(&self) -> Option { let width = self.export_widening()?; let View::Integers(ints) = self.view()? else { return None }; - Some(with_width!(width, T => ::buffer(crate::element::cast::(ints).into_owned()))) + Some(Buffer::cast_results(View::Integers(ints), DType::Integer(width), false).expect("integers convert to integers")) } /// Whether the array reads another array's items, as a view. pub(crate) fn is_view(&self) -> bool { matches!(self, Self::Array(a) if a.is_view()) } diff --git a/src/array/storage.rs b/src/array/storage.rs index e72e5a15..1e1b1ee5 100644 --- a/src/array/storage.rs +++ b/src/array/storage.rs @@ -87,6 +87,10 @@ pub(crate) fn static_width(width: Width) -> Option { crate::settings::int /// The width that stores ordinary integers that `width` holds: `width` itself, the narrowest that holds their values, under dynamic kinds, and `static_width` under static kinds. #[inline] pub(crate) fn ordinary_width(width: Width) -> Width { static_width(width).unwrap_or(width) } +/// The ordinary width of integer bounds, saturated at i64, or i64 when no bounds are known. +pub(crate) fn ordinary_width_for(range: Option) -> Width { + range.map_or(Width::I64, |r| ordinary_width(Width::narrowest(r.lo as i64, r.hi as i64))) +} /// The compact dtype that holds `item`, if any does (`Number::dtype`). pub(crate) fn item_dtype(item: &Value) -> Option { match item { Value::Number(n) => n.dtype(), Value::Character(_) => Some(DType::Character), _ => None } @@ -187,7 +191,7 @@ impl Storage { pub(crate) fn at(buffer: Buffer, fixed: Option, known: Option) -> (Self, Range) { match (buffer, fixed) { (Buffer::Integers(v), Some(width)) if v.view().tag() == width => (Self::Fixed(v), Range::ANY), (Buffer::Integers(v), Some(width)) => ( - Self::Compact(with_width!(width, T => with_ints!(v.view(), |v| T::buffer(v.iter().map(|&n| T::from_i64(n.into())).collect())))).fixed(true), + Self::Compact(Buffer::cast_results(View::Integers(v.view()), DType::Integer(width), false).expect("integers convert to integers")).fixed(true), Range::ANY), (Buffer::Integers(IntBuf::U8(v)), None) => Self::narrowed(v, known), (Buffer::Integers(IntBuf::I16(v)), None) => Self::narrowed(v, known), (Buffer::Integers(IntBuf::I32(v)), None) => Self::narrowed(v, known), (Buffer::Integers(IntBuf::I64(v)), None) => Self::narrowed(v, known), (buffer, _) => (Self::Compact(buffer), Range::ANY) @@ -208,13 +212,13 @@ impl Storage { let (min, max) = rustymath::reduce::extremes(&data); Range { lo: min.to_i64() as f64, hi: max.to_i64() as f64 } }); - let width = ordinary_width(Width::narrowest(range.lo as i64, range.hi as i64)); + let width = ordinary_width_for(Some(range)); (Self::within(data, if T::WIDTH.join(width) == T::WIDTH { width } else { T::WIDTH }), range) } /// The integers `data` at `width`, which holds them all. Integers that have the width already keep their buffer. pub(crate) fn within(data: Vec, width: Width) -> Self { Self::Compact(if width == T::WIDTH { T::buffer(data) } - else { with_width!(width, U => U::buffer(data.iter().map(|&n| U::from_i64(n.to_i64())).collect())) }) + else { Buffer::cast_results(T::view(&data), DType::Integer(width), false).expect("integers convert to integers") }) } /// Copies `range` of `source` when it has this storage's dtype, and gives whether it could. fn copy_slice(&mut self, source: Items, range: std::ops::Range) -> bool { diff --git a/src/batch.rs b/src/batch.rs index 29bec6f7..66fc736e 100644 --- a/src/batch.rs +++ b/src/batch.rs @@ -47,7 +47,7 @@ impl Batch { #[cold] #[inline(never)] pub(crate) fn storage(&self) -> &Storage { - crate::transform::Carrier::built(&self.items, rustymath::events::Route::BatchedItems, self.template().len(), |i| self.at(i)) + crate::transform::Carrier::built(&self.items, crate::events::routes::BatchedItems, self.template().len(), |i| self.at(i)) } /// This batch with each cell laid out by `layout`, as a view of the whole with no items copied. `layout` has as many items as a cell. pub(crate) fn view(&self, layout: Layout) -> Value { @@ -190,7 +190,7 @@ pub(crate) fn from_cells(level: u64, frame: &Layout, cells: Vec) -> Optio /// `f` for each cell of batched run `level`, whose frame is `frame`, in cell order, with the cells' values joined into one batch (`from_cells`). Where the values don't stack, as values of different shapes or kinds don't, the run stops for a call with no rule for these arguments (`Cause::Rule`). `•explain` counts the cells as the `itemwise` route `batch cells`. pub(crate) fn each_cell(level: u64, frame: &Layout, f: impl FnMut(usize) -> Result) -> Result { let count = frame.count(); - crate::explain::itemwise(rustymath::events::Route::BatchCells, count); + crate::explain::itemwise(crate::events::routes::BatchCells, count); let cells = (0..count).map(f).collect::, _>>()?; from_cells(level, frame, cells).ok_or_else(|| Stop::new(level, Cause::Rule).into()) } @@ -577,12 +577,7 @@ fn run(operand: &Function, x: Option<&Side>, y: &Side, frame: &Layout, entry: &E Ok(value) }; let (level, called) = match entry { - Entry::Begin => { - let level = session().begin(crate::transform::Kind::batched(frame.clone())); - let called = call(level); - session().end(); - (level, called) - } + Entry::Begin => session().with_level(crate::transform::Kind::batched(frame.clone()), |level| (level, call(level))), Entry::Widen { level, .. } => (*level, session().widened(frame, span, || call(*level))), }; match called { @@ -622,7 +617,7 @@ fn chunked(operand: &Function, x: Option<&Side>, y: &Side, frame: &Layout, chunk /// Where the `batch` pref is on, the result of the map whose frame is `layout`, from one call of `operand` on batches of its cells. With `mixed`, the cells' results mix into one array, as Rank mixes them. Otherwise they are the result's items, as in Each. `None` means the map calls its operand once for each cell instead. /// -/// The try's conditions: `tried` gives `None` for an empty frame or a frame of rank 0, and where the running levels allow no try (`tries`). Otherwise it calls `arguments` for the map's whole arguments. It gives `None` where `arguments` does, and for an argument with a frame that isn't compact (`side`). It gives `None`, after a `fallbacks` event of `•explain`, for a dfn operand whose earlier batched run stopped for a cause that it remembers for every call, without calling `arguments`, or for a cause that it remembers for these arguments (`signature`). Each refusal before the call, other than an empty frame, is a decline with its reason (`rustymath::events`). +/// The try's conditions: `tried` gives `None` for an empty frame or a frame of rank 0, and where the running levels allow no try (`tries`). Otherwise it calls `arguments` for the map's whole arguments. It gives `None` where `arguments` does, and for an argument with a frame that isn't compact (`side`). It gives `None`, after a `fallbacks` event of `•explain`, for a dfn operand whose earlier batched run stopped for a cause that it remembers for every call, without calling `arguments`, or for a cause that it remembers for these arguments (`signature`). Each refusal before the call, other than an empty frame, is a decline with its reason (`crate::events`). /// /// The run: a map outside any batched run begins one (`Entry::Begin`). A map inside one widens it (`Entry::Widen`): the run's frame comes first in the widened frame, each of the run's batches lays over the widened frame, and each constant with a frame spreads over the run's frame (`side`). /// @@ -635,7 +630,7 @@ pub(crate) fn tried( if frame.is_empty() || frame.contains(&0) { return None; } let cells = layout.count(); let declined = |reason: &'static str| -> Option> { - rustymath::events::declined(rustymath::events::Route::Batching, reason, cells); + crate::events::declined(crate::events::routes::Batching, reason, cells); None }; if !tries(cells) { return None; } @@ -680,16 +675,16 @@ pub(crate) fn tried( Entry::Widen { .. } if !one_array => return fell_back("the result", cells), Entry::Widen { level, outer } => whole.relaid(outer.concat(layout).concat(&cell)).map(|whole| batch(level, whole, outer.shape().len())), }; - if assembled.is_ok() { rustymath::events::taken(rustymath::events::Route::Batching, cells); } + if assembled.is_ok() { crate::events::taken(crate::events::routes::Batching, cells); } Some(assembled.described("invalid result")) } -/// Whether a map of `cells` cells may try a batched call: the `batch` pref is on, and the running levels admit a batched run (`transform::Levels::admits_batch`), or let the map widen the running one (`transform::Levels::widens`). Where it may not, the refusal is a decline (`rustymath::events`). `tried` checks it first. A caller checks it before building arguments that only a try reads. +/// Whether a map of `cells` cells may try a batched call: the `batch` pref is on, and the running levels admit a batched run (`transform::Levels::admits_batch`), or let the map widen the running one (`transform::Levels::widens`). Where it may not, the refusal is a decline (`crate::events`). `tried` checks it first. A caller checks it before building arguments that only a try reads. pub(crate) fn tries(cells: usize) -> bool { let reason = if !crate::settings::batch() { "the batch pref is off" } else if !session().admits_batch() && !session().widens() { "a label level, or a ∂ call inside a batched run" } else { return true; }; - rustymath::events::declined(rustymath::events::Route::Batching, reason, cells); + crate::events::declined(crate::events::routes::Batching, reason, cells); false } diff --git a/src/derivative.rs b/src/derivative.rs index 7e4745dd..6b495093 100644 --- a/src/derivative.rs +++ b/src/derivative.rs @@ -48,7 +48,7 @@ impl Dual { #[cold] #[inline(never)] pub(crate) fn storage(&self) -> &crate::array::Storage { - crate::transform::Carrier::built(&self.items, rustymath::events::Route::DerivativeItems, self.primal.len(), |i| self.element(i)) + crate::transform::Carrier::built(&self.items, crate::events::routes::DerivativeItems, self.primal.len(), |i| self.element(i)) } /// `form`'s result on `inputs`, with `params` and `fill` (`Carrier::structural`): the copy of the primals and the copy of the tangents, through the same plan (`fusion::copy`). An input that isn't a dual at this tag has zeros as its tangent, and the tangents' copy reads zeros where the primals' reads the fill. pub(crate) fn structural( @@ -341,7 +341,7 @@ pub(crate) fn differentiate(f: &Function, left: Option<&Value>, right: &Value, s None if leaf.template().is_exact() => converted(leaf, DType::Float(numeric.width_of([None])), span), None => Ok(leaf.clone()) } })?; - if let Some(result) = f.programmed(left, right, span, &|g, x, y| derived(g, x, y, span)) { return result; } + if let Some(result) = f.programmed(crate::program::trace::Purpose::Derivative, None, left, right, span, &|g, x, y| derived(g, x, y, span)) { return result; } derived(f, left, right, span) } @@ -356,10 +356,7 @@ pub(crate) fn unrendered(value: &Value) -> Value { fn derived(f: &Function, left: Option<&Value>, right: &Value, span: &Span) -> Result { let n = leaves(right)?; if n > 1 { return reverse(f, left, right, n, span); } - let tag = session().begin(crate::transform::Kind::Derivative); - let passes = pass(f, left, right, tag, n, span); - session().end(); - let (primal, columns) = passes?; + let (primal, columns) = session().with_level(crate::transform::Kind::Derivative, |tag| pass(f, left, right, tag, n, span))?; jacobian(right, &primal, columns) } diff --git a/src/derivative/tape.rs b/src/derivative/tape.rs index 5073c00f..28683c8c 100644 --- a/src/derivative/tape.rs +++ b/src/derivative/tape.rs @@ -30,13 +30,10 @@ pub(crate) struct Linearized { argument: Value, parts: Vec, pub(crate) va /// `f` linearised at `right`, with `left` as its left argument. The tape level begins before the `∂` call's, so each seed is a tape value inside a dual. pub(crate) fn linearized(f: &Function, left: Option<&Value>, right: &Value, span: &Span) -> Result { - let level = session().begin(Kind::Tape); - let tag = session().begin(Kind::Derivative); let tracer = Arc::new(Tracer::default()); - let run = linearize(f, left, right, tag, level, &tracer, span); - session().end(); - session().end(); - let (parts, value, result) = run?; + let (parts, value, result) = session().with_level(Kind::Tape, |level| { + session().with_level(Kind::Derivative, |tag| linearize(f, left, right, tag, level, &tracer, span)) + })?; Ok(Linearized { argument: right.clone(), parts, value, tape: result.map(|read| tracer.program(read)) }) } @@ -51,7 +48,7 @@ impl Linearized { /// The derivative along `seeds`, one for each part. pub(crate) fn evaluated(&self, seeds: &[Value], span: &Span) -> Result { let Some(tape) = &self.tape else { return super::float_zeros(&self.value) }; - Ok(run::read(tape, &run::values(tape, &crate::program::planned(tape), &seeds.iter().collect::>(), span)?, &tape.result)) + run::replay(tape, &crate::program::planned(tape), &seeds.iter().collect::>(), span) } /// `u` times the Jacobian, with the argument's layout. @@ -115,7 +112,7 @@ pub(crate) fn taped(op: Op<'_>, left: Option<&Value>, right: &Value, span: &Span let linear = match op { Op::Operation(Operation::Primitive(p), _) if matches!(p.facts(left.is_some()).form, Form::Moves { .. }) => return None, Op::Operation(Operation::Primitive(Primitive::Arithmetic(Arithmetic::Plus)), Pairing::At(_)) => left.is_some(), - Op::Operation(Operation::Primitive(p), Pairing::Axis(None)) => match (p.facts(left.is_some()).linear, left) { + Op::Operation(Operation::Primitive(p), Pairing::Axis(None) | Pairing::Agreed(_)) => match (p.facts(left.is_some()).linear, left) { (Some(LinearMap::Product), Some(l)) => tape(l) != tape(right), (Some(LinearMap::Quotient), Some(_)) => !tape(right), (Some(LinearMap::Sum | LinearMap::Difference), Some(_)) | (Some(LinearMap::Same | LinearMap::Negation), None) => true, _ => false }, @@ -153,12 +150,16 @@ fn glyph(op: &Op<'_>) -> String { /// The cotangent of each seed, from `u`, the cotangent of `tape`'s result: the tape run backwards, each step's transpose an ordinary call (`Back::step`). `None` for a seed that the result doesn't depend on. fn transpose(tape: &Program, u: Value, seeds: usize, span: &Span) -> Result>, Error> { let nodes = &tape.nodes; - // A node that depends on no seed holds a coefficient or a position: a held value, or a copy of one that `trace::Arg::read` made, which runs once here. + // Seed-free steps build coefficients and positions. Copies keep their read geometry until a consumer needs stored items. let mut values = vec![None; nodes.len()]; for (j, node) in nodes.iter().enumerate() { values[j] = match &node.origin { Origin::Input(_) => None, Origin::Held(v) => Some(v.clone()), Origin::Step(step) if step.reads().any(|i| values[i].is_none()) => None, - Origin::Step(step) => Some(run::run(step, nodes, &values, None, &node.desc, span)?) + Origin::Step(step) => { + let value = |i: usize| values[i].as_ref().expect("a coefficient's operands have their values"); + Some(run::copy_read(step, &value, &node.desc, span) + .unwrap_or_else(|| run::run(step, nodes, &value, None, &node.desc, span))?) + } }; } let mut back = Back { nodes, values, cotangents: vec![None; nodes.len()], span }; @@ -251,14 +252,8 @@ impl Back<'_> { self.add_at(read, Places::Walk { base, steps: &kept }, &u) } - /// A structural step's plan on its inputs' layouts, with its parameters' held values (`structural::plan`). - fn planned(&self, inputs: &[Read], form: &crate::primitive::structural::Form, params: &[Read]) -> Result { - let params = params.iter().map(|r| self.held(r)).collect::, _>>()?; - let layouts: Vec<&Layout> = inputs.iter().map(|r| &r.layout).collect(); - Ok(crate::primitive::structural::plan(form, ¶ms, &layouts)?.1) - } - fn walked(&self, j: usize) -> Option<(Read, Mapping)> { crate::program::positional_source(self.nodes, j, |j| self.values[j].as_ref()) } + fn walked(&self, j: usize) -> Option<(Read, Mapping)> { crate::program::positional_source(self.nodes, j) } /// Transposes a complete, unit-stride windows product as a full correlation, without storing or scattering its window cotangent. fn convolved_back(&self, t: &Read, c: &Value, u: &Value) -> Result, Error> { @@ -366,8 +361,7 @@ impl Back<'_> { else { let back = product_back(&c, &u, &t.layout, coefficient_left, span)?; self.add(t, back) } } Step::Copy { of, map } => { let (base, steps) = map.to_walk(desc.shape()); self.add_walked(&Read::node(self.nodes, *of), base, &steps, u) } - Step::Structural { inputs, form, params, .. } => { - let plan = self.planned(inputs, form, params)?; + Step::Structural { inputs, plan, .. } => { // A plan that reads one input by affine axes, as a transpose, reversal or reshape does, writes back along its walk, as a copy's does. if let Some((source, map)) = plan.positional(desc.shape()) { // A source past the inputs is the fill, which takes no cotangent. diff --git a/src/element.rs b/src/element.rs index 1c795898..8884608c 100644 --- a/src/element.rs +++ b/src/element.rs @@ -1,8 +1,8 @@ -//! How the element types of compact storage read their items from a `Value` and build arrays from them. Each type's fill and its conversions to other types are rustymath's `Element` and `Source`, and `Converts` for the conversions that marks make. -use crate::{Value, array::Storage, number::{Numeric, int::Int}}; +//! How compact element types read items from a `Value` and build arrays. Element fills and numeric conversions belong to rustymath. +use crate::{Value, array::Storage, number::int::Int}; use num_complex::{Complex32, Complex64}; use rustymath::{ - buffer::{ComplexBuf, Complexes, Element, FloatWidth, Ints, Source, View, Width, read_all}, tolerant::{Exactly, Tolerant}, + buffer::{ComplexBuf, Complexes, Element, FloatWidth, Source, View, Width, read_all}, with_complexes, with_floats, with_ints }; use std::borrow::Cow; @@ -69,12 +69,6 @@ impl Float for half::f16 { fn compact(value: &Value) -> Option> { read_as(value) } } -/// `ints` at width `A`: borrowed when they have it already, otherwise converted. `A` must hold every item. -pub(crate) fn cast(ints: Ints<'_>) -> Cow<'_, [A]> { - if let Some(v) = A::of(View::Integers(ints)) { return Cow::Borrowed(v); } - with_ints!(ints, |v| Cow::Owned(v.iter().map(|&n| A::from_i64(n.to_i64())).collect())) -} - /// `value`'s compact numbers as `A`s: borrowed when they are `A`s already, otherwise read through `Source`. Complex numbers read only as complex numbers. `None` when `A` can't hold them. pub(crate) fn read_as(value: &Value) -> Option> where bool: Source, u8: Source, i16: Source, i32: Source, i64: Source, f64: Source, f32: Source, half::f16: Source { @@ -85,47 +79,3 @@ pub(crate) fn read_as(value: &Value) -> Option> where bo })) } -/// An item that converts to type `T` as fixed marks, width marks and programs' conversions convert it, where `t` is the tolerance of a float's floor. A float target reads the item as `Source` reads it. An integer target takes a Boolean as 0 or 1, wraps an integer to its width, and takes the floor of a float within `t`, saturated at its range as Rust's `as` saturates. An exact fraction converts exactly: a whole one wraps as an integer does, and any other takes its floor, saturated. -pub(crate) trait Converts: Copy { fn converted(self, t: impl Tolerant) -> T; } - -/// Writes `Converts` into the float types from each item type listed, through `Source`. -macro_rules! into_floats { - ($($s:ty),+) => {$( - impl Converts for $s { #[inline] fn converted(self, _: impl Tolerant) -> f32 { Source::::read(self) } } - impl Converts for $s { #[inline] fn converted(self, _: impl Tolerant) -> f64 { Source::::read(self) } } - )+}; -} -into_floats!(bool, u8, i16, i32, i64, half::f16, f32, f64); - -/// Writes `Converts` into each integer type listed, from Booleans, integers, floats and exact fractions. -macro_rules! into_wholes { - (@ints $t:ty: $($s:ty),+) => {$( - impl Converts<$t> for $s { #[inline] fn converted(self, _: impl Tolerant) -> $t { self as $t } } - )+}; - (@floats $t:ty: $($s:ty),+) => {$( - impl Converts<$t> for $s { #[inline] fn converted(self, t: impl Tolerant) -> $t { rustymath::convert::floored::<$t, f64>(f64::from(self), t) } } - )+}; - ($($t:ty),+) => {$( - impl Converts<$t> for bool { #[inline] fn converted(self, _: impl Tolerant) -> $t { <$t>::from(self) } } - into_wholes!(@ints $t: u8, i16, i32, i64); - into_wholes!(@floats $t: half::f16, f32, f64); - impl Converts<$t> for &num_rational::BigRational { - fn converted(self, _: impl Tolerant) -> $t { - if self.is_integer() { return crate::number::low_bits(self.numer()) as $t; } - rustymath::convert::saturated(&self.floor().to_integer()) - } - } - )+}; -} -into_wholes!(u8, i16, i32, i64); - -/// The numbers of `view` converted to `T` (`Converts`), each float floored within the tolerance that `numeric` gives its width. `None` for complex numbers and characters. -pub(crate) fn converted(view: View<'_>, numeric: Numeric) -> Option> where bool: Converts, u8: Converts, i16: Converts, i32: Converts, - i64: Converts, half::f16: Converts, f32: Converts, f64: Converts { - fn each, T>(items: &[S], t: impl Tolerant) -> Vec { items.iter().map(|&x| x.converted(t)).collect() } - Some(match view { - View::Booleans(v) => each(v, Exactly), View::Integers(ints) => with_ints!(ints, |v| each(v, Exactly)), - View::Floats(v) => rustymath::with_tolerance!(numeric.tolerance_at(v.tag()), |t| with_floats!(v, |v| each(v, t))), - View::Complexes(_) | View::Characters(_) => return None - }) -} diff --git a/src/eval.rs b/src/eval.rs index 0f27f963..31100cdf 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -574,15 +574,16 @@ impl Function { // Inlined into `dispatch`, this would put a second copy of `call` and `dispatch` in the stack frame of every BPL call. #[inline(never)] fn ruled(&self, left: Option<&Value>, right: &Value, span: &Span) -> Result { self.call(left, right, span) } - /// `traced` run, from a program, on this function, or on one with its data replaced (`trace::tacit`), and these arguments, or `None` where no program takes the call and the interpreter runs it. `∂` asks first (`derivative::differentiate`). A dfn keeps its programs on its definition, which every call of its text shares. Any other function's programs are kept by its structure (`trace::Tacit`). + /// `traced` run, from a program, on this function, or on one with its data replaced (`trace::tacit`), and these arguments, or `None` where no program takes the call and the interpreter runs it. Body reuse and `∂` supply distinct purposes, so their recordings and stops stay separate. A dfn keeps its programs on its definition, which every call of its text shares. Any other function's programs are kept by its structure (`trace::Tacit`). pub(crate) fn programmed( - &self, left: Option<&Value>, right: &Value, span: &Span, traced: &dyn Fn(&Function, Option<&Value>, &Value) -> Result + &self, purpose: crate::program::trace::Purpose, previous: Option<&mut Option>, + left: Option<&Value>, right: &Value, span: &Span, traced: &dyn Fn(&Function, Option<&Value>, &Value) -> Result ) -> Option> { match self.node() { FunctionNode::Defined(closure) => { - crate::program::trace::programmed(closure.definition.programs(), closure.environment, left, right, span, &[], &|_, x, y| traced(self, x, y)) + crate::program::trace::programmed(closure.definition.programs(), purpose, previous, closure.environment, left, right, span, &[], &|_, x, y| traced(self, x, y)) } - _ => crate::program::trace::tacit(self, left, right, span, traced), + _ => crate::program::trace::tacit(self, purpose, previous, left, right, span, traced), } } /// Calls the function as a running level takes it, where one carries an argument (`transform::call`). Otherwise it calls the function that computes this call through a fast combination, where the form rewriter gave one (`rewrite`), or sends compositions, forks and dfns straight to their handlers, with the arguments as the caller gave them, and other functions to `apply`. @@ -1075,7 +1076,7 @@ pub struct Session { pub(crate) drawn: crate::distribution::Drawn, /// The program's command-line arguments. pub(crate) args: std::cell::RefCell>, - /// The programs that `∂` builds for functions other than dfns, kept by the functions' structure (`trace::tacit`). + /// The programs built for functions other than dfns, kept by the functions' structure (`trace::tacit`). pub(crate) tacit: crate::program::trace::Tacit, } diff --git a/src/eval/assign.rs b/src/eval/assign.rs index e8c003d9..a778738c 100644 --- a/src/eval/assign.rs +++ b/src/eval/assign.rs @@ -370,9 +370,7 @@ impl Session { let (binding, selection, values) = match self.direct_target(&mut nodes)? { Some((binding, selection)) => (binding, selection, right.clone()), None => { - let level = self.begin(crate::transform::Kind::Label); - let selected = self.selection_expression(&nodes, level); - self.end(); + let (level, selected) = self.with_level(crate::transform::Kind::Label, |level| (level, self.selection_expression(&nodes, level))); let (binding, mut labels, selected) = selected?; let read = labels.read(&selected, level); // The labelled value holds the array too, so dropping it lets the write change the array in place. @@ -504,7 +502,7 @@ impl Session { latest.insert(path, i); } crate::explain::accumulated(selection.targets.len(), original.len(), "item by item"); - crate::explain::itemwise(rustymath::events::Route::Accumulation, selection.targets.len()); + crate::explain::itemwise(crate::events::routes::Accumulation, selection.targets.len()); let values = match &selection.frame { ResultFrame::Direct => results.pop().unwrap_or(right), ResultFrame::Array(layout) if !results.is_empty() => { diff --git a/src/eval/operators.rs b/src/eval/operators.rs index 3aead603..aa29553a 100644 --- a/src/eval/operators.rs +++ b/src/eval/operators.rs @@ -21,6 +21,26 @@ use rustymath::error::Kind; use rustymath::{agreement::Mapping, hashing::{HashMap, HashMapExt}}; use std::borrow::Cow; +/// One operator invocation's previous signature, not another program cache. +struct BodyCaller<'a> { f: &'a Function, previous: Option } + +impl<'a> BodyCaller<'a> { + fn new(f: &'a Function) -> Self { Self { f, previous: None } } + fn call(&mut self, left: Option, right: R, span: &Span) -> Result { + let dense = |v: &Value| !v.is_atom() && crate::plan::Desc::describes(v); + // Value natives observe every argument; a formal trace input has no value to observe. + if !matches!(self.f.node(), FunctionNode::System(f) if matches!(&f.call, crate::system::Call::Value(_))) + && !session().prototype.get() && dense(right.value()) && left.as_ref().is_none_or(|v| dense(v.value())) { + check().at(span)?; + if let Some(result) = self.f.programmed(crate::program::trace::Purpose::Call, Some(&mut self.previous), + left.as_ref().map(Argument::value), right.value(), span, &|f, x, y| f.call_with(x, y, span)?.array(span)) { + return result.map(Binding::Value); + } + } else { self.previous = None; } + self.f.call_with(left, right, span).map(|result| result.value) + } +} + /// `f` called on `left` and `right` under `mark`: through the mark's own function, or on the arguments that the mark converts, where `f`'s primitive has one for it (`Primitive::marked`), and otherwise with its results converted (`converted_by`). pub(super) fn marked(f: &Function, mark: crate::number::Mark, left: Option<&Value>, right: &Value, span: &Span) -> Result { use crate::primitive::Marked; diff --git a/src/eval/operators/agenda.rs b/src/eval/operators/agenda.rs index 4b6e8db1..48f96d3c 100644 --- a/src/eval/operators/agenda.rs +++ b/src/eval/operators/agenda.rs @@ -114,7 +114,7 @@ fn selected( }; let [no, yes] = cases; let (y, x) = (items(no)?, items(yes)?); - if rustymath::events::running() { crate::explain::fast(rustymath::events::Route::MaskedSelect, text()); } + if crate::events::running() { crate::explain::fast(crate::events::routes::MaskedSelect, text()); } Ok(Some(crate::fusion::select(mask, &x, &y, right.layout(), &plan))) } @@ -441,9 +441,9 @@ fn fronted(first: &[usize], n: usize) -> Vec { first.iter().copied().chai /// `f@g` for a function `g`: `f⍥g`, with the result written back. A `g` that selects writes it back where it took it from. Any other `g` is undone by its inverse. fn under_function(f: &Function, g: &Function, left: Option<&Value>, right: &Value, span: &Span) -> Result { - let level = session().begin(crate::transform::Kind::Label); - let selected = crate::selection::labelled(right, level).and_then(|(labels, y)| Ok((labels, g.call_with(None::<&Value>, y, span)?.array(span)?))); - session().end(); + let (level, selected) = session().with_level(crate::transform::Kind::Label, |level| { + (level, crate::selection::labelled(right, level).and_then(|(labels, y)| Ok((labels, g.call_with(None::<&Value>, y, span)?.array(span)?)))) + }); let (mut labels, selected) = selected?; let read = labels.read(&selected, level); let x = left.map(|x| g.call_array(None, x, span)).transpose()?; diff --git a/src/eval/operators/each.rs b/src/eval/operators/each.rs index 6fe11cc5..c3a9a173 100644 --- a/src/eval/operators/each.rs +++ b/src/eval/operators/each.rs @@ -7,7 +7,7 @@ pub(super) fn each_pair( whole: impl FnOnce() -> Option, mixed: Option<&'static str>, span: &Span) -> Result { // One cell of atoms runs faster on its own than through a batched call. A cell of rank 1 or more doesn't. let atoms = len == 1 && { let (x, y) = pair(0)?; x.as_ref().is_none_or(Value::is_atom) && y.is_atom() }; - if !atoms { + if !atoms || crate::session().tracing().is_some() { if let Some(result) = crate::batch::tried(operand, whole, &layout, mixed.is_some(), span) { return result.map(Bound::from); } } // A recording can take the batched route; reject only the per-item fallback here. @@ -15,9 +15,10 @@ pub(super) fn each_pair( let (x, y) = pair(0)?; crate::program::trace::per_item(x.as_ref(), &y)?; } + let mut body = BodyCaller::new(operand); let items = assembled(len, layout, mixed, |i, empty| { let (x, y) = pair(i)?; - result_item(if empty { on_prototypes(operand, x.as_ref(), &y, span)? } else { operand.call_with(x, y, span)? }.value) + result_item(if empty { on_prototypes(operand, x.as_ref(), &y, span)?.value } else { body.call(x, y, span)? }) })?; Ok(if mixed.is_some() { Bound::from(items) } else { Bound::new(Binding::Value(items)) }) } diff --git a/src/eval/operators/folds.rs b/src/eval/operators/folds.rs index 4ad02983..0ded28d9 100644 --- a/src/eval/operators/folds.rs +++ b/src/eval/operators/folds.rs @@ -180,8 +180,11 @@ fn fold_array(operand: &Function, kind: FoldKind, axis: Option, seed: Opt FoldPlan::Whole(items) => items, FoldPlan::Done(result) => result, FoldPlan::Lanes { lanes, shape } => match by_lanes(operand, kind, seed, right, along, &lanes, span) { - Some(result) => result?, None => item_lanes( - kind.scan, &lanes, shape, seed, right, |x, y| operand.call_with(Some(x), y, span)?.array(span), crate::transform::carried(None, right))? + Some(result) => result?, None => { + let mut body = BodyCaller::new(operand); + item_lanes(kind.scan, &lanes, shape, seed, right, + |x, y| body.call(Some(x), y, span)?.into_value(span), crate::transform::carried(None, right))? + } }, }; folded(result, kind, along, right) @@ -256,7 +259,7 @@ fn item_lanes( scan: bool, lanes: &Lanes, shape: Vec, seed: Option<&Value>, right: &Value, apply: impl FnMut(Value, Value) -> Result, active: bool ) -> Result { crate::program::trace::per_item(seed, right)?; - if !active { crate::explain::itemwise(rustymath::events::Route::ItemLanes, right.len()); } + if !active { crate::explain::itemwise(crate::events::routes::ItemLanes, right.len()); } let data = if scan { scan_items(lanes, seed.cloned(), |i| right.at(i), apply)? } else { fold_items(lanes, seed, |i| right.at(i), apply)? }; let value = Value::new(shape, data).described(if scan { "invalid scan result" } else { "invalid fold result" })?; Ok(if active { crate::batch::joined(&value).unwrap_or(value) } else { value }) @@ -264,7 +267,7 @@ fn item_lanes( /// A fold or scan by `op` of plain numbers, with an optional `seed`: the general path for an arithmetic primitive when every item is a number. fn number_lanes(op: crate::number::Arithmetic, scan: bool, right: &Value, seed: Option<&Value>, axis: &Lanes, shape: Vec) -> Result { - crate::explain::itemwise(rustymath::events::Route::ItemLanes, right.len()); + crate::explain::itemwise(crate::events::routes::ItemLanes, right.len()); let number = |e: Value| { let Value::Number(n) = e else { unreachable!() }; n }; let (item, seed) = (|i| number(right.at(i)), seed.cloned().map(number)); let (p, t) = (Primitive::Arithmetic(op), Numeric::current()); diff --git a/src/eval/operators/key.rs b/src/eval/operators/key.rs index 5f3c658d..441bd46f 100644 --- a/src/eval/operators/key.rs +++ b/src/eval/operators/key.rs @@ -18,23 +18,23 @@ fn key_cells<'v>(left: Option<&'v Value>, right: &'v Value) -> Result<(&'v Value /// Key's fast combinations (`rewrite`): `GroupCounts` counts each group without building it, and `GroupFolds` folds each group of a vector in one pass, with no call for each group. Other arguments take Key's general path with the operand that the combination stands for, `≢⊢` or the fold of `⊢`, which reuses the class numbers that the fold read. pub(in crate::eval) fn grouped(s: crate::primitive::Special, left: Option<&Value>, right: &Value, span: &Span) -> Result { - use {crate::primitive::Special, rustymath::events::Route}; + use crate::primitive::Special; let (keys, key_cells) = key_cells(left, right)?; let (operand, numbered) = match s { Special::GroupCounts if keys.shape()[0] > 0 => { let sizes = crate::search::class_counts(&key_cells, span)?; let value = Value::positions(vec![sizes.len()], keys.shape()[0] + 1, sizes.into_iter()).described("invalid key result")?; - crate::eval::rewrite::taken(Route::GroupCounts, s); + crate::eval::rewrite::taken(crate::events::routes::GroupCounts, s); return Ok(Bound::from(value)); } Special::GroupFolds(by, first) => { let suits = left.is_some() && keys.shape()[0] > 0 && right.shape().len() == 1 && !crate::transform::carried(None, right); let numbered = if suits { Some(crate::search::numbered(&key_cells, span)?) } else { None }; if let Some(result) = numbered.as_ref().and_then(|n| crate::pervasive::fold_groups(by.primitive(), right, n)) { - crate::eval::rewrite::taken(Route::GroupFolds, s); + crate::eval::rewrite::taken(crate::events::routes::GroupFolds, s); return Ok(Bound::from(result)); } - rustymath::events::declined(Route::GroupFolds, "arguments that don't suit it", right.len()); + crate::events::declined(crate::events::routes::GroupFolds, "arguments that don't suit it", right.len()); (Function::new(FunctionNode::Fold(Function::primitive(by.primitive()), FoldKind { scan: false, first }), span)?, numbered) } _ => (Function::primitive(Primitive::Tally), None), @@ -71,7 +71,7 @@ fn groups( return Ok(Bound::from(Value::assemble(&[0], &[], &result).described("invalid key result")?)); } let firsts = &classes.firsts; - crate::explain::itemwise(rustymath::events::Route::KeyGroups, count); + crate::explain::itemwise(crate::events::routes::KeyGroups, count); let mut results = Vec::with_capacity(count); let mut apply = |g: usize, y: Value, span: &Span| -> Result<(), Error> { results.push(f.call_with(Some(&*key_cells.get(firsts[g])), y, span)?.array(span)?); diff --git a/src/eval/operators/power.rs b/src/eval/operators/power.rs index abae328b..511d15da 100644 --- a/src/eval/operators/power.rs +++ b/src/eval/operators/power.rs @@ -30,9 +30,11 @@ pub(super) fn power(operands: &[Operand; 2], left: Option<&Value>, right: impl A } if n == 0 { return Ok(Bound::from(right.owned())); } // The first step takes the argument as the caller gave it, and each later step takes the state that the step before made. - let mut result = f.call_with(left, right, span)?; - for _ in 1..n { result = f.call_with(left, result.array(span)?, span)?; } - Ok(result) + if n == 1 { return f.call_with(left, right, span); } + let mut body = BodyCaller::new(f); + let mut value = body.call(left, right, span)?.into_value(span)?; + for _ in 1..n - 1 { value = body.call(left, value, span)?.into_value(span)?; } + f.call_with(left, value, span) } // An array of counts gives one item for each count, holding the state after that many steps. Operand::Value(count) => { @@ -252,7 +254,7 @@ fn inverse_scan(f: &Function, h: FoldKind, axis: Option<&Value>, seed: Option<&V let seed = seed.map(|s| crate::primitive::weakened_beside(s, crate::plan::Kind::value(right)).unwrap_or_else(|| s.clone())); if let Some(result) = crate::pervasive::inverse_scan(p, right, seed.as_ref(), &axis) { return Ok(result); } } - crate::explain::itemwise(rustymath::events::Route::InverseScan, right.len()); + crate::explain::itemwise(crate::events::routes::InverseScan, right.len()); let mut data: Vec<_> = right.elements().collect(); for i in 0..axis.outer { for j in usize::from(seed.is_none())..axis.len { diff --git a/src/eval/operators/products.rs b/src/eval/operators/products.rs index 605fcbc2..4fafed44 100644 --- a/src/eval/operators/products.rs +++ b/src/eval/operators/products.rs @@ -140,20 +140,19 @@ pub(crate) fn primitive_product(f: Primitive, g: Primitive, left: &Value, right: /// Monadic `f.g` for primitives `f` and `g`, as `Operation::run` and the combinations `Determinant` and `Permanent` give it: for a square matrix of numbers, the determinant of `-.×` (`primitive::determinant`) and, below 64 rows, the permanent of `+.×`, and otherwise the expansion by minors that function operands take (`expansion`), whose calls of the operation by its primitives a running level takes. pub(crate) fn primitive_expansion(f: Primitive, g: Primitive, right: &Value, span: &Span) -> Result { - use rustymath::events::Route; let route = match Operation::Product(f, g) { - Operation::MINUS_TIMES => Some(Route::Determinant), Operation::PLUS_TIMES => Some(Route::Permanent), _ => None + Operation::MINUS_TIMES => Some(crate::events::routes::Determinant), Operation::PLUS_TIMES => Some(crate::events::routes::Permanent), _ => None }; if let Some(route) = route { let square = matrix(right).filter(|&[m, n]| m == n && n > 0 && !crate::transform::carried(None, right)).map(|[_, n]| n); let fast = match (route, square) { - (Route::Determinant, Some(n)) => crate::primitive::determinant(right, n)?, (Route::Permanent, Some(n)) if n < 64 => permanent(right, n)?, _ => None + (crate::events::routes::Determinant, Some(n)) => crate::primitive::determinant(right, n)?, (crate::events::routes::Permanent, Some(n)) if n < 64 => permanent(right, n)?, _ => None }; let Some(value) = fast else { - rustymath::events::declined(route, "arguments that don't suit it", right.len()); + crate::events::declined(route, "arguments that don't suit it", right.len()); return expansion(&Function::primitive(f), &Function::primitive(g), right, span)?.array(span); }; - if rustymath::events::running() { crate::explain::fast(route, format!("{}.{}", f.glyph(), g.glyph())); } + if crate::events::running() { crate::explain::fast(route, format!("{}.{}", f.glyph(), g.glyph())); } return Ok(value); } expansion(&Function::primitive(f), &Function::primitive(g), right, span)?.array(span) diff --git a/src/eval/rewrite.rs b/src/eval/rewrite.rs index e56601d2..1c46abde 100644 --- a/src/eval/rewrite.rs +++ b/src/eval/rewrite.rs @@ -10,7 +10,7 @@ use super::{ use crate::primitive::{Folding, Order, Pick, Special}; use crate::syntax::{Node, NodeKind, cover}; use crate::{Described, Error, Span, Value, name::Name, primitive::{FoldKind, OperatorKind, Primitive}}; -use rustymath::events::Route; +use crate::events::Route; use std::{borrow::Cow, cell::Cell, convert::Infallible, ops::Range, sync::OnceLock}; /// Each fast combination apart from the selections, which `rules` adds: its pattern, and the function that computes it. @@ -430,16 +430,16 @@ pub(crate) fn combined(s: Special, left: Option<&Value>, right: &Value, span: &S } let (route, fast) = match s { Special::Selected(order, pick) => { - (if matches!(order, Order::Grade(_)) { Route::PartialGrade } else { Route::PartialSort }, selected(order, pick, left, right, span)) + (if matches!(order, Order::Grade(_)) { crate::events::routes::PartialGrade } else { crate::events::routes::PartialSort }, selected(order, pick, left, right, span)) } - Special::Ranks(down) => (Route::Ranks, ranks(down, right, span)), + Special::Ranks(down) => (crate::events::routes::Ranks, ranks(down, right, span)), Special::GroupCounts | Special::GroupFolds(..) => return grouped(s, left, right, span)?.array(span), Special::Determinant => return primitive_expansion(Primitive::Arithmetic(Minus), Primitive::Arithmetic(Times), right, span), Special::Permanent => return primitive_expansion(Primitive::Arithmetic(Plus), Primitive::Arithmetic(Times), right, span), _ => unreachable!("a kernel computes this combination"), }; if let Some(result) = fast { if result.is_ok() { taken(route, s) } return result; } - rustymath::events::declined(route, "arguments that don't suit it", right.len()); + crate::events::declined(route, "arguments that don't suit it", right.len()); match s { Special::Selected(order, pick) => pick.primitive().call(left, &order.primitive().call(None, right, span)?, span), Special::Ranks(down) => Primitive::Grade(false).call(None, &Primitive::Grade(down).call(None, right, span)?, span), @@ -448,7 +448,7 @@ pub(crate) fn combined(s: Special, left: Option<&Value>, right: &Value, span: &S } /// Records for `•explain`, where it's on, that the function of combination `s` computed it by its own algorithm, along `route`. It names the combination by its row's glyph, as `↑⍋` names every selection. -pub(super) fn taken(route: Route, s: Special) { if rustymath::events::running() { crate::explain::fast(route, Primitive::Special(s).glyph().into()) } } +pub(super) fn taken(route: Route, s: Special) { if crate::events::running() { crate::explain::fast(route, Primitive::Special(s).glyph().into()) } } /// The items of `right`'s order that `pick` takes, through rustymath's partial selection, where `right` is a vector without keys. `left` is the count to take or the position to pick. fn selected(order: Order, pick: Pick, left: Option<&Value>, right: &Value, span: &Span) -> Option> { diff --git a/src/eval/session.rs b/src/eval/session.rs index 1de73673..aad36088 100644 --- a/src/eval/session.rs +++ b/src/eval/session.rs @@ -127,17 +127,21 @@ impl Session { /// Whether a level whose kind wraps values is running (`transform::Levels::transforming`). #[inline] pub(crate) fn transforming(&self) -> bool { self.transforming.get() } - /// Starts a level of `kind` and returns its id, which is larger than every running level's. A `∂` call's id is its tag, and a record level's id is the level its records carry. - pub(crate) fn begin(&self, kind: crate::transform::Kind) -> u64 { + /// Runs within a transformation level, whose id exceeds every outer level's. The level exits on return, error or panic. + pub(crate) fn with_level(&self, kind: crate::transform::Kind, run: impl FnOnce(u64) -> T) -> T { + struct End<'a>(&'a Session); + impl Drop for End<'_> { fn drop(&mut self) { self.0.end(); } } let frames = self.bindings.depth(); let id = self.levels.borrow_mut().begin(kind, frames); self.transforming.set(true); - id + let _end = End(self); + run(id) } - /// Ends the innermost level, applying its kind's exit rule to each binding outside its frames that it wrote (`transform::Kind::exit`). - pub(crate) fn end(&self) { + /// Ends the innermost level and restores routing. Normal returns also strip its data from bindings it wrote; panic unwinding does no language work. + fn end(&self) { let level = self.levels.borrow_mut().end(); self.transforming.set(self.levels.borrow().transforming()); + if std::thread::panicking() { return; } let Some(exit) = level.kind.exit() else { return }; for place in level.writes { let Some(Binding::Value(v)) = self.bindings.get(place) else { continue }; diff --git a/src/events.rs b/src/events.rs new file mode 100644 index 00000000..58914db3 --- /dev/null +++ b/src/events.rs @@ -0,0 +1,90 @@ +//! BPL's diagnostic vocabulary. Rustymath owns capture and mathematical route declarations. +pub(crate) use rustymath::events::{Column, Event, EventKind, Field, Record, Report, Route, Run, Sink, Table, declined, running, taken}; + +#[allow(non_upper_case_globals)] +pub(crate) mod routes { + use super::Route; + macro_rules! routes { + ($($id:ident: $name:literal, $taken:literal, $declined:literal, $itemwise:literal;)+) => { + pub const ALL: &[Route] = &[$($id),+]; + $(pub const $id: Route = Route::new($name, $taken, $declined, $itemwise);)+ + }; + } + routes! { + Trace: "trace", true, false, false; + Fusion: "fusion", true, true, false; + Batching: "batching", true, true, false; + PartialGrade: "partial grade", true, true, false; + PartialSort: "partial sort", true, true, false; + Ranks: "ranks", true, true, false; + GroupCounts: "group counts", true, false, false; + GroupFolds: "group folds", true, true, false; + Determinant: "determinant", true, true, false; + Permanent: "permanent", true, true, false; + MaskedSelect: "masked select", true, false, false; + Program: "program", false, true, false; + FusedFold: "fused fold", true, true, false; + Compiled: "compiled", true, true, false; + ItemPairs: "item pairs", false, false, true; + ItemLanes: "item lanes", false, false, true; + NumberParts: "number parts", false, false, true; + Accumulation: "accumulation", false, false, true; + NestedLabels: "nested labels", false, false, true; + LabelIds: "label ids", false, false, true; + InverseScan: "inverse scan", false, false, true; + KeyGroups: "key groups", false, false, true; + BatchCells: "batch cells", false, false, true; + BatchedItems: "batched items", false, false, true; + DerivativeItems: "∂ items", false, false, true; + TracedItems: "traced items", false, false, true; + } +} + +pub fn event_routes() -> impl Iterator { Route::ALL.iter().chain(routes::ALL).copied() } + +macro_rules! detail_value { ($field:ident) => { $field }; ($field:ident, $value:expr) => { $value }; } +macro_rules! detail_label { () => { false }; (label) => { true }; } +macro_rules! details { + ($($variant:ident, $table:ident, $name:literal { $($field:ident: $ty:ty => $kind:ident $(($value:expr))? $(@$label:ident)?),* };)+) => { + #[cfg_attr(web, expect(dead_code, reason = "shared report schema retains native-JIT compile rows"))] + pub(crate) enum Detail { $($variant { $($field: $ty),* }),+ } + $(const $table: Table = ($name, &[$((stringify!($field), Column::$kind)),*]);)+ + impl Detail { + pub(crate) fn event(self) -> Event { + match self { $(Self::$variant { $($field),* } => Event::Row { + table: &$table, + fields: vec![$(Field::$kind(detail_value!($field $(, $value)?).into())),*], + label: [$(detail_label!($($label)?)),*].into_iter().position(|label| label), + }),+ } + } + } + }; +} +details! { + Fused, FUSED, "fused" { glyphs: String => Text, items: usize => Count, blocks: usize => Count, runs: usize => Count, copied: usize => Count }; + Stored, STORED, "stored" { glyphs: String => Text, items: usize => Count, reason: &'static str => Text @label, partway: bool => Truth }; + Forced, FORCED, "forced" { function: String => Text @label, line: usize => Count, column: usize => Count, items: usize => Count, reason: &'static str => Text, site: Option => Text(site.unwrap_or_default()) }; + Program, PROGRAMS, "programs" { event: &'static str => Text, steps: usize => Count, listing: String => Text }; + Fast, FAST, "fast" { rule: &'static str => Text, function: String => Text }; + Fallback, FALLBACKS, "fallbacks" { reason: String => Text @label, items: usize => Count }; + Accumulation, ACCUMULATIONS, "accumulations" { items: usize => Count, size: usize => Count, path: &'static str => Text }; + Mixed, MIXED, "mixed" { code: String => Text @label, line: usize => Count, column: usize => Count, target: &'static str => Text, written: &'static str => Text, items: usize => Count }; + Compile, COMPILES, "compiles" { glyphs: String => Text, microseconds: usize => Count }; + Itemwise, ITEMWISE, "itemwise" { function: String => Text, line: usize => Count, column: usize => Count, route: Route => Text(route.name()) @label, items: usize => Count }; +} + +pub(crate) const TABLES: &[&Table] = &[ + &FUSED, &STORED, &FORCED, &PROGRAMS, &rustymath::events::STOPPED, &FAST, &FALLBACKS, &rustymath::events::DECLINED, + &ACCUMULATIONS, &MIXED, &COMPILES, &rustymath::events::ROUTES, &ITEMWISE, +]; + +#[inline] +pub(crate) fn emit(make: impl FnOnce(bool) -> Detail) { rustymath::events::emit(|sites| make(sites).event()); } + +#[inline] +pub(crate) fn stopped(reason: &'static str, items: usize, site: impl FnOnce() -> Option) { + rustymath::events::emit(|sites| Event::Route(Record { + kind: EventKind::Stopped, route: routes::Trace, reason, calls: 1, items, site: if sites { site() } else { None }, + })); +} + diff --git a/src/explain.rs b/src/explain.rs index 46f0c342..5a5d115a 100644 --- a/src/explain.rs +++ b/src/explain.rs @@ -1,8 +1,8 @@ -//! The events that `•explain` reports about a run, recorded as rustymath's producer events (`rustymath::events`), which BPL's `•explain` and rustymath's Python `explain` share, and rendered as `events::TABLES` lists them. Events are recorded only while a capture runs, as `•explain` and the route checks run one. Code on every thread records into the run's store, which `rustymath::events` holds for the whole process while the run lasts, because fusion runs its programs without a session. A process supports one capture at a time, apart from captures nested on one thread. +//! BPL report rendering and cost counts. `events` owns its vocabulary; rustymath supplies the shared process-wide capture, including worker threads and captures nested on one thread. //! //! The module also counts the pending arrays alive in the process, which `•memory` reports. use crate::{Described, Error, Value, keyed}; -use rustymath::events::{self, Column, Event, EventKind, Field, Report, Route}; +use crate::events::{self, Column, Event, EventKind, Field, Report, Route, Detail}; use std::sync::{Arc, Mutex, MutexGuard, PoisonError, atomic::{AtomicUsize, Ordering}}; /// The live pending arrays. @@ -51,17 +51,17 @@ pub(crate) fn reporting() -> bool { crate::settings::reports_fusion() || events: /// Records a fused group of `items` items, whose calls have the glyphs `glyphs`, in the order they ran, with what `tally` counted while it ran. pub(crate) fn fused(glyphs: impl FnOnce() -> String, items: usize, tally: &Tally) { - events::taken(Route::Fusion, items); + events::taken(crate::events::routes::Fusion, items); let Some(c) = &tally.0 else { return }; let count = |n: &AtomicUsize| n.load(Ordering::Relaxed); - events::emit(|_| Event::Fused { glyphs: glyphs(), items, blocks: count(&c.blocks), runs: count(&c.runs), copied: count(&c.copied) }); + events::emit(|_| Detail::Fused { glyphs: glyphs(), items, blocks: count(&c.blocks), runs: count(&c.runs), copied: count(&c.copied) }); } /// Records a pending array that a fused group read as an input, computed on its own first. `event` gives the glyphs of its call, its item count and the reason the group couldn't compute it as one of its calls. pub(crate) fn stored(event: impl FnOnce() -> (String, usize, &'static str)) { events::emit(|_| { let (glyphs, items, reason) = event(); - Event::Stored { glyphs, items, reason, partway: false } + Detail::Stored { glyphs, items, reason, partway: false } }); } @@ -82,7 +82,7 @@ impl Tally { /// Records that `function`, at `line` and `column`, computed `items` items of pending arrays: its arguments, or only the items it read from them. pub(crate) fn forced(function: String, line: usize, column: usize, items: usize) { - events::emit(|_| Event::Forced { function, line, column, items, reason: "", site: None }); + events::emit(|_| Detail::Forced { function, line, column, items, reason: "", site: None }); } /// Counts `items` items that a pending array computed, on any thread. @@ -102,30 +102,30 @@ pub(crate) fn uncounted(f: impl FnOnce() -> T) -> T { /// Records that `function` computed the fast combination `route`. pub(crate) fn fast(route: Route, function: String) { events::taken(route, 0); fast_detail(route, function) } /// Adds function details for a route already counted by its numerical implementation. -pub(crate) fn fast_detail(route: Route, function: String) { events::emit(|_| Event::Fast { rule: route.name(), function }) } +pub(crate) fn fast_detail(route: Route, function: String) { events::emit(|_| Detail::Fast { rule: route.name(), function }) } /// Records an accumulation that added `items` items into a result of `size` items, each at a position that a table of offsets or ids gives, as scattering adds them. `path` names how it added them, as `pervasive::Route` names a scatter's paths, or `"rounds"`, `"one call"` or `"item by item"` for the general paths. -pub(crate) fn accumulated(items: usize, size: usize, path: &'static str) { events::emit(|_| Event::Accumulation { items, size, path }) } +pub(crate) fn accumulated(items: usize, size: usize, path: &'static str) { events::emit(|_| Detail::Accumulation { items, size, path }) } /// Records that a batched run's calls fell back to slower code, with `reason` and the number of `items` it concerns. The reason becomes text only while `•explain` records. A stop gives its cause (`transform::Stop`). -pub(crate) fn fallback(reason: impl std::fmt::Display, items: usize) { events::emit(|_| Event::Fallback { reason: reason.to_string(), items }) } +pub(crate) fn fallback(reason: impl std::fmt::Display, items: usize) { events::emit(|_| Detail::Fallback { reason: reason.to_string(), items }) } /// Records a dfn call that ran from a program: `"built"` where its trace built the program first, and `"replayed"` where it found one, with the program's `steps` and its `listing`, which is built only while `•explain` records. pub(crate) fn program(event: &'static str, steps: usize, listing: impl FnOnce() -> String) { - events::emit(|_| Event::Program { event, steps, listing: listing() }); + events::emit(|_| Detail::Program { event, steps, listing: listing() }); } /// Records the compile of a block stage's kernel, with the glyphs of the stage's calls that `glyphs` gives and the compile's `time`. #[cfg(not(web))] pub(crate) fn compiled(glyphs: impl FnOnce() -> String, time: std::time::Duration) { - events::emit(|_| Event::Compile { glyphs: glyphs(), microseconds: time.as_micros() as usize }); + events::emit(|_| Detail::Compile { glyphs: glyphs(), microseconds: time.as_micros() as usize }); } /// Records a write by the call at `span` that left a compact array of `items` items in mixed storage, with the array's kind before the write and the kind written, as `•storage` names them. pub(crate) fn mixed(span: &crate::Span, target: &'static str, written: &'static str, items: usize) { events::emit(|_| { let (line, column) = span.position(); - Event::Mixed { code: span.text().into(), line, column, target, written, items } + Detail::Mixed { code: span.text().into(), line, column, target, written, items } }); } @@ -146,9 +146,9 @@ pub(crate) fn claim_itemwise(mark: usize, function: impl FnOnce() -> String, spa } /// An `itemwise` event for each route in `entries`, with its items summed in the order of each route's first entry, for the call at `span` whose code is `function`. -fn claimed(entries: Vec<(Route, usize)>, function: String, span: &crate::Span) -> impl Iterator { +fn claimed(entries: Vec<(Route, usize)>, function: String, span: &crate::Span) -> impl Iterator { let (line, column) = span.position(); - by_route(entries).into_iter().map(move |(route, items)| Event::Itemwise { function: function.clone(), line, column, route, items }) + by_route(entries).into_iter().map(move |(route, items)| Detail::Itemwise { function: function.clone(), line, column, route, items }) } /// `entries` with each route's items summed, in the order of each route's first entry. @@ -163,21 +163,21 @@ fn by_route(entries: impl IntoIterator) -> Vec<(Route, us impl Events { /// `•explain`'s record: the expression's `value`, then a record for each of `events::TABLES`, holding its columns, with one row for each event. Counts that no call claimed count for the `•explain` call at `span`. pub fn report(mut self, value: Value, span: &crate::Span) -> Result { - for event in claimed(std::mem::take(&mut self.unclaimed), span.text().into(), span) { self.report.record(event); } + for event in claimed(std::mem::take(&mut self.unclaimed), span.text().into(), span) { self.report.record(event.event()); } let record = |names: Vec<&str>, values: Vec| keyed::vector(names.into_iter().map(Arc::from).collect(), values).described("invalid explain record"); let mut tables = vec![value]; - for (table, &(_, columns)) in events::TABLES.iter().enumerate() { + for &table in events::TABLES { let rows: Vec<_> = self.report.records.iter().filter(|event| event.table() == table).map(Event::fields).collect(); let n = rows.len(); - let values = columns.iter().enumerate().map(|(c, &(_, column))| match column { + let values = table.1.iter().enumerate().map(|(c, &(_, column))| match column { Column::Text => keyed::texts(&[n], rows.iter().map(|row| keyed::text(row[c].text())).collect()).described("invalid explain texts"), Column::Count => Value::integers(vec![n], rows.iter().map(|row| row[c].count() as i64).collect()).described("invalid explain counts"), Column::Truth => Value::booleans(vec![n], rows.iter().map(|row| row[c].truth()).collect()).described("invalid explain truth values"), }).collect::>()?; - tables.push(record(columns.iter().map(|&(name, _)| name).collect(), values)?); + tables.push(record(table.1.iter().map(|&(name, _)| name).collect(), values)?); } - record(std::iter::once("value").chain(events::TABLES.iter().map(|&(name, _)| name)).collect(), tables) + record(std::iter::once("value").chain(events::TABLES.iter().map(|table| table.0)).collect(), tables) } pub fn observed(&self) -> &Report { &self.report } @@ -186,7 +186,7 @@ impl Events { pub fn slow(&self) -> Vec<(&'static str, String, String)> { let text = |event: &Event| { event.fields().iter().map(|f| match f { - Field::Text(t) => (*t).to_owned(), Field::Count(n) => n.to_string(), Field::Truth(t) => t.to_string() + Field::Text(t) => t.as_ref().to_owned(), Field::Count(n) => n.to_string(), Field::Truth(t) => t.to_string() }).collect::>().join(" ") }; let mut rows: Vec<_> = self.report.records.iter().filter_map(|event| Some((event.name(), event.slow_name()?.to_owned(), text(event)))).collect(); diff --git a/src/fusion.rs b/src/fusion.rs index 607e9343..7057349f 100644 --- a/src/fusion.rs +++ b/src/fusion.rs @@ -6,7 +6,7 @@ //! //! A program is data (`program::Program`). Its inputs are the arrays and atoms that the gathered calls read, held under plain layouts. A call on arrays with keys or axis names reads them through the maps that agreement gives, which read a missing key as a fill, and its pending array keeps the agreement's layout. A step reads a value of its own shape, or an atom. A step reads an array's items in order under another shape through its operand's `Read`, which holds that layout. An array read through any other map goes through a `Copy` step (`read`), which a block stage reads through without making the copy. A pending call's arrays hold items of the types that a block stage holds: floats of 32 or 64 bits, Booleans and integers. A call fuses where its kernel can't fail and gives the type that the call computes at, or Booleans, so that its plan fixes its result's type (`Pervasive::fuses`). Under dynamic kinds an ordinary integer's type depends on its values, so integers fuse there only at a fixed width. A computed array of Booleans or integers joins only a call that computes at its type: a block stage would convert it into a buffer on every run, where the call's own kernel converts each item as it reads it. The `fusion` entry of `nbs/system/prefs.qmd` lists the primitives that fuse. A comparison joins a program as any other pending array does. A select, the masked lowering of `⍚` and `@[p]` (`select`), reads a comparison as its mask and takes each item from one of two operands. A function that `pervasive::keeps_exact` takes no exact argument where it computes floats, a comparison included, because a program reads exact arguments as floats. A conversion of a pending array, as a width or fixed mark makes it, is one more pending call (`convert`), so the program stores only the converted items. Each step runs the kernel that `pervasive::map` runs for the same arguments. A program therefore gives the result that its calls give. //! -//! A structural glyph that copies one source through rustymath `Steps` (`remapped`) gives a pending copy (`copy`) where its result has at least `Sizes::min_items` items, for a source of any compact kind. Forcing composes a read of a copy with the copy's map, and moves a read with no repeat or fill under each pending pervasive call it reaches that only it holds (`Pending::resolved`). `2×⌽v+1` then runs as one program that reads `v` backwards. A read that copies (`Mapping::copies`), such as a reversal's, moves only under a call that reads one array, so `2×⌽y×x+0.5` computes `y×x+0.5` first and then reads it backwards. A call whose two array arguments copy arrays of one layout through one map that gives no fill runs on those arrays instead, and the map copies its result once (`copied_once`): `(3↕x)×3↕y` runs as `3↕x×y`. A copy of floats that no call reads through, and a copy of any other kind, walks its source as `remapped` does. A fold of a copy of windows along its own axis runs sliding folds over the source (`slide`), at a cost for each item that doesn't grow with the window's width. A short fold of windows over padding runs as a block stage that reads through the padding (`fold`). A fold along two axes of windows, as `⌈/⍠2 3` pools, folds both axes in one pass over the source (`fold_windows`). +//! A structural glyph that copies one source through rustymath `Steps` (`remapped`) gives a pending copy (`copy`) where its result has at least `Sizes::min_items` items, for a source of any compact kind. Forcing composes a read of a copy with the copy's map, and moves a read with no repeat or fill under each pending pervasive call it reaches that only it holds (`Pending::resolved`). `2×⌽v+1` then runs as one program that reads `v` backwards. A read that copies (`Mapping::copies`), such as a reversal's, moves only under a call that reads one array, so `2×⌽y×x+0.5` computes `y×x+0.5` first and then reads it backwards. A copy of floats that no call reads through, and a copy of any other kind, walks its source as `remapped` does. A fold of a copy of windows along its own axis runs sliding folds over the source (`slide`), at a cost for each item that doesn't grow with the window's width. A short fold of windows over padding runs as a block stage that reads through the padding (`fold`). A fold along two axes of windows, as `⌈/⍠2 3` pools, folds both axes in one pass over the source (`fold_windows`). use crate::array::{Footprint, Seen}; use crate::{ Described, Error, ErrorKind, Number, Source, Span, Value, agreement::{Agree, Agreement}, array::{Gather, Layout, Storage, generated_len}, explain::Live, @@ -34,8 +34,8 @@ const MAX_KEPT: usize = 8; /// An array whose items a program computes when something first reads them. The program holds the arrays and atoms that the call reads, among them other pending arrays, each pending array in a value of its own (`held`). Forcing gathers a chain of them into one program, which runs block by block. pub(crate) struct Pending { - /// What computes the items: a call on stored operands, or a typed program once calls compose. - work: Mutex>, + /// The program that computes the items. + work: Mutex>, result: OnceLock, /// Whether an actual read has already planned a nonempty selective computation of this result. partial_read: AtomicBool, @@ -48,60 +48,26 @@ pub(crate) struct Pending { pub(crate) range: Range, /// The numeric settings that planned the work, which forcing computes under (`settings::under`). numeric: Numeric, - /// The programs that forcing this array would gather: 1 plus the sizes of the pending arrays its program reads, counting a shared one each time, and at most `MAX_NODES`. + /// The steps that forcing this array would gather: its own steps plus the sizes of the pending arrays it reads, counting a shared one each time, and at most `MAX_NODES`. size: usize, /// The arrays that the chain of pending programs ending here kept alive when it was made, each once, as an id and its bytes (`kept_by`). Only an unforced array's list is read. kept: Vec<(usize, usize)>, _live: Live, } -enum Work { Program(Program), Call(Call) } - -impl Work { - /// The program, made from the lone call where this is one (`Call::program`). - fn into_program(self) -> Program { match self { Self::Program(p) => p, Self::Call(c) => c.program() } } - /// The program, where this is one rather than a lone call. - fn program(&self) -> Option<&Program> { match self { Self::Program(p) => Some(p), Self::Call(_) => None } } - /// The source and map of the copy that this makes (`copy_source`). A lone call is never a copy. - fn copied(&self) -> Option<(&Value, &Mapping)> { self.program().and_then(copy_source) } - /// The glyphs of the steps that compute the items, as `Program::glyphs` gives them. - fn glyphs(&self) -> String { match self { Self::Program(p) => p.glyphs(), Self::Call(c) => c.plan.p.glyph().into() } } -} - -/// A deferred pervasive call that reads no pending array: its plan, its arguments as the call got them, their agreement, and its result's description. -struct Call { plan: crate::plan::Pervasive, x: Option, y: Value, agreement: Agreement, desc: Desc } - -impl Call { - /// The values that the call reads. - fn held(&self) -> impl Iterator { self.x.iter().chain([&self.y]) } - /// The program that makes this call, as `defer` builds one for a call that reads pending arrays. - fn program(self) -> Program { - let Self { plan, x, y, agreement: Agreement { layout, left, right, .. }, desc } = self; - let mut nodes = Nodes::default(); - let left = x.map(|x| read(&mut nodes, &x, left, layout.shape())); - let right = read(&mut nodes, &y, right, layout.shape()); - ending(nodes, desc, Step::Map { left, right, plan }) - } - /// The call's result, made now with the agreement it was given, as eager dispatch makes it after it declines to defer (`Primitive::planned`). - fn made(&self) -> Result { Primitive::planned(&self.plan, self.x.as_ref(), &self.y, &FORCING, false, self.agreement.clone()) } -} - /// The pending array that node `node` holds, while it hasn't been computed. fn held_pending(node: &Node) -> Option<&Arc> { match &node.origin { Origin::Held(v) => v.pending(), _ => None } } /// The value that `program` copies, and the map through which it copies it, where the program is a pending copy: a copy of a held value. -fn copy_source(program: &Program) -> Option<(&Value, &Mapping)> { - let Origin::Step(Step::Copy { of, ref map }) = program.nodes[program.result.of].origin else { return None }; - match &program.nodes[of].origin { Origin::Held(v) => Some((v, map)), _ => None } +fn copy_source(program: &Program) -> Option<(&Value, Mapping)> { + let (source, map) = crate::program::positional_source(&program.nodes, program.result.of)?; + match &program.nodes[source.of].origin { Origin::Held(v) => Some((v, map)), _ => None } } -/// The arrays that `program` keeps alive, each once, as an id and its bytes (`kept_of`). -fn kept_by(program: &Program) -> Vec<(usize, usize)> { kept_of(program.nodes.iter().filter_map(|n| match &n.origin { Origin::Held(v) => Some(v), _ => None })) } - -/// The arrays that the values `held` keep alive, each once, as an id and its bytes: each array among them, and the arrays that each pending array among them keeps. An array's id is its `Value::storage_id`. -fn kept_of<'a>(held: impl Iterator) -> Vec<(usize, usize)> { +/// The arrays that `program` keeps alive, each once, as an id and its bytes: each held array and the arrays that its pending inputs keep. An array's id is its `Value::storage_id`. +fn kept_by(program: &Program) -> Vec<(usize, usize)> { let mut kept: Vec<(usize, usize)> = Vec::new(); - for v in held { + for v in program.nodes.iter().filter_map(|n| match &n.origin { Origin::Held(v) => Some(v), _ => None }) { let entries = match v.pending() { _ if v.is_atom() => Vec::new(), Some(p) => p.kept.clone(), None => vec![(v.storage_id(), v.footprint())] }; for entry in entries { if !kept.iter().any(|k| k.0 == entry.0) { kept.push(entry); } } } @@ -119,7 +85,7 @@ pub(crate) fn held_copy(value: &Value) -> bool { let Some(pending) = value.pending() else { return false }; if value.folded().is_none_or(|(_, shared)| shared) { return true; } let work = pending.work(); - work.as_ref().and_then(Work::copied).is_some_and(|(source, _)| held_copy(source)) + work.as_ref().and_then(copy_source).is_some_and(|(source, _)| held_copy(source)) } /// A read of `pending` narrowed to the positions that `outer` reaches over `frame`, as an array and a map, without consuming any pending work. `outer` maps `frame`'s positions to the pending array's own. A pending copy becomes a read of its source, through both maps. A pending array's first nonempty small read becomes a new pending array that computes only those positions of its elementwise program (`Program::narrowed`), even where something else holds it. A later read computes the whole array and keeps its items. Copies, empty reads and reads of fills alone don't count as that first read. Missing positions fill outside the new program, after it computes only the selected items. A pending array that the new program holds narrows the same way where it can. The new program holds any other pending array in a value of its own (`held`), which computes it whole and keeps its items. `None` where the read would compute `pending` whole: where the read isn't small, where its first read already narrowed, and where a map doesn't compose or the program can't narrow. @@ -130,8 +96,8 @@ fn narrow(pending: &Arc, outer: &Mapping, frame: &[usize], small: bool) return Some((source, outer.clone())); } let shape = pending.layout.shape(); - let work = pending.programmed(); - let program = work.as_ref()?.program()?; + let work = pending.work(); + let program = work.as_ref()?; if let Some((source, map)) = copy_source(program) { let mapping = map.composed(outer, shape, frame, small)?; let Some(p) = source.pending() else { return Some((source.clone(), mapping)) }; @@ -162,9 +128,9 @@ fn narrow(pending: &Arc, outer: &Mapping, frame: &[usize], small: bool) let result = narrowed.add_to(&mut nodes, |nodes, at, _, node| match node.origin { Origin::Step(Step::Copy { of, ref map }) => { let (value, mapping) = held_pending(&nodes[at[of]]).and_then(|p| narrow(p, map, node.desc.shape(), small))?; - Some(read(nodes, &value, mapping, node.desc.shape()).of) + Some(nodes.copied_value(&value, mapping, node.desc.shape()).of) } - Origin::Held(ref value) if value.pending().is_some() => Some(held(nodes, value)), + Origin::Held(ref value) if value.pending().is_some() => Some(nodes.held(value, Desc::held(value)).of), _ => None, }); Some(pending.with_work(Program::new(nodes, result).live(), frame)) @@ -203,7 +169,7 @@ fn resolved(program: Program) -> Program { }), _ => None, }; - if let Some((value, mapping)) = through { return Some(read(nodes, &value, mapping, shape).of); } + if let Some((value, mapping)) = through { return Some(nodes.copied_value(&value, mapping, shape).of); } let Origin::Step(Step::Copy { of, map }) = &node.origin else { return None }; let copy = crate::program::copy_of_copy(nodes, at[*of], map, shape)?; Some(nodes.add(Node { desc: node.desc.clone(), origin: Origin::Step(copy) })) @@ -217,8 +183,8 @@ fn moved(pending: &Arc, map: &Mapping, frame: &[usize]) -> Option<(Valu let shape = pending.layout.shape(); let Mapping::Steps { base: 0, steps: outer } = map else { return None }; if map.fills() || Arc::strong_count(pending) != 1 || !pending.runs() || outer.iter().any(Steps::spreads) || Steps::rereads(outer, shape) { return None; } - let mut work = pending.programmed(); - let program = work.as_ref()?.program()?; + let mut work = pending.work(); + let program = work.as_ref()?; let arrays = program.nodes.iter().filter(|n| matches!(n.origin, Origin::Held(_)) && !n.desc.atom).count(); if !matches!(program.nodes[program.result.of].origin, Origin::Step(Step::Map { .. })) || map.copies(crate::settings::sizes().block) && arrays > 1 { return None; @@ -291,7 +257,7 @@ pub(crate) fn chosen(mask: &Value, yes: &Value, no: &Value, span: &Span) -> Resu fn chosen_mapped((mask, m): (Value, Mapping), (x, xm): (Value, Mapping), (y, ym): (Value, Mapping), layout: &Layout, len: usize) -> Value { let held: Vec = [&mask, &y, &x].into_iter().map(|v| if v.is_atom() { Layout::from([1]).filled(v) } else { v.clone() }).collect(); let args = [(&held[0], &m, len), (&held[1], &ym, len), (&held[2], &xm, len)]; - pervasive::with_reads(&args, rustymath::agreement::Missing::Default, |reads, types| { + pervasive::with_reads(args, rustymath::agreement::Missing::Default, |reads, types| { let mut buffer = Buffer::with_capacity(types[2], len); rustymath::operation::choose((reads[0], DType::Boolean), &[reads[1], reads[2]], 0..len, types[2], false, &mut buffer) .expect("a select of floats of one width never fails"); @@ -299,17 +265,13 @@ fn chosen_mapped((mask, m): (Value, Mapping), (x, xm): (Value, Mapping), (y, ym) }).expect("a select reads compact items") } -/// Whether a call with a result of `len` items is deferred: the call settles (`pervasive::settles`), so that a deferred chain never meets an overflow or a change of kind, and its result has at least the sizes' `min_items` (`settings::sizes`). The one decision between running a call now and deferring it. A call large enough to wait that doesn't settle declines fusion. -pub(crate) fn defers(settles: bool, len: usize) -> bool { - if len < crate::settings::sizes().min_items { return false; } - if !settles { rustymath::events::declined(rustymath::events::Route::Fusion, "a result kind that depends on values", len); } - settles -} +/// Whether `len` reaches the size threshold for pending work under the current settings. +pub(crate) fn defers(len: usize) -> bool { len >= crate::settings::sizes().min_items } /// The call of `p` on `x` and `y` as a pending array, or `agreement` back where the call can't be deferred. Its plan must fix its result's type (`Pervasive::fuses`), and each array that it reads must hold items of a type that a block stage holds (`blocks::readable`). A stored array of Booleans or integers, or a pending copy of one, must also be of the type that the call computes at, unless the stage compiles on its first run (`blocks::compiles_on_first_run`): a compiled part converts such items in registers as it reads them, while a precompiled step converts them into a buffer on every run, which costs more than computing the call at once. Where the call would gather more than `MAX_NODES` calls, the pending arrays it reads run first. Out of line, so that its code stays out of the frame of dispatch, which stays on the stack through each level of recursion. #[inline(never)] -pub(crate) fn defer(p: Primitive, plan: &crate::plan::Pervasive, x: Option<&Value>, y: &Value, agreement: Agreement) -> Result { - if let Some(copied) = x.and_then(|x| copied_once(p, plan, x, y, &agreement)) { return Ok(copied); } +pub(crate) fn defer(plan: &crate::plan::Pervasive, x: Option<&Value>, y: &Value, agreement: Agreement) -> Result { + if !plan.fuses() { return declined(agreement, "a result kind that depends on values"); } let args = || [x, Some(y)].into_iter().flatten(); let at = plan.computes(); let compiles = compiles_on_first_run(agreement.len); @@ -317,53 +279,21 @@ pub(crate) fn defer(p: Primitive, plan: &crate::plan::Pervasive, x: Option<&Valu v.is_atom() || v.dtype() .is_some_and(|d| readable(d) && (compiles || matches!(d, DType::Float(_)) || Some(d) == at || v.pending().is_some_and(|p| p.computes()))) }) { return declined(agreement, "an argument of another kind"); } - let dtype = plan.result().expect("a plan that fuses fixes its result's type"); - let fixed = plan.numeric.fixed.filter(|_| matches!(dtype, DType::Integer(_))); - let prototype = Desc { kind: crate::plan::Kind::stored(Some(dtype), fixed, false), atom: true, layout: Default::default(), range: Range::ANY }.zero(); - let (layout, desc) = (agreement.layout.clone(), described(dtype, &prototype, agreement.layout.shape(), plan.result_range())); - let call = Call { plan: plan.clone(), x: x.cloned(), y: y.clone(), agreement, desc }; - if args().all(|v| v.pending().is_none()) { - let kept = kept_of(call.held()); - return Ok(Value::deferred(layout.clone(), Arc::new(Pending::new(Work::Call(call), layout, dtype, prototype, 1, kept)))); - } - Ok(pending_program(call.program(), layout, dtype, prototype)) + let mapped = crate::plan::Mapped::new(plan.clone(), agreement, false, false).expect("a fusible plan fixes its result type"); + let (layout, dtype, prototype) = (mapped.desc.layout.clone(), mapped.desc.dtype(), mapped.desc.zero()); + let mut nodes = Nodes::default(); + let left = x.map(|x| nodes.held(x, Desc::held(x))); + let right = nodes.held(y, Desc::held(y)); + let result = nodes.mapped(mapped, left, right); + Ok(pending_program(Program::new(nodes, result), layout, dtype, prototype)) } -/// `agreement` back, after `defer` declines to fuse its call for `reason` (`rustymath::events`). +/// `agreement` back, after `defer` declines to fuse its call for `reason` (`crate::events`). fn declined(agreement: Agreement, reason: &'static str) -> Result { - rustymath::events::declined(rustymath::events::Route::Fusion, reason, agreement.len); + crate::events::declined(crate::events::routes::Fusion, reason, agreement.len); Err(agreement) } -/// The node of `nodes` that holds `v`, an atom or an array under a plain layout of its own shape, added where no node holds it. A pending array is held in a value of its own, under the shape that the read sees it with, so that each program that holds it counts once among its holders (`Pending::gathered`). -fn held(nodes: &mut Nodes, v: &Value) -> usize { - let value = match v.pending() { Some(p) => Value::deferred(Layout::from(v.shape().to_vec()), p.clone()), None => unlabelled(v.clone()) }; - nodes.add(Node { desc: held_desc(v), origin: Origin::Held(value) }) -} - -/// The description of the node that holds `v` (`held`), which every plan of a step that reads it takes: a pending array's own, as its program planned it, or a value's as a constant (`Desc::held`). Characters, which only a copy reads, take a description that no step reads. -fn held_desc(v: &Value) -> Desc { - if let Some(p) = v.pending() { return described(p.dtype, &p.prototype, v.shape(), p.range); } - let value = unlabelled(v.clone()); - Desc::of(&value).unwrap_or_else(|| Desc { kind: crate::plan::Kind::value(&value), atom: false, layout: value.layout().clone(), range: Range::ANY }) -} - -/// The read that a step whose result has the shape `frame` takes of `v` through `mapping`: an atom, or a value of that shape. A value read in order under another shape of as many items is a view, and one read through any other map goes through a copy (`Step::Copy`), which a block stage reads through without making the copy. -fn read(nodes: &mut Nodes, v: &Value, mapping: Mapping, frame: &[usize]) -> Read { - let at = held(nodes, v); - let desc = &nodes[at].desc; - if desc.atom { return Read::node(nodes, at); } - if mapping.in_order(desc.layout.count(), frame) { return Read::new(at, frame.into()); } - let desc = Desc { layout: frame.into(), ..desc.clone() }; - let at = nodes.add(Node { desc, origin: Origin::Step(Step::Copy { of: at, map: mapping }) }); - Read::node(nodes, at) -} - -/// The program of `nodes` with `step` after them as its result, whose items `desc` describes. -fn ending(mut nodes: Nodes, desc: Desc, step: Step) -> Program { - let result = nodes.push(Node { desc, origin: Origin::Step(step) }); - Program::at(nodes, result) -} /// The description of a pending array's items as a value of its program: items of `dtype`, fixed where `prototype` is, laid out in `frame`, within `range`. fn described(dtype: DType, prototype: &Value, frame: &[usize], range: Range) -> Desc { @@ -382,20 +312,49 @@ fn keeps_too_much(kept: &[(usize, usize)], len: usize, dtype: DType) -> bool { kept.iter().map(|k| k.1).sum::() > MAX_KEPT * largest } +fn own_steps(program: &Program) -> usize { program.nodes.iter().filter(|n| matches!(n.origin, Origin::Step(_))).count() } + +/// The bounded ordinary-body subset: one dense result, existing fusible steps, and no unresolved pending views. +pub(crate) fn records_dense(program: &Program) -> bool { + !program.result.desc(&program.nodes).atom && program.outputs.is_empty() && (1..=MAX_NODES).contains(&own_steps(program)) && program.nodes.iter().all(|node| { + readable(node.desc.dtype()) && match &node.origin { + Origin::Step(Step::Map { plan, .. }) => plan.settled().is_ok() && plan.fuses(), + Origin::Step(Step::Copy { .. } | Step::Convert { .. } | Step::Select { .. }) => true, + Origin::Held(value) => !value.is_view() || value.pending().is_none(), Origin::Input(_) => !node.desc.atom, _ => false + } + }) +} + +/// Publishes an admitted dense recording through eager fusion's holding and pending-work rules. +pub(crate) fn recorded(program: &Program, inputs: &[&Value]) -> Option { + let desc = program.result.desc(&program.nodes); + if !defers(desc.layout.count()) || inputs.iter().any(|v| v.is_view() && v.pending().is_some()) { return None; } + let mut nodes = Nodes::default(); + let result = Program::new(program.nodes.iter().cloned().collect(), program.result.clone()).add_to(&mut nodes, |nodes, _, _, node| { + let value = match &node.origin { Origin::Input(k) => inputs[*k], Origin::Held(value) => value, _ => return None }; + Some(nodes.held(value, node.desc.clone()).of) + }); + let program = Program::new(nodes, result); + // Rebinding a repeated dense input can add a Copy beyond the recipe's own step count. + if own_steps(&program) > MAX_NODES { return None; } + Some(pending_program(program, desc.layout.clone(), desc.dtype(), desc.zero())) +} + /// `program` as a pending array of `layout`, holding items of `dtype`, with `prototype` as its prototype. Where gathering it would take more than `MAX_NODES` programs, or it would keep alive more than `MAX_KEPT` times its largest array, the pending arrays it reads run first. fn pending_program(program: Program, layout: Layout, dtype: DType, prototype: Value) -> Value { - let gathered = |program: &Program| 1 + program.nodes.iter().filter_map(held_pending).map(|p| p.size).sum::(); + let own = own_steps(&program); + let gathered = |program: &Program| own + program.nodes.iter().filter_map(held_pending).map(|p| p.size).sum::(); let (mut size, mut kept) = (gathered(&program), kept_by(&program)); if size > MAX_NODES || keeps_too_much(&kept, layout.count(), dtype) { for pending in program.nodes.iter().filter_map(held_pending) { pending.storage(); } - (size, kept) = (1, kept_by(&program)); + (size, kept) = (own, kept_by(&program)); } - let pending = Pending::new(Work::Program(program), layout.clone(), dtype, prototype, size, kept); + let pending = Pending::new(program, layout.clone(), dtype, prototype, size, kept); Value::deferred(layout, Arc::new(pending)) } /// Whether `select` defers a select by `mask` into a result of `layout`: where the result has at least `Sizes::min_items` items and `mask` holds Booleans still to be computed that nothing else holds. One program then computes the mask, the operands and the select together, and the mask never becomes stored Booleans. -pub(crate) fn defers_select(mask: &Value, layout: &Layout) -> bool { defers(true, layout.count()) && mask.unshared_pending().is_some_and(|c| c.compares()) } +pub(crate) fn defers_select(mask: &Value, layout: &Layout) -> bool { defers(layout.count()) && mask.unshared_pending().is_some_and(|c| c.compares()) } /// `x` where `mask` is 1 and `y` where it's 0, the masked lowering of `⍚` and `@[p]` (`selected` in `eval/operators/agenda.rs`), as `plan` plans it (`plan::select`). `x` and `y` hold floats of one width, each as an array of `layout` or an atom of that width, and `mask` holds Booleans of `layout`. The select is pending where `defers_select` says, and otherwise runs at once. pub(crate) fn select(mask: &Value, x: &Value, y: &Value, layout: &Layout, plan: &crate::plan::Select) -> Value { @@ -403,19 +362,19 @@ pub(crate) fn select(mask: &Value, x: &Value, y: &Value, layout: &Layout, plan: let len = layout.count(); return chosen_mapped((mask.clone(), Mapping::Linear(1)), (x.clone(), plan.yes.clone()), (y.clone(), plan.no.clone()), layout, len); } - let frame = layout.shape().to_vec(); let mut nodes = Nodes::default(); - let yes = read(&mut nodes, x, plan.yes.clone(), &frame); - let no = read(&mut nodes, y, plan.no.clone(), &frame); - let mask = read(&mut nodes, mask, Mapping::Linear(1), &frame); + let yes = nodes.held(x, Desc::held(x)); + let no = nodes.held(y, Desc::held(y)); + let mask = nodes.held(mask, Desc::held(mask)); let (dtype, prototype) = (plan.desc.dtype(), plan.desc.zero()); - pending_program(ending(nodes, described(dtype, &prototype, &frame, plan.desc.range), Step::Select { mask, yes, no }), layout.clone(), dtype, prototype) + let result = nodes.selected(plan, mask, yes, no); + pending_program(Program::new(nodes, result), layout.clone(), dtype, prototype) } /// `value` with its numbers converted to `to` as one more pending call, which its program computes as a step (`Step::Convert`). `value` is an array of at least `Sizes::min_items` items: a pending array that a program computes, whose program then stores only the converted items, as for a size mark after a fused chain, or a compact array of a type that a block stage holds or of f16, which a later call reads through the conversion: a block stage that fuses it with its steps, or a product (`widened`). `numeric` gives the tolerance of a float's floor. `None` for any other value, such as complex numbers, where `value` already holds items of `to`, and for a type `to` that a block stage doesn't hold. The size test runs inline. The rest runs out of line, in `pending_conversion`. #[inline] pub(crate) fn convert(value: &Value, to: DType, numeric: Numeric) -> Option { - if value.is_atom() || !defers(true, value.len()) { return None; } + if value.is_atom() || !defers(value.len()) { return None; } pending_conversion(value, to, numeric) } /// `convert`'s pending call, for an array that passed its size test. @@ -426,39 +385,53 @@ fn pending_conversion(value: &Value, to: DType, numeric: Numeric) -> Option Desc::of(&unlabelled(value.clone()))?, None => return None }; if desc.dtype() == to || !readable(to) { return None; } - let prototype = crate::plan::convert(&desc, to).ok()?.zero(); - let frame = value.shape().to_vec(); let mut nodes = Nodes::default(); - let of = read(&mut nodes, value, Mapping::Linear(1), &frame); - Some(pending_program(ending(nodes, described(to, &prototype, &frame, Range::ANY), Step::Convert { of, numeric }), value.layout().clone(), to, prototype)) + let of = nodes.held(value, desc); + let result = nodes.converted(of, to, numeric).ok()?; + let prototype = result.desc(&nodes).zero(); + Some(pending_program(Program::new(nodes, result), value.layout().clone(), to, prototype)) } /// The array that `value`, a pending conversion of a whole array in order (`convert`), converts, and the type it converts to. A product reads the array through the conversion where `pervasive::reads_widened` accepts the pair, which computes the array where it's pending, and the conversion then never runs. `None` for any other value, and once the conversion has run. pub(crate) fn widened(value: &Value) -> Option<(Value, DType)> { let pending = value.pending().filter(|p| !p.is_forced())?; let work = pending.work(); - let program = work.as_ref()?.program()?; + let program = work.as_ref()?; let Origin::Step(Step::Convert { ref of, .. }) = program.nodes[program.result.of].origin else { return None }; let Origin::Held(source) = &program.nodes[of.of].origin else { return None }; Some((source.clone(), pending.dtype)) } -/// `p` on `x` and `y`, run on the arrays that they copy, where both are pending copies of arrays of one layout through one map that gives no fill, the call pairs them item for item, and neither copy has fewer items than its source. The result copies the call's result once through the map, where a program would read each argument through it: `(3↕x)×3↕y` runs as `3↕x×y`. The sources hold what the copies hold, so `plan` serves the call on them too. `None` where the call can't run that way. -fn copied_once(p: Primitive, plan: &crate::plan::Pervasive, x: &Value, y: &Value, agreement: &Agreement) -> Option { - if !matches!((&agreement.left, &agreement.right), (Mapping::Linear(1), Mapping::Linear(1))) { return None; } - let ((xs, xm), (ys, ym)) = (source(x)?, source(y)?); - if xm != ym || xm.fills() || xs.layout() != ys.layout() || xs.len() > x.len() { return None; } - let inner = defer(p, plan, Some(&xs), &ys, crate::agreement::Agree::new(xs.layout(), ys.layout()).ok()?).ok()?; - deferred_copy(&inner, &agreement.layout, xm) -} - -/// A structural primitive's result: `form` with `params`, on `inputs`, laid out as `structural::plan` plans it on their layouts. A carried input or parameter hands the call to its carrier (`Carrier::structural`). A plan that reads its one source through a map copies it (`copied`). Any other plan reads every source into the kind that the sources holding items join to (`array::items_kind`), with `fill` at a position that reaches no item: compact sources and atoms, each atom as one item, in one typed read, and other sources item by item. Where no source holds an item, the result reads item by item and takes the first input's prototype, as `Gather::joined_kind` does. `Step::Structural` replays through it. +/// A structural call, planned once from its parameters and input layouts. pub(crate) fn copy(form: &Form, params: &[Value], inputs: &[&Value], fill: Option<&Value>) -> Result { if let Some(carrier) = crate::transform::newest_carrier(params.iter().chain(inputs.iter().copied())) { return carrier.structural(form, params, inputs, fill); } - let layouts: Vec<&Layout> = inputs.iter().map(|v| v.layout()).collect(); - let (layout, plan) = crate::primitive::structural::plan(form, params, &layouts)?; + let descriptions: Vec = inputs.iter().map(|v| Desc::held(v)).collect(); + let fill_desc = fill.map(Desc::held); + let (desc, plan) = crate::plan::structural(form, params, &descriptions, fill_desc.as_ref())?; + if !desc.atom && desc.kind.dtype.is_some() && fill.is_none() && defers(desc.layout.count()) + && let Some((source, map)) = plan.positional(desc.shape()) { + if let [value] = inputs && source == 0 && map.in_order(value.len(), desc.shape()) { + return value.relaid(desc.layout).described("a structural view"); + } + let (layout, dtype, prototype) = (desc.layout.clone(), desc.dtype(), desc.zero()); + let mut nodes = Nodes::default(); + let params = params.iter().map(|v| nodes.held(v, Desc::held(v))).collect(); + let inputs = inputs.iter().zip(descriptions).map(|(v, d)| nodes.held(v, d)).collect(); + let result = nodes.structured(form.clone(), params, inputs, None, desc, plan); + return Ok(pending_program(Program::new(nodes, result), layout, dtype, prototype)); + } + structural_value(&desc, &plan, inputs, fill, true) +} + +/// Executes retained structural geometry without re-scheduling the same copy. +pub(crate) fn structured(desc: &Desc, plan: &rustymath::agreement::ReadPlan, inputs: &[&Value], fill: Option<&Value>) -> Result { + structural_value(desc, plan, inputs, fill, false) +} + +fn structural_value(desc: &Desc, plan: &rustymath::agreement::ReadPlan, inputs: &[&Value], fill: Option<&Value>, defer: bool) -> Result { + let layout = desc.layout.clone(); let prototype = inputs.first().map_or_else(|| Value::Number(Number::from_bool(false)), |v| v.prototype()); let item = |i: usize| match plan.at(i) { Run::Source { source, .. } if source == inputs.len() => Ok(fill.expect("a plan reads its fill source only when the call has a fill").clone()), @@ -466,21 +439,19 @@ pub(crate) fn copy(form: &Form, params: &[Value], inputs: &[&Value], fill: Optio Run::Invalid => Err(Error::new(rustymath::error::Kind::Index, "a structural read reaches no source")), Run::Index { .. } => unreachable!("a structural plan reads sources, never indices") }; - // A copy of atoms to a result with no axes gives an atom where the form keeps atoms (`Form::keeps_atoms`), as `⌽3` and `⍬↓0` do, and otherwise a rank-0 array, as `⍬⍴5` gives. if layout.shape().is_empty() && inputs.iter().copied().chain(fill).all(|v| v.is_atom()) { - return if form.keeps_atoms() { item(0) } else { item(0)?.relaid(layout).described("a structural copy") }; + return if desc.atom { item(0) } else { item(0)?.relaid(layout).described("a structural copy") }; } - if let [source] = inputs && let Some(copy) = copied_through(source, &plan, &layout, fill) { return copy.described("a structural copy"); } + if let [source] = inputs && let Some(copy) = copy_through(source, plan, &layout, fill, defer) { return copy.described("a structural copy"); } let len = generated_len(layout.shape())?; let held: Vec> = inputs.iter().copied().chain(fill).map(|v| v.is_atom().then(|| Layout::from([1]).filled(v))).collect(); let views: Option>> = inputs.iter().copied().chain(fill).zip(&held).map(|(v, held)| held.as_ref().unwrap_or(v).view().map(Binding::full)).collect(); let sources: Vec<&Value> = inputs.iter().copied().chain(fill).collect(); - let kind = crate::array::Gather::joined_kind(&sources, crate::array::Widening::Arrays); - if let (Some(views), Some(dtype)) = (views, kind.and_then(|k| k.dtype)) { + if let (Some(views), Some(dtype)) = (views, desc.kind.dtype) { let mut out = Buffer::with_capacity(dtype, len); - PlanRead { sources: &views, plan: &plan }.read_into(0..len, dtype, true, &mut out)?; - return Value::from_bounded(layout, out, kind.and_then(|k| k.fixed), copy_range(sources.iter().copied())).described("a structural result"); + PlanRead { sources: &views, plan }.read_into(0..len, dtype, true, &mut out)?; + return Value::from_bounded(layout, out, desc.kind.fixed, desc.range).described("a structural result"); } let mut data = crate::array::Gather::with_widening(&sources, len, crate::array::Widening::Arrays); for i in 0..len { data.add(item(i)?); } @@ -489,14 +460,23 @@ pub(crate) fn copy(form: &Form, params: &[Value], inputs: &[&Value], fill: Optio /// `source` read through `plan`, a plan of that one source, laid out by `layout`: `source` itself where the plan reads it whole, and otherwise a copy through each of the plan's maps in turn (`copied`), inner map first. Windows over a padded copy so walk the padded copy, made at once, by plain strides. `None` for a plan that joins sources or reads indices. pub(crate) fn copied_through(source: &Value, plan: &rustymath::agreement::ReadPlan, layout: &Layout, fill: Option<&Value>) -> Option> { - let ReadRoute::Map { input, map, .. } = plan.route() else { return matches!(plan.route(), ReadRoute::Input(0)).then(|| source.relaid(layout.clone())) }; + copy_through(source, plan, layout, fill, true) +} + +fn copy_through(source: &Value, plan: &rustymath::agreement::ReadPlan, layout: &Layout, fill: Option<&Value>, defer: bool) -> Option> { + let ReadRoute::Map(read) = plan.route() else { return matches!(plan.route(), ReadRoute::Input(0)).then(|| source.relaid(layout.clone())) }; + let (input, map) = (&read.input, &read.map); Some(match input.route() { - ReadRoute::Input(0) => copied(source, layout.clone(), map, fill), - ReadRoute::Map { map: Mapping::Steps { steps, .. }, .. } => { + ReadRoute::Input(0) => copy_value(source, layout.clone(), map, fill, defer), + ReadRoute::Map(inner) => { + let Mapping::Steps { steps, .. } = &inner.map else { return None }; + if let Some((0, identity)) = plan.positional(layout.shape()) && identity.in_order(source.len(), layout.shape()) { + return Some(copy_value(source, layout.clone(), &identity, fill, defer)); + } let inner = Layout::from(steps.iter().map(Steps::len).collect::>()); - copied_through(source, input, &inner, fill)?.and_then(|padded| { + copy_through(source, input, &inner, fill, defer)?.and_then(|padded| { padded.force(); - copied(&padded, layout.clone(), map, fill) + copy_value(&padded, layout.clone(), map, fill, defer) }) } _ => return None, @@ -505,12 +485,22 @@ pub(crate) fn copied_through(source: &Value, plan: &rustymath::agreement::ReadPl /// Items of `right` laid out by `layout`, read through `map` as `Gather::walk` reads a walk's tables (`Mapping::to_walk`): `right` itself under `layout` where the map reads every item in order (`Mapping::in_order`), a carried array's copy (`Carrier::copied`), a pending copy where the copy defers, and otherwise a copy made at once (`walked`). A position that reaches no item reads `fill`, or the prototype where there's none. A copy with a fill is made at once. pub(crate) fn copied(right: &Value, layout: Layout, map: &Mapping, fill: Option<&Value>) -> Result { - let len = generated_len(layout.shape())?; + copy_value(right, layout, map, fill, true) +} + +/// Executes an existing copy without scheduling equivalent pending work while a program is running. +pub(crate) fn copied_now(right: &Value, layout: Layout, map: &Mapping, fill: Option<&Value>) -> Result { + copy_value(right, layout, map, fill, false) +} + +fn copy_value(right: &Value, layout: Layout, map: &Mapping, fill: Option<&Value>, defer: bool) -> Result { + // A zero-copy view allocates no items and needs no generation admission. if right.is_atom() && layout.shape().is_empty() { return Ok(right.clone()); } if map.in_order(right.len(), layout.shape()) { return right.relaid(layout); } + let len = generated_len(layout.shape())?; let fill = fill.filter(|_| map.fills()); if let Some(carrier) = right.carrier() { return carrier.copied(layout, map, fill); } - if fill.is_none() && defers(true, len) && let Some(copy) = pending(right, &layout, map) { return Ok(copy); } + if defer && fill.is_none() && defers(len) && let Some(copy) = pending(right, &layout, map) { return Ok(copy); } let (base, tables) = map.to_walk(layout.shape()); walked(right, layout, base, &tables, fill) } @@ -556,11 +546,12 @@ fn deferred_copy(source: &Value, layout: &Layout, mapping: Mapping) -> Option= MAX_NODES) { source.force(); } let frame = layout.shape(); let mut nodes = Nodes::default(); - let of = held(&mut nodes, source); - let desc = Desc { layout: frame.into(), ..nodes[of].desc.clone() }; - let program = ending(nodes, desc, Step::Copy { of, map: mapping }); + let of = nodes.held(source, Desc::held(source)); + let desc = crate::plan::copy(&of.desc(&nodes), layout).ok()?; + let result = nodes.copied(of, mapping, desc); + let program = Program::new(nodes, result); let (size, kept) = (1 + program.nodes.iter().filter_map(held_pending).map(|p| p.size).sum::(), kept_by(&program)); - let pending = Pending::new(Work::Program(program), Layout::from(frame.to_vec()), dtype, source.prototype(), size, kept); + let pending = Pending::new(program, Layout::from(frame.to_vec()), dtype, source.prototype(), size, kept); Some(Value::deferred(layout.clone(), Arc::new(pending))) } @@ -621,7 +612,7 @@ pub(crate) static FORCING: LazyLock = LazyLock::new(|| Span { source: Sour /// The value of `program`'s result, run by its plan's units (`program::planned`) after its maps move to the storage order that their operands and readers need (`program::layout::reorder`). A program whose only step gives its result, as a lone call forced at once makes, runs that step on its own with no plan (`run::lone`). Each output's value becomes the items of the pending array that `kept` gives for it, which no `forced` row counts. An error from a call inside the program stops the run. The pending array that holds `program` keeps its own nodes. On failure, each uncomputed output's owner recovers a program for that output, including only its dependencies. fn replayed(program: &Program, kept: &[Arc]) -> Result { - if let Some(value) = run::lone(program, &FORCING) { return value; } + if let Some(value) = run::lone(program, &[], &FORCING) { return value; } let moved = crate::program::layout::reorder(program); let program = moved.as_ref().unwrap_or(program); let mut values = match run::values(program, &crate::program::planned(program), &[], &FORCING) { @@ -650,7 +641,7 @@ pub(crate) fn fold(value: &Value, p: Primitive, kind: FoldKind, axes: &[usize], // The fold's items as its program describes them, under the value's own shape, and the fold's result. let folds = folded.and_then(|(pending, _)| { let items = Desc { layout: value.shape().into(), ..pending.desc() }; - let seed = seed.map(held_desc); + let seed = seed.map(Desc::held); let (result, _) = crate::plan::fold_desc(p, kind, Some(axis), seed.as_ref(), &items, Numeric::current()).ok()?; Some((items, result)) }); @@ -675,7 +666,7 @@ pub(crate) fn fold(value: &Value, p: Primitive, kind: FoldKind, axes: &[usize], }); nodes.truncate(len); pending.put(Program { nodes, result: root, outputs }); - if result.is_none() { rustymath::events::declined(rustymath::events::Route::FusedFold, "a copy that no call moves under", pending.len()); } + if result.is_none() { crate::events::declined(crate::events::routes::FusedFold, "a copy that no call moves under", pending.len()); } return result.map(|result| result.map(|v| (v, 1))); } let (Program { mut nodes, result: root, outputs }, kept) = pending.gathered(program); @@ -691,9 +682,10 @@ pub(crate) fn fold(value: &Value, p: Primitive, kind: FoldKind, axes: &[usize], result = Some(replayed(&program, &kept).map(|v| (v, axes.len()))); (nodes, stored) = (program.nodes, false); } else { - let seed = seed.map(|s| { let at = held(&mut nodes, s); Read::node(&nodes, at) }); - let at = nodes.push(Node { desc, origin: Origin::Step(Step::Fold { p, kind, axis, seed, of }) }); - let program = Program { outputs: outputs.iter().copied().chain(shared.then_some(root)).collect(), ..Program::at(nodes, at) }; + let seed = seed.map(|s| nodes.held(s, Desc::held(s))); + let result_read = nodes.folded(p, kind, axis, seed, of, desc); + let at = result_read.of; + let program = Program { outputs: outputs.iter().copied().chain(shared.then_some(root)).collect(), ..Program::new(nodes, result_read) }; let plan = crate::program::planned(&program); if plan.units.iter().any(|u| matches!(u, Unit::Blocks(b) if b.nodes.contains(&at))) { let kept: Vec<_> = kept.iter().cloned().chain(shared.then(|| pending.clone())).collect(); @@ -708,7 +700,7 @@ pub(crate) fn fold(value: &Value, p: Primitive, kind: FoldKind, axes: &[usize], Some(Ok(_)) => pending.put(Program::at(nodes, root)), Some(Err(_)) => pending.put(Program { outputs, ..Program::at(nodes, root) }), None => { - rustymath::events::declined(rustymath::events::Route::FusedFold, "a fold that its group can't run", pending.len()); + crate::events::declined(crate::events::routes::FusedFold, "a fold that its group can't run", pending.len()); let program = Program { outputs, ..Program::at(nodes, root) }; match replayed(&program, &kept) { Ok(value) => pending.result.set(pending.counted(value)).expect("a pending array computes its items once"), @@ -723,23 +715,23 @@ pub(crate) fn fold(value: &Value, p: Primitive, kind: FoldKind, axes: &[usize], fn staged(pending: &Pending, p: Primitive, kind: FoldKind, axis: usize, items: &Desc, result: &Desc) -> bool { let shape = items.shape(); let last = (0..shape.len()).rev().find(|&a| a != axis).map_or(0, |a| shape[a]); - let copy = |p: &Pending| p.work().as_ref().and_then(Work::copied).is_some(); + let copy = |p: &Pending| p.work().as_ref().and_then(copy_source).is_some(); let work = pending.work(); - let over_copy = work.as_ref().and_then(Work::copied).and_then(|(source, _)| source.pending()).is_some_and(|q| copy(q)); + let over_copy = work.as_ref().and_then(copy_source).and_then(|(source, _)| source.pending()).is_some_and(|q| copy(q)); over_copy && last > rustymath::reduce::DIRECT && crate::program::blocks::short_window(p, kind, shape[axis], items.dtype(), result) } impl Pending { /// A pending array whose `work` computes items of `dtype`, laid out as `layout`. - fn new(work: Work, layout: Layout, dtype: DType, prototype: Value, size: usize, kept: Vec<(usize, usize)>) -> Self { - let range = match &work { Work::Program(p) => p.nodes[p.result.of].desc.range, Work::Call(c) => c.desc.range }; + fn new(work: Program, layout: Layout, dtype: DType, prototype: Value, size: usize, kept: Vec<(usize, usize)>) -> Self { + let range = work.nodes[work.result.of].desc.range; Self { work: Mutex::new(Some(work)), result: OnceLock::new(), partial_read: AtomicBool::new(false), layout, dtype, prototype, range, size, kept, numeric: Numeric::current(), _live: Live::new() } } /// A read in order of a new pending array with this one's dtype, prototype, size and kept arrays, whose `work` computes the items of `frame`. fn with_work(&self, work: Program, frame: &[usize]) -> (Value, Mapping) { let layout = Layout::from(frame.to_vec()); - let mut pending = Self::new(Work::Program(work), layout.clone(), self.dtype, self.prototype.clone(), self.size, self.kept.clone()); + let mut pending = Self::new(work, layout.clone(), self.dtype, self.prototype.clone(), self.size, self.kept.clone()); pending.numeric = self.numeric; (Value::deferred(layout, Arc::new(pending)), Mapping::Linear(1)) } @@ -767,45 +759,36 @@ impl Pending { fn compute(&self) -> Result<(), Error> { let mut work = self.work(); if self.result.get().is_some() { return Ok(()); } - let value = if let Some(Work::Call(call)) = work.as_ref() { call.made()? } else { - let Some(taken) = work.take() else { return Ok(()) }; - let program = resolved(taken.into_program()); - match copy_source(&program) { - Some((source, map)) => { - let (base, tables) = map.to_walk(self.layout.shape()); - walked(source, self.layout.clone(), base, &tables, None).expect("a copy's items fit its layout") - } - None => { - let (program, kept) = self.gathered(program); - match replayed(&program, &kept) { Ok(value) => value, Err(error) => { *work = Some(Work::Program(program)); return Err(error); } } - } + let Some(taken) = work.take() else { return Ok(()) }; + let program = resolved(taken); + let value = match copy_source(&program) { + Some((source, map)) => { + let (base, tables) = map.to_walk(self.layout.shape()); + walked(source, self.layout.clone(), base, &tables, None).expect("a copy's items fit its layout") + } + None => { + let (program, kept) = self.gathered(program); + match replayed(&program, &kept) { Ok(value) => value, Err(error) => { *work = Some(program); return Err(error); } } } }; - *work = None; let _ = self.result.set(self.counted(value)); Ok(()) } /// The items, computed if they haven't been, with no copy. pub(crate) fn into_storage(mut self) -> Storage { self.storage(); std::mem::take(&mut self.result).into_inner().expect("the items are computed above") } /// What computes the items, under its lock, or `None` once something has taken it. - fn work(&self) -> MutexGuard<'_, Option> { self.work.lock().unwrap_or_else(PoisonError::into_inner) } - /// The work promoted to a program where a composed read needs one. - fn programmed(&self) -> MutexGuard<'_, Option> { - let mut work = self.work(); - if matches!(*work, Some(Work::Call(_))) { *work = work.take().map(|w| Work::Program(w.into_program())); } - work - } - fn take(&self) -> Program { self.work().take().expect("a pending array computes its items once").into_program() } - fn put(&self, work: Program) { *self.work() = Some(Work::Program(work)); } + fn work(&self) -> MutexGuard<'_, Option> { self.work.lock().unwrap_or_else(PoisonError::into_inner) } + fn take(&self) -> Program { self.work().take().expect("a pending array computes its items once") } + fn put(&self, work: Program) { *self.work() = Some(work); } fn len(&self) -> usize { self.layout.count() } /// Whether a program can compute this array's items as a step: items of a type that a block stage holds (`blocks::readable`). fn runs(&self) -> bool { readable(self.dtype) } /// Whether this array is Booleans still to be computed, which a program can compute as a step. fn compares(&self) -> bool { self.dtype == DType::Boolean && self.work().is_some() } /// Whether this array's program computes its items, as a pending copy's doesn't. - fn computes(&self) -> bool { self.work().as_ref().is_some_and(|w| w.copied().is_none()) } + fn computes(&self) -> bool { self.work().as_ref().is_some_and(|w| copy_source(w).is_none()) } /// The glyphs of the program that computes the items. - fn glyphs(&self) -> String { self.work().as_ref().map(Work::glyphs).unwrap_or_default() } + fn glyphs(&self) -> String { self.work().as_ref().map(Program::glyphs).unwrap_or_default() } /// The description of this array as a value of a program. fn desc(&self) -> Desc { described(self.dtype, &self.prototype, self.layout.shape(), self.range) } @@ -885,10 +868,8 @@ impl Footprint for Pending { /// The bytes that this pending array keeps alive: its items once computed, and until then the values that its program holds. fn footprint_in(&self, seen: &mut Seen) -> usize { if let Some(storage) = self.result.get() { return storage.footprint_in(seen); } - match &*self.work() { - Some(Work::Call(call)) => call.held().map(|v| v.footprint_in(seen)).sum(), - Some(Work::Program(program)) => program.nodes.iter().map(|n| match &n.origin { Origin::Held(v) => v.footprint_in(seen), _ => 0 }).sum(), None => 0 - } + self.work().as_ref().map_or(0, |program| + program.nodes.iter().map(|n| match &n.origin { Origin::Held(v) => v.footprint_in(seen), _ => 0 }).sum()) } } @@ -896,7 +877,7 @@ impl Drop for Pending { /// Drops the pending arrays that the program holds through `drop_chain`, so that a long chain of them can't overflow the stack. A program that a fold left holds only arrays that have been computed. fn drop(&mut self) { crate::array::drop_chain(self, |pending, chain| { - let Some(Work::Program(program)) = pending.work.get_mut().unwrap_or_else(PoisonError::into_inner).take() else { return }; + let Some(program) = pending.work.get_mut().unwrap_or_else(PoisonError::into_inner).take() else { return }; chain.extend(program.nodes.into_iter().filter_map(|node| match node.origin { Origin::Held(value) => value.into_pending(), _ => None })); }); } diff --git a/src/lib.rs b/src/lib.rs index fbab72b5..3eb11e21 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,7 @@ mod distribution; mod element; mod error; mod eval; +mod events; mod execution; mod explain; #[cfg(not(web))] @@ -51,6 +52,7 @@ mod xml; /// What `basedpl-testkit`'s reference checks and speed measurements take from the interpreter's internals. Not part of BPL's API. #[doc(hidden)] pub mod hooks { + pub use crate::events::event_routes; pub use crate::explain::{Events, explained}; pub use crate::number::Numeric; pub use crate::program::{blocks::compiled_runs, forget_plans}; diff --git a/src/number.rs b/src/number.rs index beb59366..09d31e75 100644 --- a/src/number.rs +++ b/src/number.rs @@ -562,13 +562,13 @@ impl Number { let right = crate::plan::Kind::number(right); match left { Some(left) => crate::plan::Kind::fixed([crate::plan::Kind::number(left), right]), None => crate::plan::Kind::fixed([right]) } } - /// This number as a fixed integer of `width`, converted as `element::Converts` converts it, with a float floored within the tolerance that `numeric` gives its width. A complex number is DOMAIN. + /// This number as a fixed integer of `width`: integers wrap, floats floor within their width's tolerance and saturate, and exact fractions floor and saturate unless whole. A complex number is DOMAIN. pub(crate) fn to_fixed(&self, width: Width, numeric: Numeric) -> Result { - use {crate::element::Converts, rustymath::tolerant::Exactly}; let n = rustymath::with_width!(width, T => i64::from(match (self.as_integer(), self.0.float(), &self.0) { - (Some(n), ..) => >::converted(n, Exactly), - (_, Some((x, w)), _) => >::converted(x, numeric.tolerance_at(w)), - (_, _, Exact(q, _)) => <&BigRational as Converts>::converted(q, Exactly), + (Some(n), ..) => n as T, + (_, Some((x, w)), _) => rustymath::convert::floored::(x, numeric.tolerance_at(w)), + (_, _, Exact(q, _)) if q.is_integer() => low_bits(q.numer()) as T, + (_, _, Exact(q, _)) => rustymath::convert::saturated(&q.floor().to_integer()), _ => return Err(Kind::Domain.into()), })); Ok(Self::fixed(n, width)) diff --git a/src/pervasive.rs b/src/pervasive.rs index eb68c26a..dcf7ef38 100644 --- a/src/pervasive.rs +++ b/src/pervasive.rs @@ -141,16 +141,16 @@ pub(crate) fn integral(p: Primitive, dyadic: bool, kind: DType, numeric: Numeric /// The whole floats `floats` as integers, saturating as Rust's `as` does (`Buffer::cast_results`): NaN gives 0, and an infinity or a value past the width gives the width's nearest limit, as `⌊2*70` gives the largest i64. Where integers wrap, the width is the `int` width. Otherwise it's the width that stores the floats' range (`ordinary_width`), from their least and greatest, so storage keeps the buffer as it is; i64 where those come out NaN. fn whole(floats: View<'_>, numeric: Numeric) -> Buffer { - use rustymath::{convert::saturated, reduce::{max, min}}; + use rustymath::reduce::{max, min}; let range = match floats { View::Floats(f) if !numeric.wrapping() => with_floats!(f, |xs| { let (lo, hi) = (min(xs), max(xs)); - (!lo.is_nan() && !hi.is_nan()).then(|| (saturated::(lo), saturated::(hi))) + (!lo.is_nan() && !hi.is_nan()).then(|| rustymath::domain::Range { lo: lo.into(), hi: hi.into() }) }), _ => None, }; let width = - if numeric.wrapping() { numeric.integer_width() } else { range.map_or(Width::I64, |(lo, hi)| crate::array::ordinary_width(Width::narrowest(lo, hi))) }; + if numeric.wrapping() { numeric.integer_width() } else { crate::array::ordinary_width_for(range) }; Buffer::cast_results(floats, DType::Integer(width), false).expect("floats convert to integers") } @@ -222,14 +222,18 @@ fn computed( } /// Reads of `args` as rustymath kernels take them, with each value's kind: each value through its mapping onto `len` items, with `missing` for the positions a mapping reaches no item at. `None` where a value isn't compact. -pub(crate) fn with_reads(args: &[(&Value, &Mapping, usize)], missing: Missing, f: impl FnOnce(&[Read<'_>], &[DType]) -> R) -> Option { +pub(crate) fn with_reads(args: [(&Value, &Mapping, usize); N], missing: Missing, f: impl FnOnce(&[Read<'_>], &[DType]) -> R) -> Option { use rustymath::agreement::{Binding, ReadPlan}; - let views: Vec> = args.iter().map(|(value, ..)| value.view()).collect::>()?; - let types: Vec = views.iter().map(|v| v.dtype()).collect(); - let sources: Vec<_> = views.iter().map(|&view| Binding::full(view)).collect(); - let plans: Vec<_> = args.iter().zip(&views).enumerate() - .map(|(source, ((_, mapping, len), view))| ReadPlan::copy(source, view.len(), (*mapping).clone(), *len, missing.clone())).collect(); - let reads: Vec<_> = plans.iter().map(|plan| Read { sources: &sources, plan }).collect(); + let mut bindings = args.iter().map_while(|(value, ..)| value.view().map(Binding::full)).fuse(); + let sources: [_; N] = std::array::from_fn(|_| bindings.next()); + if sources.iter().any(Option::is_none) { return None; } + let sources = sources.map(Option::unwrap); + let types = sources.map(|binding| binding.view.dtype()); + let plans: [_; N] = std::array::from_fn(|source| { + let (_, mapping, len) = args[source]; + ReadPlan::copy(source, sources[source].items, mapping.clone(), len, missing.clone()) + }); + let reads = plans.each_ref().map(|plan| Read { sources: &sources, plan }); Some(f(&reads, &types)) } @@ -241,20 +245,18 @@ fn applied(kernel: &Kernel, reads: &[Read<'_>], len: usize, settings: rustymath: /// Floor a logical float input directly to its narrowest integer width, chosen before writing from the floors of its two extrema. fn floored(read: &Read<'_>, len: usize, at: DType, numeric: Numeric) -> Result { - use rustymath::{convert::saturated, tolerant::Tolerant}; + use rustymath::tolerant::Tolerant; let mut scratch = Buffer::with_capacity(at, 0); let view = read.part_view(0..len, at, !numeric.is_static(), &mut scratch)?; let View::Floats(floats) = view else { unreachable!("floor input has its float computing dtype") }; rustymath::with_tolerance!(numeric.tolerance_of(at), |t| { - let width = with_floats!(floats, |xs| { + let range = with_floats!(floats, |xs| { let (lo, hi) = (reduce::min(xs), reduce::max(xs)); - if lo.is_nan() || hi.is_nan() || xs.is_empty() { - Width::I64 - } else { - crate::array::ordinary_width(Width::narrowest(saturated::(t.floor(lo)), saturated::(t.floor(hi)))) - } + (!lo.is_nan() && !hi.is_nan() && !xs.is_empty()) + .then(|| rustymath::domain::Range { lo: t.floor(lo).into(), hi: t.floor(hi).into() }) }); - rustymath::events::taken(rustymath::events::Route::VectorKernel, len); + let width = crate::array::ordinary_width_for(range); + crate::events::taken(crate::events::Route::VectorKernel, len); rustymath::operation::report_maps([read.plan], 0..len); Buffer::floored(view, DType::Integer(width), t) }) @@ -263,21 +265,23 @@ fn floored(read: &Read<'_>, len: usize, at: DType, numeric: Numeric) -> Result, right: &Value, agreement: &Agreement) -> Option> { let (p, numeric, len) = (plan.p, plan.numeric, agreement.len); - let code = match left { Some(left) if p.info().dyad.code => Some(code_of(left)?), _ => None }; + let code = match left { Some(_) if p.info().dyad.code => Some(plan.constants[0].and_then(circle_code)?), _ => None }; // A comparison's constant that stands for an integer (`Pervasive::narrowed`) reads as that integer, stored at its narrowest width, so its kernel resolves at the plan's integer kind. let narrow = plan.narrowed().and_then(|(side, n)| Some((side, Value::integers(Vec::::new(), vec![n]).ok()?))); let (left, right) = match &narrow { Some((0, n)) => (Some(n), right), Some((_, n)) => (left, n), None => (left, right) }; - let args: Vec<(&Value, &Mapping, usize)> = match left.filter(|_| code.is_none()) { - Some(left) => vec![(left, &agreement.left, len), (right, &agreement.right, len)], None => vec![(right, &agreement.right, len)] - }; let at = plan.computes()?; let settings = numeric.settings_for(at); - let (out, dtype) = match with_reads(&args, Missing::Default, |reads, types| { + let run = |reads: &[Read<'_>], types: &[DType]| { let flooring = !numeric.wrapping() && integral(p, plan.dyadic, at, numeric) && matches!(formula(p, types.len(), numeric.mathish_at(at), false, numeric.tolerance_of(at)), Some(Op::Floor(_))); if flooring { floored(&reads[0], len, at, numeric).map(|out| Some((out, at))) } else { computed(p, code, types, at, numeric, |kernel| applied(kernel, reads, len, settings)) } - })? { Ok(Some(result)) => result, Ok(None) => return None, Err(error) => return Some(Err(error.into())) }; + }; + let result = match left.filter(|_| code.is_none()) { + Some(left) => with_reads([(left, &agreement.left, len), (right, &agreement.right, len)], Missing::Default, run), + None => with_reads([(right, &agreement.right, len)], Missing::Default, run) + }?; + let (out, dtype) = match result { Ok(Some(result)) => result, Ok(None) => return None, Err(error) => return Some(Err(error.into())) }; let out = whole_results(p, plan.dyadic, dtype, out, numeric); let known = [left, Some(right)].into_iter().flatten().all(|v| v.is_atom() || v.range() != rustymath::domain::Range::ANY); Some( @@ -298,7 +302,7 @@ pub(crate) fn character_item(p: Primitive, x: &Value, y: &Value) -> Option, right: &Value) -> [Option; 2] { [left.and_then(Value::as_real), right.as_real()] } /// The argument that `plan`'s call on `left` and `right` gives unchanged (`Pervasive::kept`), where the result has its shape: the other argument is an atom, or the kept one isn't. A weak number beside a strong one isn't kept: the call's result is strong, as `∞⌈1ₓ₁` gives a strong `∞`, an ordinary float. -pub(crate) fn unchanged<'a>(plan: &crate::plan::Pervasive, left: Option<&'a Value>, right: &'a Value) -> Option<&'a Value> { - let (kept, other) = if plan.kept()? == 0 { (left?, Some(right)) } else { (right, left) }; - let strengthens = matches!((kept, other), (Value::Number(n), Some(Value::Number(o))) if n.is_weak() && !o.is_weak()); +pub(crate) fn unchanged<'a>(plan: &crate::plan::Pervasive, left: Option<&'a Value>, right: &'a Value, agreement: &Agreement) -> Option<&'a Value> { + let (kept, other, map) = if plan.kept()? == 0 { (left?, Some(right), &agreement.left) } else { (right, left, &agreement.right) }; + // Returning the source also returns its layout and item order, not the call's broadcast or permutation. + if kept.layout() != &agreement.layout || !map.in_order(kept.len(), agreement.layout.shape()) { return None; } + let strengthens = Kind::value(kept).weak && other.is_some_and(|o| !Kind::value(o).weak); (!strengthens && other.is_none_or(|o| o.is_atom() || !kept.is_atom())).then_some(kept) } @@ -519,7 +525,7 @@ pub(crate) fn unchanged<'a>(plan: &crate::plan::Pervasive, left: Option<&'a Valu pub(crate) fn expected(p: Primitive, left: Option<&Value>, right: &Value) -> Option { let numeric = Numeric::current(); if numeric.is_static() { return None; } - plan::pervasive_on(p, left, right, numeric).kernel().map(|k| k.result_dtype()) + plan::pervasive_on(p, left, right, numeric, None).kernel().map(|k| k.result_dtype()) } /// `p` on the number `y`, and on `x` where it's dyadic: the one path for functions on numbers, through `p`'s formula as `map` computes arrays. Machine numbers compute at the kind `map` chooses, with its reruns (`computed`). Exact numbers, rationals and unbounded integers, compute through the formula's exact form. Where the formula has no exact form or gives no exact result, as for an irrational root, they compute at the widest float argument's width, as a machine integer beside it does, or at `numeric`'s float width beside no float. diff --git a/src/pervasive/fold.rs b/src/pervasive/fold.rs index 473c7caa..1b44fef3 100644 --- a/src/pervasive/fold.rs +++ b/src/pervasive/fold.rs @@ -18,14 +18,17 @@ pub(crate) fn fold(p: Primitive, right: &Value, axis: usize, seed: Option<&Value let len = shape.remove(axis); let mapping = copy.as_ref().map_or(Mapping::Linear(1), |(_, mapping)| mapping.clone()); let fill = mapping.fills().then(|| crate::array::Layout::from([1]).filled(&right.prototype())); - let args: Vec<_> = std::iter::once((items, &mapping, right.len())).chain(fill.as_ref().map(|f| (f, &Mapping::Single, 1))).collect(); - let missing = if fill.is_some() { Missing::Scalar(1) } else { Missing::Default }; - let ((out, order), _) = with_reads(&args, missing, |reads, types| { + let run = |reads: &[Read<'_>], types: &[DType]| { computed(p, None, &[types[0]; 2], dtype, numeric, |kernel| { let mut out = Buffer::with_capacity(kernel.fold_dtype(types[0], len, None), 0); kernel.fold_read(right.shape(), Some(axis), Seed::Identity, Direction::Right, settings, reads[0], &mut out).map(|order| (out, order)) }) - })?.ok().flatten()?; + }; + let reduced = match fill.as_ref() { + Some(fill) => with_reads([(items, &mapping, right.len()), (fill, &Mapping::Single, 1)], Missing::Scalar(1), run), + None => with_reads([(items, &mapping, right.len())], Missing::Default, run) + }?; + let ((out, order), _) = reduced.ok().flatten()?; // A copy lays out folds left in their source's order, pending where it's large, so that a later fold of windows can slide over them. let laid = if order.is_some() { vec![out.len()] } else { shape.clone() }; let result = @@ -35,7 +38,7 @@ pub(crate) fn fold(p: Primitive, right: &Value, axis: usize, seed: Option<&Value }; let Some(seed) = seed else { return Some(Ok(result)) }; let agreement = Agreement::new(result.layout(), seed.layout()).ok()?; - map(&plan::pervasive_on(p, Some(&result), seed, numeric), Some(&result), seed, &agreement) + map(&plan::pervasive_on(p, Some(&result), seed, numeric, Some(&agreement)), Some(&result), seed, &agreement) } /// The fold by `p` of each group of the vector `right` that `groups` gives, by `p`'s reduction (`Reduce::run_groups`) at the kind the fold computes in. Each group's fold combines its items in their order, or in four interleaved parts among few groups (`Grouping::fold`), where the reduction may combine them in another order. `None` for a function with no reduction, for storage it doesn't fold, and where a checked total doesn't fit. diff --git a/src/pervasive/product.rs b/src/pervasive/product.rs index 41643140..1aadfe82 100644 --- a/src/pervasive/product.rs +++ b/src/pervasive/product.rs @@ -8,10 +8,10 @@ pub(crate) fn inner( use rustymath::error::{Error, Kind}; let (g, folds) = (&plan.g, plan.folds()); let operands = [x, y].map(read); - let args: Vec<(&Value, &Mapping, usize)> = operands.iter().zip([x, y]).map(|((v, mapping), o)| (v, mapping, o.len())).collect(); + let args = [(&operands[0], x), (&operands[1], y)].map(|((v, mapping), o)| (v, mapping, o.len())); let at = g.computes()?; let settings = g.numeric.settings_for(at); - let ran = with_reads(&args, Missing::Default, |reads, types| { + let ran = with_reads(args, Missing::Default, |reads, types| { computed(g.p, None, types, at, g.numeric, |map| { let t = map.result_dtype(); let fold = table_kernel(plan.f, true, t, t, [None; 2], folds).ok_or_else(|| Error::new(Kind::Domain, "a product whose fold has no formula"))?; diff --git a/src/pervasive/scan.rs b/src/pervasive/scan.rs index c375944e..cad96445 100644 --- a/src/pervasive/scan.rs +++ b/src/pervasive/scan.rs @@ -4,7 +4,7 @@ use rustymath::{agreement::{Binding, Missing, ReadPlan}, operation::Settings, re /// The scan by `p` of stored values along `axis`, left to right, through `p`'s formula (`Kernel::scan`) at the kind that a scan by `p` computes in, with `map`'s reruns at wider kinds (`computed`). A seed of rank 0 starts every lane. A comparison's or residue's seed that stands for one integer beside the items (`Pervasive::narrowed`, `Pervasive::whole_float`) scans as that integer at its narrowest width: each step then computes at the items' integer kind. Where the scan with the seed as written gives floats (`Fold::result`), the results take that float kind. `None` where the formula fails or the seed's rank isn't 0: the general scan then gives the result or error. pub(crate) fn scan(p: Primitive, right: &Value, seed: Option<&Value>, axis: &Lanes) -> Option { - let whole = seed.map(|s| plan::pervasive_on(p, Some(s), right, Numeric::current())).and_then(|step| step.narrowed().or_else(|| step.whole_float())); + let whole = seed.map(|s| plan::pervasive_on(p, Some(s), right, Numeric::current(), None)).and_then(|step| step.narrowed().or_else(|| step.whole_float())); let Some((0, n)) = whole else { return scanned(p, right, seed, axis) }; let value = scanned(p, right, Some(&Value::integers(Vec::::new(), vec![n]).ok()?), axis)?; match scan_plan(p, right, seed).result() { Some(DType::Float(width)) => value.to_float_width(width).ok(), _ => Some(value) } diff --git a/src/plan.rs b/src/plan.rs index 6eca51c1..40979ad1 100644 --- a/src/plan.rs +++ b/src/plan.rs @@ -9,13 +9,15 @@ pub(crate) struct Kind { pub(crate) dtype: Option, pub(crate) fixed: Option, pub(crate) weak: bool, - weak_integer: bool, /// The sign of a bound, the bare `∞` or `¯∞` (`Number::order_class`): 1 or ¯1, and 0 for every other kind. bound: i8, - /// The narrowest width that holds an integer's value, which `fixed` checks a weak integer against. An ordinary integer atom's `dtype` takes the `int` width instead under static kinds (`Kind::number`). - narrowest: Option, + /// The actual minimum width needed for weak integer promotion, not a computed result's provisional dtype. + minimum: Minimum, } +#[derive(Clone, Copy, Debug, Default, PartialEq)] +enum Minimum { #[default] Unneeded, Known(Width), Unknown } + /// The fixed width that parts give, built one part at a time, for `Kind::fixed` and `Kind::join`. `fixed` joins the parts' declared fixed widths, `required` the narrowest widths of their weak machine integers, and `ordinary` the widths that static kinds give their strong ordinary integers (`array::static_width`), which join a declared width but never make one. `other` marks a part that is none of these: a weak integer in the unbounded domain (`ᵣ`) has no narrowest width, so it counts as other. #[derive(Default)] struct Fixing { fixed: Option, required: Option, ordinary: Option, other: bool } @@ -24,9 +26,10 @@ impl Fixing { fn add(&mut self, part: Kind) { let join = |into: &mut Option, w: Width| *into = Some(into.map_or(w, |v| v.join(w))); if self.other { return; } + let part = part.for_promotion(); if let Some(w) = part.fixed { join(&mut self.fixed, w); } else if part.dtype == Some(DType::Boolean) { } - else if let Some(w) = part.narrowest.filter(|_| part.weak_integer) { + else if let Minimum::Known(w) = part.minimum { join(&mut self.required, w); } else if let Some(DType::Integer(w)) = part.dtype && let Some(w) = crate::array::static_width(w) { join(&mut self.ordinary, w); } else { self.other = true; } @@ -40,7 +43,7 @@ impl Kind { pub(crate) fn number(n: &Number) -> Self { let fixed = n.fixed_width(); let bound = n.order_class().filter(|&c| c != 0).map_or(0, |c| c as i8); - let mut kind = Self { weak_integer: n.is_weak() && n.is_integral(), bound, ..Self::stored(n.dtype(), fixed, n.is_weak()) }; + let mut kind = Self { bound, ..Self::stored(n.dtype(), fixed, n.is_weak()) }; if fixed.is_none() && let Some(DType::Integer(w)) = kind.dtype && let Some(s) = crate::array::static_width(w) { kind.dtype = Some(DType::Integer(s)); } kind } @@ -56,9 +59,12 @@ impl Kind { } #[inline] pub(crate) fn stored(dtype: Option, fixed: Option, weak: bool) -> Self { - let narrowest = match dtype { Some(DType::Integer(w)) => Some(w), _ => None }; - Self { dtype, fixed, weak, weak_integer: weak && dtype.is_some_and(DType::is_integral), narrowest, bound: 0 } + let minimum = match (dtype, fixed, weak) { (Some(DType::Integer(w)), None, true) => Minimum::Known(w), _ => Minimum::Unneeded }; + Self { dtype, fixed, weak, minimum, bound: 0 } } + + /// Kind facts usable by a promotion that needs a weak integer's actual minimum width. + fn for_promotion(self) -> Self { if self.minimum == Minimum::Unknown { Self::default() } else { self } } /// The fixed width that `parts` give a call or a join: the widest of their fixed widths, where every other part is a Boolean or a weak machine integer. `None` where no part is fixed, or a part is something else, such as a number in the unbounded domain (`ᵣ`), which mixing with machine integers keeps. A weak integer that doesn't fit widens the width to the narrowest that holds it (weak-literal minimum-width promotion), as `1ₓ₁+256ₓ` computes at 16 bits, and a comparison such as `v<300ₓ` of bytes compares there. pub(crate) fn fixed(parts: impl IntoIterator) -> Option { let mut fixing = Fixing::default(); @@ -67,7 +73,7 @@ impl Kind { } /// `parts` joined into one kind under `widening`, as collected items join into one array's dtype. A fixed width that `fixed` finds wins. Where two parts' types don't join, the kind is unknown (`Kind::default()`). Each part is read once. pub(crate) fn join(parts: impl IntoIterator, widening: Widening) -> Self { - let mut parts = parts.into_iter(); + let mut parts = parts.into_iter().map(Self::for_promotion); let Some(first) = parts.next() else { return Self::default() }; // A lone part is its own join: its fixed width, if it has one, is the only width `Fixing` could find. let Some(second) = parts.next() else { return first }; @@ -101,7 +107,7 @@ impl Kind { } /// The width of the floats that operands of `kinds` compute in under `numeric`, for a function whose results are approximate, such as `⌹`: machine integers and Booleans compute as floats of the widest width that their division gives (`Numeric::exact_width`). `None` where an operand isn't machine integers or Booleans. Numbers in the unbounded domain (`ᵣ`) then stay exact. pub(crate) fn approximate(kinds: impl IntoIterator, numeric: Numeric) -> Option { - let kinds = kinds.into_iter(); + let kinds = kinds.into_iter().map(Self::for_promotion); let t = numeric.for_kinds(kinds.clone()); let mut widest = None; for kind in kinds { @@ -111,7 +117,8 @@ impl Kind { widest } /// The kind that a weak operand of this kind takes beside an operand of kind `other`, as `primitive::weakened_beside` converts a weak number: the width that NEP 50 gives it beside strong floats, or the width of fixed integers beside it where a weak integer fits them (`Kind::fixed`). A strong kind, or one beside another weak kind, stays as it is. So does a weak integer that doesn't fit, which promotion widens the fixed width to hold. - pub(crate) fn beside(self, other: Kind) -> Self { + pub(crate) fn beside(mut self, other: Kind) -> Self { + if other.fixed.is_some() { self = self.for_promotion(); } if !self.weak || other.weak { return self; } if let Some(w) = other.fixed && matches!(self.dtype, Some(DType::Integer(_))) { return if Self::fixed([self, other]) == Some(w) { Self::stored(Some(DType::Integer(w)), Some(w), false) } else { self }; @@ -123,7 +130,7 @@ impl Kind { } } -/// What a program knows of a value without its items: its kind (`plan::Kind`), whether it's an atom, its layout (its shape, keys and axis names), and a bound on its items from its type and the steps that made it (`rustymath::facts`), coarsened (`coarse`). A description's kind leaves out the narrowest width that holds a number's value. Two atoms of one type therefore share a program's key, whatever their magnitudes. An array's bound includes its recorded range (`Value::range`), so arrays share a key where their items have one sign and power-of-two magnitude. +/// What a program knows of a value without its items: its kind, atom/array category, layout and coarsened item range. Strong atoms use their logical kind; weak integers retain the minimum-width class needed for fixed promotion, but no exact-value key. Arrays retain their recorded range, so they share a key where their items have one sign and power-of-two magnitude. #[derive(Clone, Debug, PartialEq)] pub(crate) struct Desc { pub(crate) kind: Kind, pub(crate) atom: bool, pub(crate) layout: Layout, pub(crate) range: Range } @@ -133,6 +140,10 @@ impl Desc { let kind = Self::kind_of(value)?; Some(Self { kind, atom: value.is_atom(), layout: value.layout().clone(), range: kind.dtype.map_or(Range::ANY, Range::of_dtype).meet(value.range()) }) } + + pub(crate) fn held(value: &Value) -> Self { + Self::of(value).unwrap_or_else(|| Self { kind: Kind::value(value), atom: value.is_atom(), layout: value.layout().clone(), range: Range::ANY }) + } /// What a plan reads of `value`, a held value that this describes: a plain atom itself, whose number can pick a kernel as a constant, and otherwise this description, whose facts stay fixed while the value's items are computed or carry a level. The trace plans each step with it (`trace::Arg::operand`), and verification plans again with it (`verify::planned`). pub(crate) fn operand<'a>(&'a self, value: &'a Value) -> Operand<'a> { if value.is_atom() && !value.carried() { Operand::Held(value) } else { Operand::Traced(self) } @@ -144,13 +155,15 @@ impl Desc { if value.carrier().is_some() || value.renderer().is_some() { return None; } let kind = Kind::value(value); let dtype = kind.dtype.filter(|&d| d != DType::Character)?; - Some(Kind::stored(Some(dtype), kind.fixed, kind.weak)) + let mut described = Kind::stored(Some(dtype), kind.fixed, kind.weak); + if kind.weak { described.minimum = kind.minimum; described.bound = kind.bound; } + Some(described) } - /// The type of the described items. Only a pending copy's program holds characters, and no step reads them (`fusion::held`). + /// The stored item type. Pending structural copies may hold characters; traced numeric operations use `describes`. pub(crate) fn dtype(&self) -> DType { self.kind.dtype.expect("a description has a type") } /// The shape of the described value. pub(crate) fn shape(&self) -> &[usize] { self.layout.shape() } - /// Whether `value` matches this description's kind in every way that later steps' plans read: its type, its fixed width, and whether it's an atom. A weak flag doesn't count, because an array is never weak and an atom's flag changes no kind that a plan gave. Nor does the width of an ordinary integer atom, a number with no storage width (`Number::dtype` gives the narrowest that holds it): it matches any planned width that holds it, as a count by `+` of one item does. An array's dtype matches exactly, because a program stores each step's items at its planned dtype (`run::node`, `Value::stored_at`). Nor does a running level's data on `value`, whose kind its template gives, as where a tape's coefficients carry an outer `∂`'s duals. Nor does the layout: a recording replays at new lengths, each step computing its own layout. + /// Runtime result conformance: type, fixed width and atom/array category. Ordinary integer atoms may use any narrower physical width that fits the plan (`Number::dtype`), as a one-item count does. Weakness does not change the kind already chosen for this result; input capture guards compare canonical kinds separately (`Entry::reread`). Array dtypes match exactly (`Value::stored_at`). Carriers expose their logical/template kinds, including tape coefficients carrying outer duals. Layouts are rebound separately. pub(crate) fn matches(&self, value: &Value) -> bool { let kind = Kind::value(value); let dtype = match (kind.dtype, self.kind.dtype) { @@ -165,7 +178,7 @@ impl Desc { DType::Boolean => Number::from_bool(false), DType::Integer(w) if self.kind.fixed.is_some() => Number::fixed(0, w), DType::Integer(_) => Number::from_integer(0), DType::Float(w) => Number::float(0.0, w), DType::Complex(w) => Number::complex(num_complex::Complex64::new(0.0, 0.0), w.into()), - DType::Character => unreachable!("a program holds no characters") + DType::Character => return Value::Character(' '), }; Value::Number(n.marked(self.kind.weak)) } @@ -271,6 +284,10 @@ impl Pervasive { if crate::pervasive::integral(self.p, self.dyadic, self.computes()?, self.numeric) { return Some(integers); } Some(match self.kernel()?.result_dtype() { DType::Integer(_) if self.numeric.wrapping() => integers, k => k }) } + /// The settled result under its layout, atom and weak-number flags. + pub(crate) fn result_desc(&self, layout: Layout, atom: bool, weak: bool) -> Result { + Ok(Desc { kind: computed(self.settled()?, self.numeric, weak), atom, layout, range: self.result_range() }) + } /// The operand that the call gives unchanged, as its kinds and constants show it, 0 for the left and 1 for the right: the operand that an order-only call picks by its order class (`picking`), monadic `+` on a real operand, `⌊` and `⌈` on integers, and a dyadic call whose constant leaves the other operand unchanged, as its kernel's formula says (`Kernel::kept`): `x+0`, `0+x`, `x-0`, `x×1`, `1×x`, `x÷1` and `x*1`. The result must have the operand's kind, so a float constant beside integers, which widens them, keeps nothing. Integers that don't wrap keep their kind through `+`, `⌊` and `⌈` whatever `result` says. A Boolean's floor is an integer. The exact `⌊` and `⌈` remove fixedness, as plain `vₓ` does. `x+0` gives ¯0 where `x` is ¯0, where the sum is 0: Jeremy accepted that difference on 30 October 2026. A constant of ¯0 counts as 0, so `x-¯0` keeps `x` too, with the same difference where `x` is ¯0. `pervasive::unchanged` adds the condition on shapes. pub(crate) fn kept(&self) -> Option { if let Some(k) = self.picking() { return k.selected_input(); } @@ -318,6 +335,11 @@ pub(crate) fn pervasive(p: Primitive, left: Option, right: Kind, constants /// The settings and operand kinds that a call of `p` computes with: `numeric` with the fixed width that the operands give (`Numeric::for_kinds`). A function that takes no fixed width (`takes_fixed`) keeps `numeric`. Where 16-bit floats compute in 32 bits, the settings widen, the kinds read 32-bit floats, and the flag says whether float results round back to 16 bits. fn prepared(p: Primitive, left: Option, right: Kind, numeric: Numeric) -> (Numeric, Halving, Option, Kind) { + let (left, right) = match left { Some(left) => (Some(left.beside(right)), right.beside(left)), None => (None, right) }; + // A reduction drops its seed's kind; keep an unresolved promotion on both sides. + if left.is_some_and(|k| k.dtype.is_none()) || right.dtype.is_none() { + return (numeric, Halving::Off, left.map(|_| Kind::default()), Kind::default()); + } let numeric = if takes_fixed(p) { numeric.for_kinds(left.into_iter().chain([right])) } else { numeric }; let (numeric, halve) = numeric.halving(left.into_iter().chain([right]).map(Kind::float_width)); let read = |k: Kind| Kind { dtype: k.dtype.map(|d| halve.operand(d)), ..k }; @@ -340,12 +362,15 @@ pub(crate) fn whole_float(p: Primitive, c: f64, width: FloatWidth, ints: DType, (p != Primitive::Math(crate::number::Math::Magnitude) || 2.0 * most.max(n.unsigned_abs() as f64) < limit).then_some(n) } -/// `pervasive`'s plan for `p` on the values `left` and `right`: their kinds, with the ones that are one real number as constants, their recorded ranges (`Value::range`), and whether they're one value. -pub(crate) fn pervasive_on(p: Primitive, left: Option<&Value>, right: &Value, numeric: Numeric) -> Pervasive { - let plan = pervasive( - p, left.map(Kind::value), Kind::value(right), crate::pervasive::constants(left, right), [left.map_or(Range::ANY, Value::range), right.range()], numeric - ); - Pervasive { same: left.is_some_and(|l| std::ptr::eq(l, right)), ..plan } +/// One source is pointwise identical on both sides only when the paired reads agree. +fn same_at(same: bool, agreed: Option<&Agreement>) -> bool { + same && agreed.is_none_or(|a| a.left == a.right) +} + +/// `pervasive` on the values' kinds, constants and ranges, under the supplied pairing or ordinary agreement. +pub(crate) fn pervasive_on(p: Primitive, left: Option<&Value>, right: &Value, numeric: Numeric, agreed: Option<&Agreement>) -> Pervasive { + let same = same_at(left.is_some_and(|l| std::ptr::eq(l, right)), agreed); + operand_plan(p, left.map(Operand::Held).as_ref(), &Operand::Held(right), same, numeric) } /// An operand of a planned call: a traced value, known only by its description, or a value that the program holds. @@ -367,40 +392,67 @@ impl Operand<'_> { } /// A planned pervasive call: its result's description, and its plan. -pub(crate) struct Mapped { pub(crate) desc: Desc, pub(crate) plan: Pervasive } +pub(crate) struct Mapped { pub(crate) desc: Desc, pub(crate) plan: Pervasive, pub(crate) agreement: Agreement } -/// The plan of pervasive primitive `p` on `left` and `right` under `numeric`, where `same` says whether both are one value (`Pervasive::same`), with its result's description, or the reason that a program can't hold the call. -pub(crate) fn map(p: Primitive, left: Option<&Operand>, right: &Operand, same: bool, numeric: Numeric) -> Result { - if !p.scalar(left.is_some()) { return Err("a call that isn't pervasive"); } - // A traced weak number beside integers of a fixed width (`Numeric::fixes`) converts to that width only if its value fits, which planning can't see. - let fixed = |o: &Operand| match o { - Operand::Traced(d) => numeric.fixes(d.kind.dtype, d.kind.fixed), Operand::Held(v) => numeric.fixes(v.dtype(), v.fixed_width()) - }; - if left.is_some_and(|l| matches!(l, Operand::Traced(d) if d.kind.weak) && fixed(right) || matches!(right, Operand::Traced(d) if d.kind.weak) && fixed(l)) { - return Err("a weak number argument beside fixed integers"); +impl Mapped { + pub(crate) fn new(plan: Pervasive, agreement: Agreement, atom: bool, weak: bool) -> Result { + let desc = plan.result_desc(agreement.layout.clone(), atom, weak)?; + Ok(Self { desc, plan, agreement }) } - let (x, y) = (left.map(Operand::kind), right.kind()); - // A weak operand beside a strong one converts as `primitive::weakened` converts a weak number (`Kind::beside`). - let (x, y) = match x { Some(x) if x.weak != y.weak => (Some(x.beside(y)), y.beside(x)), _ => (x, y) }; +} + +/// The operand facts of a pervasive call, before settling its result. +fn operand_plan(p: Primitive, left: Option<&Operand>, right: &Operand, same: bool, numeric: Numeric) -> Pervasive { let bounds = [left.map_or(Range::ANY, Operand::range), right.range()]; - let plan = Pervasive { same, ..pervasive(p, x, y, [left.and_then(Operand::constant), right.constant()], bounds, numeric) }; + Pervasive { same, ..pervasive(p, left.map(Operand::kind), right.kind(), [left.and_then(Operand::constant), right.constant()], bounds, numeric) } +} + + +fn map_plan(p: Primitive, left: Option<&Operand>, right: &Operand, same: bool, numeric: Numeric) -> Result { + if !p.scalar(left.is_some()) { return Err("a call that isn't pervasive"); } + let plan = operand_plan(p, left, right, same, numeric); // 16-bit floats compute at 32 bits and round back (`pervasive::map`), which programs don't model yet. if plan.halve != Halving::Off { return Err("a 16-bit float"); } - let dtype = plan.settled()?; - let agreement = Agreement::new(&left.map_or_else(Layout::default, Operand::layout), &right.layout()).map_err(|_| "shapes that don't agree")?; + Ok(plan) +} + +/// A pervasive call's output, with geometry from its operands and numeric facts from its plan. +fn mapped(plan: Pervasive, left: Option<&Operand>, right: &Operand, agreed: Option<&Agreement>) -> Result { + let agreement = match agreed { + Some(agreement) => agreement.clone(), + None => Agreement::new(&left.map_or_else(Layout::default, Operand::layout), &right.layout()).map_err(|_| "shapes that don't agree")?, + }; let atom = right.atom() && left.is_none_or(Operand::atom); let weak = atom && right.weak() && left.is_none_or(Operand::weak); - Ok(Mapped { desc: Desc { kind: computed(dtype, plan.numeric, weak), atom, layout: agreement.layout, range: plan.result_range() }, plan }) + Mapped::new(plan, agreement, atom, weak) +} + +/// Plan the original operands under either their ordinary agreement or an already resolved pairing. +pub(crate) fn map(p: Primitive, left: Option<&Operand>, right: &Operand, same: bool, numeric: Numeric, agreed: Option<&Agreement>) -> Result { + let plan = map_plan(p, left, right, same_at(same, agreed), numeric)?; + mapped(plan, left, right, agreed) +} + +/// Checks a declared plan against its operands before settling its result. Constants compare by their bits, so that a NaN constant matches itself, only where a held operand still exposes them. Normalization can hide an observed scalar constant, as in `(⊂3)+4`; the declared plan owns that observation and its derived computing kind (`at`). +pub(crate) fn check_map(plan: &Pervasive, left: Option<&Operand>, right: &Operand) -> Result { + let inferred = map_plan(plan.p, left, right, plan.same, plan.numeric)?; + let held = [left.is_some_and(|o| matches!(o, Operand::Held(_))), matches!(right, Operand::Held(_))]; + let constants = |p: &Pervasive| [0, 1].map(|k| p.constants[k].filter(|_| held[k] && plan.constants[k].is_some()).map(f64::to_bits)); + let without_constants = |p: &Pervasive| Pervasive { constants: [None; 2], at: None, ..p.clone() }; + if without_constants(&inferred) != without_constants(plan) || constants(&inferred) != constants(plan) { + return Err("a plan other than the one that its operands give"); + } + mapped(plan.clone(), left, right, None).map(|mapped| mapped.desc) } /// The description of what `p` gives where it combines items that `items` describes into the base that `base` describes, as `Pairing::At` pairs them (`pervasive::combine_at`): the base's layout, and `p`'s result on one item of each joined with the base's kind as `Widening::Items` joins items. An error for an atom base, where `p` doesn't plan on the two, or where the join would leave the base mixed, which programs can't hold. pub(crate) fn at(p: Primitive, base: &Desc, items: &Desc, numeric: Numeric) -> Result { if base.atom { return Err("an accumulation into an atom"); } let item = |d: &Desc| Desc { atom: true, layout: Layout::default(), ..d.clone() }; - let combined = map(p, Some(&Operand::Traced(&item(base))), &Operand::Traced(&item(items)), false, numeric)?.desc.kind; + let combined = map(p, Some(&Operand::Traced(&item(base))), &Operand::Traced(&item(items)), false, numeric, None)?.desc.kind; let kind = Kind::join([base.kind, combined], Widening::Items); - if kind.dtype.is_none() { return Err("a write that leaves its target mixed"); } - Ok(Desc { kind, atom: false, layout: base.layout.clone(), range: Range::ANY }) + let Some(dtype) = kind.dtype else { return Err("a write that leaves its target mixed"); }; + Ok(Desc { kind, atom: false, layout: base.layout.clone(), range: Range::of_dtype(dtype) }) } /// The plan of a fold or scan by `p`: `kind`, the settings that the items and any seed give together (`prepared`), and the kinds of each step's operands, the running result's first, as those settings compute them. A scan's first step takes the seed, so a scan computes in the kind that the seed's and the items' kinds join to. A fold applies its seed after it folds the items. Where 16-bit floats compute in 32 bits, `halve` says whether float results round back to 16 bits. @@ -408,7 +460,7 @@ pub(crate) struct Fold { pub(crate) kind: FoldKind, p: Primitive, pub(crate) num /// The plan of a fold or scan by `p` of items of kind `items`, with a seed of kind `seed`, under the session's settings `numeric`. A weak seed converts beside the items as `primitive::weakened_beside` converts one (`Kind::beside`). pub(crate) fn fold(p: Primitive, kind: FoldKind, seed: Option, items: Kind, numeric: Numeric) -> Fold { - let (numeric, halve, seed, items) = prepared(p, seed.map(|s| s.beside(items)), items, numeric); + let (numeric, halve, seed, items) = prepared(p, seed, items, numeric); let left = if kind.scan { seed.unwrap_or(items) } else { items }; Fold { kind, p, numeric, halve, kinds: [left.dtype, items.dtype] } } @@ -471,7 +523,10 @@ pub(crate) fn takes_fixed(p: Primitive) -> bool { !matches!(p, Primitive::Exact( /// The kind of a result of `dtype` from a call under `numeric`, weak if `weak` is. An integer result is fixed where `numeric` has a fixed width. fn computed(dtype: DType, numeric: Numeric, weak: bool) -> Kind { - Kind::stored(Some(dtype), numeric.fixed.filter(|_| matches!(dtype, DType::Integer(_))), weak) + let mut kind = Kind::stored(Some(dtype), numeric.fixed.filter(|_| matches!(dtype, DType::Integer(_))), weak); + // An integer Number can narrow after execution; its result dtype does not prove its minimum width. + if matches!(kind.minimum, Minimum::Known(_)) { kind.minimum = Minimum::Unknown; } + kind } /// The plan of one step of a fold by `p` under `numeric`, on a running result of kind `left` and an item of kind `items`. @@ -519,7 +574,7 @@ impl Product { let g = self.g.settled()?; let folds = self.folds(); let result = self.g.halve.result(folded(self.f, g, g, folds)?); - Ok(Desc { kind: computed(result, folds, false), atom: layout.shape().is_empty(), layout, range: Range::ANY }) + Ok(Desc { kind: computed(result, folds, false), atom: layout.shape().is_empty(), layout, range: Range::of_dtype(result) }) } /// The description of `f.g⍤2` on the stacks of matrices that `left` and `right` describe: the frame that pairing their frames gives, as Rank pairs them (`Frames`), around the product of one matrix of each. Stacks with names or keys don't plan. pub(crate) fn stacked_desc(&self, left: &Desc, right: &Desc) -> Result { @@ -537,6 +592,26 @@ pub(crate) fn copy(source: &Desc, layout: &Layout) -> Result Ok(Desc { kind: Kind::stored(source.kind.dtype, source.kind.fixed, false), atom: false, layout: layout.clone(), range: source.range }) } + +pub(crate) fn structural(form: &crate::primitive::structural::Form, params: &[Value], inputs: &[Desc], fill: Option<&Desc>) + -> Result<(Desc, rustymath::agreement::ReadPlan), crate::Error> { + let layouts: Vec<&Layout> = inputs.iter().map(|d| &d.layout).collect(); + let (layout, plan) = crate::primitive::structural::plan(form, params, &layouts)?; + let first = &inputs[0]; + let parts = || inputs.iter().chain(fill); + let kind = crate::array::items_kind(parts().map(|d| (d.layout.count(), d.kind)), crate::array::Widening::Arrays).unwrap_or(first.kind); + let atom = form.keeps_atoms() && layout.shape().is_empty() && parts().all(|d| d.atom); + // Provenance cannot tighten a result whose every possible source/fill has unconstrained bounds. + let range = if parts().all(|d| d.range == Range::ANY) { Range::ANY } else { + match plan.positional(layout.shape()) { + Some((source, _)) if fill.is_none() => inputs[source].range, + _ => parts().fold(Range::of(Some(0.0)), |range, d| range.join(d.range)), + } + }; + let kind = Kind::stored(kind.dtype, kind.fixed, false); + Ok((Desc { kind, atom, layout, range }, plan)) +} + /// The description of a copy of one item of a value that `source` describes, as `Value::at` reads one: an atom of the source's type and fixedness. pub(crate) fn item(source: &Desc) -> Result { Ok(Desc { atom: true, ..copy(source, &Layout::default())? }) } @@ -544,7 +619,7 @@ pub(crate) fn item(source: &Desc) -> Result { Ok(Desc { atom pub(crate) fn picks(of: &Desc, axis: usize) -> Result { if axis >= of.shape().len() { return Err("an axis that the items lack"); } if !matches!(of.kind.dtype, Some(DType::Boolean | DType::Integer(_) | DType::Float(_))) { return Err("picks from items that aren't real numbers"); } - Ok(Desc { kind: Kind::stored(Some(DType::Boolean), None, false), ..copy(of, &of.layout)? }) + Ok(Desc { kind: Kind::stored(Some(DType::Boolean), None, false), range: Range::of_dtype(DType::Boolean), ..copy(of, &of.layout)? }) } /// A planned masked select: its result's description, and the map through which each case reads from the result's positions. The mask reads them in order. A weak atom case takes the result's type, which its executor converts it to. @@ -562,7 +637,7 @@ pub(crate) fn select(mask: &Operand, yes: &Operand, no: &Operand) -> Result Result { (DType::Complex(_), _) => return Err("complex numbers that convert to integers"), (_, DType::Float(_)) => (to, None), (_, DType::Integer(width)) => (to, Some(width)), _ => return Err("a conversion to a type that no mark gives") }; - Ok(Desc { kind: Kind::stored(Some(dtype), fixed, false), range: Range::ANY, ..source.clone() }) + Ok(Desc { kind: Kind::stored(Some(dtype), fixed, false), range: Range::of_dtype(dtype), ..source.clone() }) } /// The description of a fold step of `p` along `axis` over `items`, with `seed` where it has one, and the axis it folds along (`fold`, then `Fold::desc`): the one plan that recording, fusion and verification give a `Step::Fold`. diff --git a/src/primitive.rs b/src/primitive.rs index 0fc2dfc6..9990a58d 100644 --- a/src/primitive.rs +++ b/src/primitive.rs @@ -714,11 +714,11 @@ impl Primitive { } /// The function applied to the items that `agreement` pairs, laid out as `agreement` lays out the result, as the call's plan plans it (`plan::Pervasive`). fn pervasive_mapped(self, left: Option<&Value>, right: &Value, span: &Span, fill: bool, agreement: Agreement) -> Result { - let plan = crate::plan::pervasive_on(self, left, right, Numeric::current()); - if let Some(kept) = crate::pervasive::unchanged(&plan, left, right) { return Ok(kept.clone()); } + let plan = crate::plan::pervasive_on(self, left, right, Numeric::current(), Some(&agreement)); + if let Some(kept) = crate::pervasive::unchanged(&plan, left, right, &agreement) { return Ok(kept.clone()); } // A large fusible call on floats becomes a pending array, which `fusion` computes with the calls that read it. - let agreement = if agreement.len != 0 && !fill && crate::fusion::defers(plan.fuses(), agreement.len) { - match crate::fusion::defer(self, &plan, left, right, agreement) { Ok(pending) => return Ok(pending), Err(agreement) => agreement } + let agreement = if agreement.len != 0 && !fill && crate::fusion::defers(agreement.len) { + match crate::fusion::defer(&plan, left, right, agreement) { Ok(pending) => return Ok(pending), Err(agreement) => agreement } } else { agreement }; Self::planned(&plan, left, right, span, fill, agreement) } @@ -819,7 +819,7 @@ fn int_count(n: usize) -> u64 { /// The depth of `right`: 0 for an atom, and for an array one more than its deepest element. An empty array counts its prototype. A carried array answers from its carrier's template, without reading its items. pub(crate) fn depth(right: &Value) -> usize { if right.is_atom() { return 0; } - if let Some(carrier) = right.carrier() { return depth(carrier.template()); } + if let Some(carrier) = right.carrier() { return depth(carrier.template()).max(1); } 1 + match right.as_items() { Items::Values([]) => depth(&right.prototype()), Items::Values(items) => items.iter().map(depth).max().unwrap_or(0), _ => 0 } } @@ -863,7 +863,7 @@ pub(crate) fn weakened(left: Option<&Value>, right: &Value, f: impl FnOnce(Op /// `f` on each pair of items that `agreement` pairs from `left` and `right`, gathered as items and laid out as `agreement` lays out the result. fn pairwise(left: Option<&Value>, right: &Value, agreement: Agreement, f: &dyn Fn(Option<&Value>, &Value) -> Result) -> Result { - crate::explain::itemwise(rustymath::events::Route::ItemPairs, agreement.len); + crate::explain::itemwise(crate::events::routes::ItemPairs, agreement.len); let mut data = Gather::items(agreement.len); for i in 0..agreement.len { let (x, y) = agreement.values_ref(left, right, i); diff --git a/src/primitive/numeric.rs b/src/primitive/numeric.rs index 4c63b95b..9dab2108 100644 --- a/src/primitive/numeric.rs +++ b/src/primitive/numeric.rs @@ -38,7 +38,7 @@ pub(super) fn complex_parts(right: &Value, polar: bool) -> Result _ => None, }; if let Some(parts) = parts { return Ok(parts); } - crate::explain::itemwise(rustymath::events::Route::NumberParts, right.len()); + crate::explain::itemwise(crate::events::routes::NumberParts, right.len()); let mut data = Vec::with_capacity(generated_len(layout.shape())?); for item in right.elements() { check()?; diff --git a/src/program.rs b/src/program.rs index 5b530513..c7ae7997 100644 --- a/src/program.rs +++ b/src/program.rs @@ -8,7 +8,7 @@ pub(crate) mod trace; pub(crate) mod verify; use crate::{Value, array::Layout, number::Numeric, plan::Desc, primitive::{FoldKind, Operation, Primitive, structural::Form}}; -use rustymath::{agreement::Mapping, gather::Steps, hashing::{HashMap, Hashing}, layout::strides, structure::IndexPart}; +use rustymath::{agreement::{Mapping, ReadPlan}, gather::Steps, hashing::{HashMap, Hashing}, layout::strides, structure::IndexPart}; use std::hash::{BuildHasher, Hash, Hasher}; use std::sync::{Arc, Mutex, PoisonError}; @@ -63,7 +63,7 @@ fn held_identity(v: &Value) -> Option<*const ()> { pub(crate) struct Nodes { list: Vec, index: HashMap> } impl Nodes { - /// Adds `node`, or finds the newest node that gives the same value (`Node::matches`), and gives its number. The trace and fusion's pending programs both add their nodes through it, so that a program computes each value once. + /// Interns an already-described node, returning the newest equivalent node (`Node::matches`) or its new number. This runs no operation rewrites, so contraction/layout can emit replacement steps without restarting `folded`. pub(crate) fn add(&mut self, node: Node) -> usize { let key = node.key(self.index.hasher()); let found = key.and_then(|k| self.index.get(&k)?.iter().rev().copied().find(|&j| self.list[j].matches(&node))); @@ -88,6 +88,89 @@ impl Nodes { self.list.push(node); at } + + pub(crate) fn held(&mut self, value: &Value, desc: Desc) -> Read { + let layout = desc.layout.clone(); + // Pending identities belong to demand/retention; resolution composes their reads when sharing permits it. + let one = (!value.carried() && value.len() > 1 && value.pending().is_none() + && value.view().is_some_and(rustymath::buffer::Buffer::same)).then(|| value.at(0)); + if let Some(item) = one && let Some(d) = Desc::of(&item) { + // Every item has the array's proved bounds and kind; extraction may narrow an ordinary integer atom. + let of = self.held(&item, Desc { kind: desc.kind, range: desc.range, ..d }); + return self.copied(of, Mapping::Single, desc); + } + let plain = Layout::from(value.shape().to_vec()); + let held = match value.pending() { + Some(p) => Value::deferred(plain.clone(), p.clone()), + None if value.layout().plain() => value.clone(), + None => value.relaid(plain.clone()).expect("a held value keeps its item count"), + }; + let of = self.add(Node { desc: Desc { layout: plain, ..desc }, origin: Origin::Held(held) }); + Read::new(of, layout) + } + pub(crate) fn stepped(&mut self, desc: Desc, step: Step) -> Read { + let layout = desc.layout.clone(); + let of = self.add(Node { desc, origin: Origin::Step(step) }); + Read::new(of, layout) + } + pub(crate) fn copied(&mut self, of: Read, map: Mapping, desc: Desc) -> Read { + // Preserve compact cycles/repeats; geometry consumers expand them through Mapping only where needed. + let step = copy_of_copy(self, of.of, &map, desc.shape()).unwrap_or_else(|| Step::Copy { of: of.of, map }); + self.stepped(desc, step) + } + fn mapped_read(&mut self, of: Read, map: Mapping, frame: &Layout) -> Read { + let desc = of.desc(self); + if desc.atom { return of; } + if map.in_order(desc.layout.count(), frame.shape()) && positional_source(self, of.of).is_none() { return Read::new(of.of, frame.clone()); } + self.copied(of, map, Desc { layout: frame.clone(), ..desc }) + } + + /// An explicit array copy, including where one atom supplies all the requested items. + pub(crate) fn copied_value(&mut self, value: &Value, map: Mapping, frame: &[usize]) -> Read { + let of = self.held(value, Desc::held(value)); + let layout = Layout::from(frame.to_vec()); + if of.desc(self).atom { + let desc = crate::plan::copy(&of.desc(self), &layout).expect("a generated copy has no renderer"); + return self.copied(of, map, desc); + } + self.mapped_read(of, map, &layout) + } + pub(crate) fn mapped(&mut self, mapped: crate::plan::Mapped, left: Option, right: Read) -> Read { + let crate::plan::Mapped { desc, plan, agreement } = mapped; + let frame: Layout = desc.shape().into(); + let left = left.map(|of| self.mapped_read(of, agreement.left, &frame)); + let right = self.mapped_read(right, agreement.right, &frame); + let layout = desc.layout.clone(); + let read = self.stepped(Desc { layout: frame, ..desc }, Step::Map { left, right, plan }); + Read::new(read.of, layout) + } + pub(crate) fn selected(&mut self, plan: &crate::plan::Select, mask: Read, yes: Read, no: Read) -> Read { + let frame: Layout = plan.desc.shape().into(); + let mask = self.mapped_read(mask, Mapping::Linear(1), &frame); + let yes = self.mapped_read(yes, plan.yes.clone(), &frame); + let no = self.mapped_read(no, plan.no.clone(), &frame); + let read = self.stepped(Desc { layout: frame, ..plan.desc.clone() }, Step::Select { mask, yes, no }); + Read::new(read.of, plan.desc.layout.clone()) + } + pub(crate) fn converted(&mut self, of: Read, to: rustymath::buffer::DType, numeric: Numeric) -> Result { + let desc = crate::plan::convert(&of.desc(self), to)?; + Ok(self.stepped(desc, Step::Convert { of, numeric })) + } + pub(crate) fn folded(&mut self, p: Primitive, kind: FoldKind, axis: usize, seed: Option, of: Read, desc: Desc) -> Read { + if seed.is_none() && let Some(read) = contraction::routed(self, p, kind, &of, &[axis], &desc, Numeric::current()) { return read; } + self.stepped(desc, Step::Fold { p, kind, axis, seed, of }) + } + pub(crate) fn structured(&mut self, form: Form, params: Vec, inputs: Vec, fill: Option, desc: Desc, plan: ReadPlan) -> Read { + if fill.is_none() && let Some((source, map)) = plan.positional(desc.shape()) + && let Ok(copied) = crate::plan::copy(&inputs[source].desc(self), &desc.layout) && copied.kind == desc.kind { + let input = inputs[source].desc(self); + if input.kind == desc.kind && input.atom == desc.atom && map.in_order(input.layout.count(), desc.shape()) { + return Read::new(inputs[source].of, desc.layout); + } + return self.copied(inputs[source].clone(), map, desc); + } + self.stepped(desc, Step::Structural { inputs, form, params, fill, plan }) + } } impl std::ops::Deref for Nodes { @@ -124,18 +207,11 @@ fn added(own: Nodes, nodes: &mut Nodes, mut replaced: impl FnMut(&mut Nodes, &[u at } -/// The positional source of a copy or kind-preserving structural step. Structural parameters must already be stored under their recorded layouts; otherwise returns `None` without forcing or observation. -pub(crate) fn positional_source<'a>(nodes: &[Node], node: usize, value: impl Fn(usize) -> Option<&'a Value>) -> Option<(Read, Mapping)> { +/// The source and resolved positional mapping of a kind-preserving structural read, without item access. +pub(crate) fn positional_source(nodes: &[Node], node: usize) -> Option<(Read, Mapping)> { match &nodes[node].origin { Origin::Step(Step::Copy { of, map }) => Some((Read::node(nodes, *of), map.clone())), - Origin::Step(Step::Structural { inputs, form, params, .. }) => { - let params = params.iter().map(|r| { - let v = value(r.of)?; - if v.pending().is_some() || v.carried() || v.layout() != &r.layout { return None; } - Some(v.clone()) - }).collect::>>()?; - let layouts: Vec<&Layout> = inputs.iter().map(|r| &r.layout).collect(); - let plan = crate::primitive::structural::plan(form, ¶ms, &layouts).ok()?.1; + Origin::Step(Step::Structural { inputs, plan, .. }) => { let (source, map) = plan.positional(nodes[node].desc.shape())?; let source = inputs.get(source)?; if nodes[source.of].desc.kind != nodes[node].desc.kind { return None; } @@ -145,9 +221,9 @@ pub(crate) fn positional_source<'a>(nodes: &[Node], node: usize, value: impl Fn( } } -/// One copy of node `node`'s positional source, composing its map with `map` for a result of `shape`. Unresolved parameters and maps that don't compose give `None`. +/// One copy of node `node`'s positional source, composing its map with `map` for a result of `shape`. pub(crate) fn copy_of_copy(nodes: &[Node], node: usize, map: &Mapping, shape: &[usize]) -> Option { - let (source, inner) = positional_source(nodes, node, |j| match &nodes[j].origin { Origin::Held(v) => Some(v), _ => None })?; + let (source, inner) = positional_source(nodes, node)?; Some(Step::Copy { of: source.of, map: inner.compose(map, nodes[node].desc.shape(), shape)? }) } @@ -159,7 +235,7 @@ pub(crate) fn through_copies(nodes: &[Node], of: usize, map: &Mapping, frame: &[ } /// The axis of an array of `shape` that each axis of `frame` steps forwards along by that axis's stride, where `map` reads the array over `frame` from its first item, or `None` for an axis that repeats one item. `None` where `map` starts at another item, where an axis steps any other way or past the end of its source axis (`Walk::new`), and where two axes step along one axis of the array. -pub(crate) fn stepped(map: &Mapping, frame: &[usize], shape: &[usize]) -> Option>> { +pub(crate) fn forward_axes(map: &Mapping, frame: &[usize], shape: &[usize]) -> Option>> { let walk = rustymath::structure::Walk::new(&map.tables(frame), frame, shape)?; let forward = |(s, b): (&Steps, &Option)| match (s, *b) { (Steps::Stride { start: 0, .. }, None) => true, @@ -204,8 +280,8 @@ pub(crate) enum Step { Convert { of: Read, numeric: Numeric }, /// A value's items read through `map`, as a structural glyph copies them (`fusion::copied`). Only a fused chain holds one. Copy { of: usize, map: Mapping }, - /// A structural primitive's call on `inputs`, with parameters `params` and an explicit `fill`, which plans its read on the inputs' layouts when it runs (`structural::plan`) and reads through `fusion::copy`. A recording holds the structural call rather than its resolved copy. - Structural { inputs: Vec, form: Form, params: Vec, fill: Option }, + /// A structural call and its resolved source-relative geometry. Its recipe remains available for transformed execution and deliberate rebinding. + Structural { inputs: Vec, form: Form, params: Vec, fill: Option, plan: ReadPlan }, /// The items of `of` that `by`'s parts select, as `.[]` or `⌷` reads them, planned at replay on the parts' values (`primitive::parts_plan`), where a part's positions are a value that the program computes, such as an argument's. Gather { of: Read, by: Selector }, /// A fold or scan by `p` along `axis`, with an atom seed (`plan::fold`). @@ -218,7 +294,7 @@ pub(crate) enum Step { At { p: Primitive, base: Read, items: Read, writes: Writes }, /// A mask of Booleans in the layout of `of`, true at the item of each lane along `axis` that `p`, `⌈` or `⌊`, picks, the first of any ties (`derivative::extreme_mask`, `plan::picks`). Picks { p: Primitive, axis: usize, of: Read }, - /// Monadic `p`, `≢` or `⍴`, of `of`, read from its value's layout when the step runs, so that a replay at new lengths gives the new count or shape (`trace::build` needn't key the lengths exactly). + /// Monadic `p`, `≢` or `⍴`, of `of`, read from its current layout when the step runs. Exact-layout guards also protect any shape facts used during construction. Shape { p: Primitive, of: Read }, } @@ -254,6 +330,8 @@ impl Read { pub(crate) fn desc(&self, nodes: &[Node]) -> Desc { Desc { layout: self.layout.clone(), ..nodes[self.of].desc.clone() } } /// Whether this read is a view: a layout other than its node's own, which a replay relays its node's value to. pub(crate) fn view(&self, nodes: &[Node]) -> bool { self.layout != nodes[self.of].desc.layout } + /// A normalized pointwise operand reads an atom repeatedly and an array in frame order. + pub(crate) fn pointwise(&self, nodes: &[Node]) -> Mapping { if nodes[self.of].desc.atom { Mapping::Single } else { Mapping::Linear(1) } } } /// The reads of `$step`'s operands, with optional ones taken through `$as`, `as_ref` or `as_mut`, and a selector's and a structural call's reads through `$iter`, `iter` or `iter_mut`. @@ -327,20 +405,21 @@ impl Program { pub(crate) fn at(nodes: Nodes, result: usize) -> Self { let result = Read::node(&nodes, result); Self::new(nodes, result) } /// Whether the caller takes node `j`'s value: the node that the result reads, or one of the outputs. pub(crate) fn takes(&self, j: usize) -> bool { j == self.result.of || self.outputs.contains(&j) } - /// The program as text, one line for each value: its number, what makes it, and its type and shape. `input` names input `k`. + /// The program as text, one line for each value: its number, what makes it, and its type and shape. `input` names input `k`. Only plain atoms print their literal; listing arrays or carriers never reads their items. pub(crate) fn listing(&self, input: impl Fn(usize) -> String) -> String { let at = |i: usize| format!("v{i}"); let read = |r: &Read| at(r.of); self.nodes.iter().enumerate().map(|(i, node)| { let made = match &node.origin { Origin::Input(k) => input(*k), - Origin::Held(v) => format!("held {}", v.source(crate::display::Elide::NONE)), + Origin::Held(v) if v.is_atom() && !v.carried() => format!("held {}", v.source(crate::display::Elide::NONE)), + Origin::Held(_) => "held".into(), Origin::Step(step @ Step::Map { left, right, .. }) => { format!("{} {}", step.glyph(), left.iter().chain([right]).map(read).collect::>().join(" ")) } Origin::Step(Step::Convert { of, .. }) => format!("convert {}", read(of)), Origin::Step(Step::Copy { of, .. }) => format!("copy {}", at(*of)), - Origin::Step(Step::Structural { inputs, form, params, fill }) => format!( + Origin::Step(Step::Structural { inputs, form, params, fill, .. }) => format!( "{form:?} {}{}{}", inputs.iter().map(read).collect::>().join(" "), params.iter().map(|p| " ".to_owned() + &read(p)).collect::(), fill.as_ref().map(|f| " fill ".to_owned() + &read(f)).unwrap_or_default() @@ -478,7 +557,7 @@ impl Recent { impl Default for Recent { fn default() -> Self { Self(Vec::new()) } } /// The plans that `planned` made, each with the settings it was made under and its program with inputs in place of held arrays (`Program::keyed`). -static PLANS: Mutex), PLANNED>> = Mutex::new(Recent(Vec::new())); +static PLANS: Mutex), PLANNED>> = Mutex::new(Recent(Vec::new())); /// The most plans that `PLANS` keeps. const PLANNED: usize = 32; @@ -486,17 +565,17 @@ const PLANNED: usize = 32; /// Forgets every kept plan, as a new process has none. pub fn forget_plans() { PLANS.lock().unwrap_or_else(PoisonError::into_inner).0.clear() } -/// The plan that runs `program` (`blocks::plan`), made once for each structure of program under each of the settings that planning reads (`Numeric`). The plan comes from the program with inputs in place of its held arrays, whose items a block stage reads as it runs. Programs that differ only in their arrays' items share a plan, as each force of one pending chain does. Programs whose held atoms differ get plans of their own, because a plan can depend on an atom's value, as `○`'s does on its left argument. A program with a table of positions (`Step::tabled`), which can hold as many positions as the data has items, is planned each time and never kept. Where the session checks programs (`settings::verifies`), each program that's planned is checked first as it is, because a step planned on a held array's values plans differently on the input that replaces it. +/// The plan that runs `program` (`blocks::plan`), made once for each structure of program under each of the settings that planning reads (`Numeric` and `Sizes`). The plan comes from the program with inputs in place of its held arrays, whose items a block stage reads as it runs. Programs that differ only in their arrays' items share a plan, as each force of one pending chain does. Programs whose held atoms differ get plans of their own, because a plan can depend on an atom's value, as `○`'s does on its left argument. A program with a table of positions (`Step::tabled`), which can hold as many positions as the data has items, is planned each time and never kept. Where the session checks programs (`settings::verifies`), each program that's planned is checked first as it is, because a step planned on a held array's values plans differently on the input that replaces it. pub(crate) fn planned(program: &Program) -> Arc { let tabled = program.nodes.iter().any(|n| matches!(&n.origin, Origin::Step(s) if s.tabled())); - let numeric = Numeric::current(); + let (numeric, sizes) = (Numeric::current(), crate::settings::sizes()); let plans = || PLANS.lock().unwrap_or_else(PoisonError::into_inner); - if !tabled && let Some((_, _, plan)) = plans().find(|(n, p, _)| *n == numeric && p.plans_as(program)) { return plan.clone(); } + if !tabled && let Some((_, _, _, plan)) = plans().find(|(n, s, p, _)| *n == numeric && *s == sizes && p.plans_as(program)) { return plan.clone(); } if crate::settings::verifies() { verify::check(program); } if tabled { return Arc::new(blocks::plan(program)); } // Planning runs without the lock, which a force on another thread may need meanwhile. let keyed = program.keyed(); let plan = Arc::new(blocks::plan(&keyed)); - plans().keep((numeric, keyed, plan.clone())); + plans().keep((numeric, sizes, keyed, plan.clone())); plan } diff --git a/src/program/blocks.rs b/src/program/blocks.rs index 0178c024..934c15b2 100644 --- a/src/program/blocks.rs +++ b/src/program/blocks.rs @@ -1,39 +1,24 @@ -//! Block stages: a run of a program's elementwise steps on values of one item count, and a fold of the last of them that ends the run, computed a block of items at a time. Pending work and dfn replays run this way. `plan` finds the runs. +//! Stage execution: one native operation runs whole on prepared inputs; real elementwise chains and their terminal fold run cache-sized blocks. Pending work and function replays share these stages; `plan` groups them. //! -//! Each step computes at one type, which it reads its operands as: floats of 32 or 64 bits, Booleans, or integers, which wrap. It writes a run's items into a buffer of that type, or Booleans for a comparison that the stage stores or folds or that a step at another type reads. A comparison at a float width that only steps at float widths read writes 1 and 0 as floats, which those steps read as they read floats. A map step's kernel is its formula's, resolved at its type (`Pervasive::kernel`), which it runs through `Kernel::map`. A buffer holds a later step's items once every step that reads it has run, and the last step writes straight into its value, or into the block that the fold reads. A step reads an operand of another type by converting a run of its items (`Converts`). +//! RM's reads bind each operand's geometry and actual storage type; its kernels read at their computing type and write final slots through the shared output writer. A comparison writes Booleans when stored or folded, and may write 1 and 0 at a later float step's type when only such steps read it. Explicit language conversions use RM's floored/tolerant conversion, distinct from input preparation. //! //! A stage's steps fall into parts (`Part`): runs of steps whose formulas' recipes record as machine operations giving their kernels' results exactly (`recorded`), each at least two steps long or the whole stage, and the steps around them. A compiled part's kernel computes its steps for a run in one call, in registers. It reads the run's inputs where the driver places them, the stage's atoms as arguments and earlier parts' buffers, and writes each value that leaves the part: to its buffer, its output, the value's slots or the block that the fold reads. The steps around compiled parts run on their precompiled kernels, and so does every step where the stage doesn't compile. A stage compiles all its parts or none. The browser build compiles nothing. //! -//! The stage reads each array made outside it through the map that agreement gives it, or through the map of a copy or view of it, without making the copy. A block splits into runs at each repeat of at least the sizes' `run` items, and each run reads one repeat in place, except that a compiled part reads each repeat's item from a table, so that its runs needn't end there (`Reading::Tabled`). A cycle shorter than a block reads from its period, written out once. A map that walks entries for each cycle, as a broadcast of a transposed read does, reads one cycle, which the stage gathers once each time it runs. A map whose reads copy most blocks (`Mapping::copies`) copies each run into a buffer. Ranges of whole blocks split across threads. A stage with a fold folds each block's items while they're in the cache, in the pieces that its kernel's fold takes (`Kernel::fold_pieces`). +//! The stage reads each array made outside it through its saved mapping without making the copy. Compiled runs end at repeats of at least the sizes' `run` items, unless a compiled part reads each repeat's item from a table (`Reading::Tabled`). Native kernels split their own reads. A cycle shorter than a block reads from its period, written out once. A map that walks entries for each cycle, as a broadcast of a transposed read does, reads one cycle, which the stage gathers once each time it runs. A map whose reads copy most blocks (`Mapping::copies`) copies each run into a buffer. Ranges of whole blocks split across threads. A stage with a fold folds each block's items while they're in the cache, in the pieces that its kernel's fold takes (`Kernel::fold_pieces`). use super::{Origin, Program, Read, Step, run::{Plan, Unit}}; use crate::plan::Desc; use crate::{ - Number, Value, agreement::{Agree, Agreement}, array::{Layout, Storage, float_width_of}, element::{Converts, Stored}, explain::Tally, + Number, Value, agreement::{Agree, Agreement}, array::{Storage, float_width_of}, element::Stored, explain::Tally, number::{Halving, Numeric}, pervasive::{self, Reduce}, plan::Pervasive, primitive::{FoldKind, Primitive} }; use rustymath::{ - Arg, agreement::{Mapping, Period, Walked, expanded, expanded_items, in_place, tiled_cycle}, buffer::{Buffer, DType, Element, FloatWidth, View, Width}, - gather::Steps, layout::strides, operation::{Kernel, Settings}, parallel::{self, Slots}, reduce, tolerant::{Exactly, Tolerance} + agreement::{Binding, Mapping, Missing, Period, ReadPlan, Walked}, buffer::{Buffer, DType, Element, FloatWidth, MapOutput, Scratch, Width}, + gather::Steps, layout::strides, operation::{Kernel, Settings}, parallel::{self, Slots}, reduce, tolerant::Tolerance }; use std::{borrow::Cow, ops::Range}; #[cfg(not(web))] -use {crate::jit, std::sync::{Arc, OnceLock, atomic::Ordering::Relaxed}}; - -/// Runs `$body` with `$x` bound to the items of `$e`, a `Got`, at their own type. -macro_rules! typed { - ($e:expr, $enum:ident, |$x:ident| $body:expr) => { - match $e { - $enum::F32($x) => $body, - $enum::F64($x) => $body, - $enum::Bool($x) => $body, - $enum::U8($x) => $body, - $enum::I16($x) => $body, - $enum::I32($x) => $body, - $enum::I64($x) => $body, - } - }; -} +use {crate::jit, rustymath::buffer::View, std::sync::{Arc, OnceLock, atomic::Ordering::Relaxed}}; /// Runs `$body` with `$t` bound to the item type of `$dtype`, a type that a stage holds (`readable`). macro_rules! with_item { @@ -99,38 +84,43 @@ pub fn compiled_runs() -> usize { 0 } #[cfg(not(web))] fn ran_compiled(items: usize) { COMPILED_RUNS.fetch_add(1, Relaxed); - rustymath::events::taken(rustymath::events::Route::Compiled, items); + crate::events::taken(crate::events::routes::Compiled, items); } -/// An array that a stage reads: the program's node `of`, read through `map`. Where the node's own map walks entries for each cycle (`Period::Walks`), or cycles with a period shorter than a block, `gather` holds that map, the stage writes the node's cycle out once each time it runs (`gathered_once`), and `map` reads it as `Tiled`. Where the stage would walk each block of the node's map as one strided run, while the whole copy reads through the tiled transpose (`tiles_whole`), the stage gathers the whole copy through `gather` instead, and `map` reads it in order. Both maps find their walk and cycle once and keep them for every run (`Walked`). +/// An array that a stage reads: the program's node `of`, read through `map`. Where the node's own map walks entries for each cycle (`Period::Walks`), or cycles with a period shorter than a block, `gather` holds that map, RM writes the node's cycle out once each time it runs (`Walked::gathered`), and `map` reads it as `Tiled`. Where the stage would walk each block of the node's map as one strided run, while the whole copy reads through the tiled transpose (`tiles_whole`), RM gathers the whole copy through `gather` instead, and `map` reads it in order. Both maps find their walk and cycle once and keep them for every run (`Walked`). #[derive(PartialEq)] -struct Input { of: usize, map: Walked<'static>, gather: Option> } +struct Input { of: usize, items: usize, map: Walked<'static>, gather: Option> } impl Input { /// Node `of` read through `map`, as its cycle written out once where `map` walks entries, or cycles with a period shorter than a block that a reader writes out (`Period::tiling`, as `tiled_cycle` writes it), or as one gathered copy where `map` tiles whole. A longer cycle reads in place. - fn new(of: usize, map: Mapping) -> Self { + fn new(of: usize, items: usize, map: Mapping) -> Self { let block = crate::settings::sizes().block; match map.period() { - Some(Period::Walks { len, .. }) => Self { of, map: Mapping::Tiled(len).into(), gather: Some(map.into()) }, + Some(Period::Walks { len, .. }) => Self { of, items, map: Mapping::Tiled(len).into(), gather: Some(map.into()) }, Some(period @ Period::Cycles(p)) if p < block && period.tiling(block).is_some() => { - Self { of, map: Mapping::Tiled(p).into(), gather: Some(map.into()) } + Self { of, items, map: Mapping::Tiled(p).into(), gather: Some(map.into()) } } - _ if map.tiles_whole(block) => Self { of, map: Mapping::Linear(1).into(), gather: Some(map.into()) }, - _ => Self { of, map: map.into(), gather: None }, + _ if map.tiles_whole(block) => Self { of, items, map: Mapping::Linear(1).into(), gather: Some(map.into()) }, + _ => Self { of, items, map: map.into(), gather: None }, } } + fn plan(&self, n: usize, block: usize) -> ReadPlan { + let items = self.gather.as_ref().map_or(self.items, |g| + g.period().and_then(|p| p.tiling(block)).map_or(n, |(_, items)| items)); + ReadPlan::copy(0, items, (*self.map).clone(), n, Missing::Default) + } } -/// Whether a stage's runs read an input through `map` as one item each: a repeated item, or a repeat of at least `run` items (`Mapping::repeats`), at whose ends the runs split (`reading`). A repeat whose items compiled parts read from a table, as `read` says (`Reading::Tabled`), isn't one item. +/// Whether compiled runs read an input through `map` as one item each: a repeated item, or a repeat of at least `run` items (`Mapping::repeats`), at whose ends the runs split (`reading`). A repeat whose items compiled parts read from a table, as `read` says (`Reading::Tabled`), isn't one item. #[cfg(not(web))] fn one_item(map: &Mapping, read: Reading, run: usize) -> bool { - !matches!(read, Reading::Tabled(_)) && (matches!(map, Mapping::Single) || map.repeats().is_some_and(|r| r >= run)) + !matches!(read, Reading::Tabled) && (matches!(map, Mapping::Single) || map.repeats().is_some_and(|r| r >= run)) } /// A block stage: steps on `n` items, which read the program's arrays in `inputs` through their maps and its atoms in `constants`, then store the steps that `stores` names as the program's values, and fold the last step where `fold` says. `extra` lists the stored steps that write outputs of their own: each stored step but the last, which writes the value's slots, unless the fold reads the last. `slots` gives each step's buffer, of the type that `kinds` gives. `count` is the number of buffers that the steps would take if every buffer had one type, which with `size`, the bytes of the widest type that a step computes at, sizes the blocks. `converted` holds each step's constants at its type, where the program holds every constant that the stage reads, and `laid` the stage's split into blocks under the sizes in force when it was made. `plain` says whether every input reads in place over any range, as a repeated item or the identity, and `tables` holds each tabled input's runs: their length and stride, and the offset of each run's first item (`Mapping::runs`). `parts` splits the steps into the runs that each run computes in turn (`Part`), and `folding` is the kernel that folds the last step for a stage of one compiled part (`Blocks::over`). `nodes` are the program's nodes that the stage computes, and `glyphs` their glyphs, for `•explain`. pub(crate) struct Blocks { n: usize, inputs: Vec, constants: Vec, steps: Vec, stores: Vec, extra: Vec, fold: Option, - slots: Vec>, kinds: Vec, count: usize, size: usize, converted: Option>; 3]>>, laid: Laid, plain: bool, + slots: Vec>, kinds: Vec, count: usize, size: usize, converted: Option; 3]>>, laid: Laid, direct: ReadPlan, single: ReadPlan, plain: bool, parts: Vec, glyphs: String, pub(crate) nodes: Vec, #[cfg(not(web))] folding: Option, #[cfg(not(web))] declines: Vec<&'static str>, #[cfg(not(web))] tables: Vec)>> } @@ -139,9 +129,10 @@ impl std::fmt::Debug for Blocks { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Blocks").field("nodes", &self.nodes).finish() } } -/// One step of a block stage: program node `j`, what it computes, its operands, the type it computes at and reads its operands as, the tolerance of a float's floor where it converts floats to integers, whether it gives Booleans, whether its value holds fixed integers, and its node's layout, which its value takes with its keys and axis names. +/// One block-stage step. `op` and `tolerance` feed native JIT lowering; `map` executes precompiled work with its resolved settings. `desc` preserves the published atom, kind and layout independently of the temporary computing dtype (`made`). +#[cfg_attr(web, expect(dead_code, reason = "op and tolerance are shared-construction metadata for native JIT lowering"))] struct BlockStep { - j: usize, op: Op, operands: [Option; 3], at: DType, tolerance: Tolerance, booleans: bool, fixed: bool, layout: Layout, kernel: Option + j: usize, op: Op, operands: [Option; 3], at: DType, tolerance: Tolerance, booleans: bool, desc: Desc, map: rustymath::operation::Map } impl BlockStep { @@ -150,19 +141,24 @@ impl BlockStep { /// The earlier steps that this step reads. fn reads(&self) -> impl Iterator + '_ { self.operands.iter().flatten().filter_map(|o| match *o { Operand::Step(t) => Some(t), _ => None }) } /// The step's constant operands at its type: constant `k` from `number(k)`, the number of the stage's constant `k`, and a fold's identity (`identity`). `None` where `number` gives none or a number doesn't convert to the type. - fn constants<'a>(&self, number: impl Fn(usize) -> Option) -> Option<[Option>; 3]> { - let mut at = [None; 3]; + fn constants(&self, number: impl Fn(usize) -> Option) -> Option<[Option; 3]> { + let mut at = std::array::from_fn(|_| None); for (c, o) in at.iter_mut().zip(self.operands) { let n = match o { Some(Operand::Constant(k)) => number(k)?, Some(Operand::Identity(r)) => Number::float(identity(r)?, float_width_of(self.at)?), _ => continue }; - *c = Some(with_item!(self.at, A => A::constant(&n).map(|v| A::got(Arg::One(v))))?); + *c = Some(match self.at { + DType::Float(w) => Number::float(n.to_float().ok()?, w), + DType::Integer(w) => Number::fixed(n.marked_exact()?.as_integer()?, w), + DType::Boolean => Number::from_bool(n.as_bool()?), _ => return None + }); } Some(at) } } /// What a step computes: a pervasive primitive as its plan plans it, its operand converted to the step's type, or its first operand where its third, a mask, is true and its second where it's false. +#[cfg_attr(web, expect(dead_code, reason = "Map’s plan is native JIT lowering metadata"))] enum Op { Map(Pervasive), Convert, Select } impl Op { @@ -175,9 +171,9 @@ impl Op { #[derive(Clone, Copy, PartialEq)] enum Operand { Input(usize), Constant(usize), Identity(Reduce), Step(usize) } -/// A fold by `p` of the last step's items, laid out as `dims`, `[outer, len, inner]`, along its middle axis, whose totals, laid out as `layout`, with its node's keys and axis names, and of type `dtype`, are program node `j`. `fixed` is whether those totals are fixed integers. `seed` is the atom node that each total takes as its last item, where the fold has a seed. `kernel` folds the lanes under `numeric`, at the last step's type. +/// A fold by `p` over `[outer, len, inner]` lanes. `desc` describes its published totals, including atom versus rank-0 array. The kernel folds at the last step's computing type; `seed` is the node taken as each total's last item. struct BlockFold { - j: usize, p: Primitive, dims: [usize; 3], layout: Layout, dtype: DType, fixed: bool, seed: Option, numeric: Numeric, settings: Settings, + j: usize, p: Primitive, dims: [usize; 3], desc: Desc, seed: Option, numeric: Numeric, settings: Settings, kernel: Option } @@ -224,29 +220,37 @@ impl Blocks { fn new(program: &Program, readers: &[Vec], members: &[usize], n: usize) -> Option { let mut b = Blocks { n, inputs: Vec::new(), constants: Vec::new(), steps: Vec::new(), stores: Vec::new(), extra: Vec::new(), fold: None, slots: Vec::new(), kinds: Vec::new(), count: 0, size: 0, converted: None, - laid: Laid { sizes: crate::settings::Sizes::DEFAULT, block: 0, reads: Vec::new() }, glyphs: String::new(), nodes: members.to_vec(), plain: false, + direct: ReadPlan::input(0, n), single: ReadPlan::copy(0, 1, Mapping::Single, n, Missing::Default), + laid: Laid { sizes: crate::settings::Sizes::DEFAULT, block: 0, reads: Vec::new(), plans: Vec::new() }, glyphs: String::new(), nodes: members.to_vec(), plain: false, parts: Vec::new(), #[cfg(not(web))] folding: None, #[cfg(not(web))] declines: Vec::new(), #[cfg(not(web))] tables: Vec::new() }; for &j in members { let node = &program.nodes[j]; let Origin::Step(step) = &node.origin else { return None }; let shape = node.desc.shape(); - let made = |op, operands, at, tolerance| BlockStep { j, op, operands, at, tolerance, booleans: node.desc.dtype() == DType::Boolean, - fixed: node.desc.kind.fixed.is_some(), layout: node.desc.layout.clone(), kernel: None }; + let made = |op, operands, at, tolerance, map| BlockStep { j, op, operands, at, tolerance, booleans: node.desc.dtype() == DType::Boolean, + desc: node.desc.clone(), map }; match step { Step::Map { left, right, plan } => { let at = computes(plan, node.desc.dtype())?; - let (x, y) = b.paired(program, members, left.as_ref(), right, shape)?; - b.steps.push(BlockStep { kernel: Some(plan.kernel()?.clone()), ..made(Op::Map(plan.clone()), [x, Some(y), None], at, Tolerance::EXACT) }); + let x = match left { Some(r) => Some(b.operand(program, members, r, &r.pointwise(&program.nodes), shape)?), None => None }; + let y = b.operand(program, members, right, &right.pointwise(&program.nodes), shape)?; + let map = rustymath::operation::Map::Kernel(plan.kernel()?.clone(), plan.numeric.settings_for(plan.computes()?)); + b.steps.push(made(Op::Map(plan.clone()), [x, Some(y), None], at, Tolerance::EXACT, map)); } Step::Convert { of, numeric } => { let y = b.operand(program, members, of, &Mapping::Linear(1), shape)?; - let tolerance = numeric.tolerance_of(program.nodes[of.of].desc.dtype()); - b.steps.push(made(Op::Convert, [None, Some(y), None], node.desc.dtype(), tolerance)); + let input = program.nodes[of.of].desc.dtype(); + let tolerance = numeric.tolerance_of(input); + let conversion = rustymath::convert::Conversion { input, output: node.desc.dtype(), kind: rustymath::convert::ConversionKind::Floor }; + let map = rustymath::operation::Map::Convert(conversion, Settings { tolerance }, false); + b.steps.push(made(Op::Convert, [None, Some(y), None], node.desc.dtype(), tolerance, map)); } Step::Select { mask, yes, no } => { - let read = |r: &Read| if program.nodes[r.of].desc.atom { Mapping::Single } else { Mapping::Linear(1) }; - let [m, x, y] = [mask, yes, no].map(|r| b.operand(program, members, r, &read(r), shape)); - b.steps.push(made(Op::Select, [Some(x?), Some(y?), Some(m?)], node.desc.dtype(), Tolerance::EXACT)); + let [m, x, y] = [mask, yes, no].map(|r| b.operand(program, members, r, &r.pointwise(&program.nodes), shape)); + let at = node.desc.dtype(); + let map = rustymath::operation::Map::Kernel( + Kernel::new(rustymath::Choose, rustymath::domain::Variant { dtype: at, checked: false, wrapping: true }), Settings { tolerance: Tolerance::EXACT }); + b.steps.push(made(Op::Select, [Some(x?), Some(y?), Some(m?)], at, Tolerance::EXACT, map)); } Step::Copy { .. } => {} &Step::Fold { p, axis, ref of, ref seed, .. } => match short_fold(program, j) { @@ -261,7 +265,7 @@ impl Blocks { let dims = [crate::array::count(&shape[..axis]), shape[axis], crate::array::count(&shape[axis + 1..])]; let current = Numeric::current(); let numeric = Numeric { fixed: node.desc.kind.fixed.or(current.fixed), ..current }; - b.fold = Some(BlockFold { j, p, dims, layout: node.desc.layout.clone(), dtype: node.desc.dtype(), fixed: node.desc.kind.fixed.is_some(), + b.fold = Some(BlockFold { j, p, dims, desc: node.desc.clone(), seed: seed.as_ref().map(|s| s.of), numeric, settings: numeric.settings_for(program.nodes[of.of].desc.dtype()), kernel: None }); } }, @@ -326,20 +330,13 @@ impl Blocks { p, Some(items.kind), items.kind, [None, constant], [rustymath::domain::Range::ANY; 2], crate::number::Numeric::current()); let at = computes(&plan, node.desc.dtype())?; // As in a step of rustymath's `Folds`, each read is the left operand and the total the right. Beside a NaN, `Core::maximum` and `minimum` give the right operand, the total. - self.steps.push(BlockStep { j, kernel: Some(plan.kernel()?.clone()), op: Op::Map(plan), operands: [Some(x), Some(total), None], at, - tolerance: Tolerance::EXACT, booleans: node.desc.dtype() == DType::Boolean, fixed: node.desc.kind.fixed.is_some(), - layout: node.desc.layout.clone() }); + let map = rustymath::operation::Map::Kernel(plan.kernel()?.clone(), plan.numeric.settings_for(plan.computes()?)); + self.steps.push(BlockStep { j, map, op: Op::Map(plan), operands: [Some(x), Some(total), None], at, + tolerance: Tolerance::EXACT, booleans: node.desc.dtype() == DType::Boolean, desc: node.desc.clone() }); total = Operand::Step(self.steps.len() - 1); } Some(()) } - /// The operands of a step that reads `left`, where it's dyadic, and `right` through the maps that agreement gives them (`operand`). - fn paired(&mut self, program: &Program, members: &[usize], left: Option<&Read>, right: &Read, frame: &[usize]) -> Option<(Option, Operand)> { - let agreement = agreement(left, right)?; - let x = match left { Some(r) => Some(self.operand(program, members, r, &agreement.left, frame)?), None => None }; - Some((x, self.operand(program, members, right, &agreement.right, frame)?)) - } - /// The operand that reads `read`, which agreement lays over a step's items of `frame` through `map`: a step of the stage, an atom, or an array that the stage reads through `map`, composed with the map of any copy of the stage's that it reaches it through. `None` for a step of the stage that `map` doesn't read item for item, or a copy whose map doesn't compose with `map`. fn operand(&mut self, program: &Program, members: &[usize], read: &Read, map: &Mapping, frame: &[usize]) -> Option { let (mut i, mut map) = (read.of, map.clone()); @@ -357,7 +354,7 @@ impl Blocks { }); return Some(Operand::Constant(k)); } - let input = Input::new(i, map); + let input = Input::new(i, program.nodes[i].desc.layout.count(), map); let k = self.inputs.iter().position(|x| *x == input).unwrap_or_else(|| { self.inputs.push(input); self.inputs.len() - 1 @@ -385,29 +382,31 @@ impl Blocks { /// How the stage splits its items under `sizes`. fn laid_out(&self, sizes: crate::settings::Sizes) -> Laid { let block = self.block(sizes); - Laid { sizes, block, reads: self.inputs.iter().map(|x| reading(&x.map, block, sizes.run)).collect() } + Laid { sizes, block, plans: self.inputs.iter().map(|x| x.plan(self.n, block)).collect(), + reads: self.inputs.iter().map(|x| reading(&x.map, block, sizes.run)).collect() } } - /// Runs the stage on its inputs' values in `values`, and puts the values it stores and folds there. `None`, with nothing changed, where an input isn't compact items of a type that a stage holds, an atom doesn't convert to the type of each step that reads it, or a step fails: the caller then runs the stage's nodes on their own. Constants that the program holds were converted when the stage was made, and the stage's split into blocks was found then under the sizes in force. The stage's compiled parts run where the compile setting asks for them (`compiles`). `•explain` records a run of two or more steps, its fold included, as one fused group, with its blocks, runs and copied items. A lone step that reads through a copy isn't a fused group. A failed seed combination returns its error without storing the stage's results. + /// Runs with borrowed inputs and private destination buffers, publishing stored and folded values after success. `None` discards the attempt for eager execution of the stage's nodes: an input lacks a supported compact type, a constant cannot convert, or a step fails after writing private results. A failed seed combination returns its error without publishing results. + /// Held constants and block partitions are prepared when the stage is built; changed sizes rebind its partition. Compiled parts run under `compiles`. `•explain` records two or more steps (fold included) as one fused group, with blocks, runs and copied items. A lone step reading through a copy is not a fused group. pub(crate) fn run(&self, values: &mut [Option]) -> Option> { let tally = Tally::new(); let sizes = crate::settings::sizes(); let laid = if self.laid.sizes == sizes { Cow::Borrowed(&self.laid) } else { Cow::Owned(self.laid_out(sizes)) }; let (main, rest) = { let read = |of: usize| values[of].as_ref()?.view().filter(|v| readable(v.dtype())); - // What each input gathers once for every range (`gathered_once`): its cycle, written out to cover a block at any phase where it's shorter, or the whole copy. + // RM prepares a cycle's backing or a whole walking copy once, before any parallel ranges. let gathered: Vec> = self.inputs.iter().zip(&laid.reads).map(|(x, r)| match &x.gather { Some(g) => { - let items = gathered_once(read(x.of)?, g, laid.block); + let items = g.gathered(read(x.of)?, laid.block)?; if matches!(r, Reading::Cycle) { tally.copied(items.len()); } Some(Some(items)) } None => Some(None), }).collect::>()?; - let inputs: Vec> = self.inputs.iter().zip(&gathered).map(|(x, g)| match g { - Some(g) => Some(g.view()), None => read(x.of) + let inputs: Vec> = self.inputs.iter().zip(&gathered).map(|(x, g)| match g { + Some(g) => Some(Binding::full(g.view())), None => read(x.of).map(Binding::full) }).collect::>()?; - let constants: Cow<'_, [[Option>; 3]]> = match &self.converted { + let constants: Cow<'_, [[Option; 3]]> = match &self.converted { Some(converted) => Cow::Borrowed(converted), None => { let numbers: Vec = self.constants.iter().map(|&i| values[i].as_ref()?.as_number()).collect::>()?; @@ -418,7 +417,8 @@ impl Blocks { let compiled = self.compiles(sizes); #[cfg(web)] let compiled = false; - let run = Run { inputs, constants, reads: &laid.reads, block: laid.block, tally: &tally, compiled }; + let run = Run { inputs, plans: &laid.plans, direct: &self.direct, single: &self.single, + constants, reads: &laid.reads, block: laid.block, tally: &tally, compiled }; let mut outs: Vec = self.extra.iter().map(|&s| Buffer::with_capacity(self.steps[s].made(), self.n)).collect(); let shared: Vec> = (0..self.steps.len()).map(|s| self.extra.iter().position(|&e| e == s).map(|k| Shared(outs[k].as_mut_ptr()))).collect(); @@ -426,7 +426,7 @@ impl Blocks { None => { let step = &self.steps[self.steps.len() - 1]; let buffer = with_item!(step.made(), T => T::buffer(self.written::(&run, &shared, T::last)?)); - (step.j, built(buffer, &step.layout, step.fixed)?) + (step.j, built(buffer, &step.desc)?) } Some(fold) => { let seed = match fold.seed { Some(s) => Some(values[s].as_ref()?.as_number()?), None => None }; @@ -437,36 +437,40 @@ impl Blocks { let rest: Vec<(usize, Value)> = outs.into_iter().zip(&self.extra).map(|(mut buffer, &s)| { // SAFETY: the ranges cover `0..n`, and each wrote its items of every stored step (`range`). unsafe { buffer.set_len(self.n) }; - Some((self.steps[s].j, built(buffer, &self.steps[s].layout, self.steps[s].fixed)?)) + Some((self.steps[s].j, built(buffer, &self.steps[s].desc)?)) }) .collect::>()?; (main, rest) }; if self.steps.len() + usize::from(self.fold.is_some()) >= 2 { crate::explain::fused(|| self.glyphs.clone(), self.n, &tally); } - if self.fold.is_some() && !self.steps.is_empty() { rustymath::events::taken(rustymath::events::Route::FusedFold, self.n); } + if self.fold.is_some() && !self.steps.is_empty() { crate::events::taken(crate::events::routes::FusedFold, self.n); } values[main.0] = Some(main.1); for (j, value) in rest { values[j] = Some(value); } Some(Ok(())) } + /// Input copies are geometry, not extra operations whose intermediate values need block fusion. + fn one_operation(&self) -> bool { self.steps.len() == 1 && self.fold.is_none() } + /// The kernels of the stage's compiled parts. #[cfg(not(web))] fn kernels(&self) -> impl Iterator { self.parts.iter().filter_map(|p| p.lowered.as_ref()).map(|l| &l.code) } - /// Whether this run uses the stage's compiled parts, as the compile setting asks (`settings::compiles`). A stage compiles all its parts or none. `Never` uses none, and `Always` compiles them where they aren't compiled yet. The policy compiles them where the items repay a compile (`COMPILES_AT`) or where the first part's kernel ran or compiled before, in this stage or another (`jit::Kernel::seen`). A dfn program's stages therefore compile on the program's first reuse. A part reads an input as one item for the sizes it was lowered under (`one_item`), so it serves only runs under those sizes. It reads each input at its node's planned dtype, which a program stores each step's items at (`run::node`). Each run that uses compiled parts counts in `compiled_runs` and takes the `compiled` route. Where the setting asks, each reason that `parted` found for a step that compiles in no part declines that route. + /// Whether compiled parts serve this run. Policy compiles stages with intermediate work to fuse when their size repays it or their kernel ran before; one-operation stages stay on whole-array native kernels. Always still requests compilation for either category. A part serves only the run sizes and input dtypes it was lowered for. Asked-for unsupported parts record their existing decline reasons. #[cfg(not(web))] fn compiles(&self, sizes: crate::settings::Sizes) -> bool { use crate::settings::Compiles; - use rustymath::events::{Route, declined}; + use crate::events::declined; let first = self.kernels().next(); let asks = sizes.run == self.laid.sizes.run && match crate::settings::compiles() { Compiles::Never => false, Compiles::Always => true, - Compiles::Policy => compiles_on_first_run(self.n) || first.is_some_and(|f| f.code.get().is_some() || f.kernel.seen()) + Compiles::Policy => !self.one_operation() + && (compiles_on_first_run(self.n) || first.is_some_and(|f| f.code.get().is_some() || f.kernel.seen())) }; - if asks { for &reason in &self.declines { declined(Route::Compiled, reason, self.n); } } + if asks { for &reason in &self.declines { declined(crate::events::routes::Compiled, reason, self.n); } } if first.is_none() || !asks { return false; } if !self.kernels().all(|c| c.compiled(|| self.glyphs.clone()).is_some()) { - declined(Route::Compiled, "a kernel that doesn't compile", self.n); + declined(crate::events::routes::Compiled, "a kernel that doesn't compile", self.n); return false; } // The folding kernel compiles with the part that it serves. @@ -519,11 +523,10 @@ impl Blocks { self.parts.iter().any(|p| p.lowered.is_some() && p.steps.contains(&s)) || !step.operands.contains(&Some(Operand::Input(k))) }); let Some((l, step)) = self.inputs[k].map.regular() else { continue }; - // A repeat that runs would read as one item keeps its length, at which a run that no compiled part takes still ends. - let repeat = match self.laid.reads[k] { Reading::Copied => None, Reading::Runs(Some(r)) if step == 0 => Some(r), _ => continue }; + match self.laid.reads[k] { Reading::Copied => {}, Reading::Runs(Some(_)) if step == 0 => {}, _ => continue } if !only || step.unsigned_abs() > STRIDE || l < least || len.is_some_and(|len| len != l) { continue; } let Some(runs) = self.inputs[k].map.runs(self.n) else { continue }; - (len, self.laid.reads[k], self.tables[k]) = (Some(l), Reading::Tabled(repeat), Some(runs)); + (len, self.laid.reads[k], self.tables[k]) = (Some(l), Reading::Tabled, Some(runs)); } len.is_some() } @@ -533,7 +536,7 @@ impl Blocks { fn read(&self, o: Operand) -> jit::Read { let Operand::Input(k) = o else { return jit::Read::Forward }; match (self.laid.reads[k], self.tables.get(k).and_then(Option::as_ref)) { - (Reading::Tabled(_), Some(&(_, step, _))) => jit::Read::Runs(match step { + (Reading::Tabled, Some(&(_, step, _))) => jit::Read::Runs(match step { 0 => jit::Within::Repeated, 1 => jit::Within::Forward, -1 => jit::Within::Backward, _ => jit::Within::Strided }), _ => jit::Read::Forward, @@ -633,14 +636,15 @@ impl Blocks { fn written( &self, run: &Run<'_>, outs: &[Option], last: impl for<'a, 'b> Fn(&'a mut Slots<'b, B>) -> Last<'a, 'b> + Sync) -> Option> { let mut items = Vec::new(); - parallel::extended(&mut items, self.n, run.block, &|range, slots| self.range(range, run, outs, &mut last(slots))).then_some(items) + rustymath::error::checked(|| Ok(parallel::extended(&mut items, self.n, run.block, &|range, slots| self.range(range, run, outs, &mut last(slots))))) + .ok()?.then_some(items) } /// The fold's value, through its kernel's fold of the pieces that the stage hands over a block at a time (`Kernel::fold_pieces`): each block's items, or their total where a compiled part folds them. The kernel's formula picks the reduction, as the eager fold's does (`pervasive::fold`), so `-` and `÷` fold as alternating sums and products. A seed then combines with each total through `pervasive::map`, as the eager fold's does. A fold of every item gives its one total as an atom, as `folded` in `eval::operators` takes it. `None` where a step or the kernel fails. A failed seed combination returns its error. fn folded(&self, fold: &BlockFold, seed: Option<&Number>, run: &Run<'_>, outs: &[Option]) -> Option> { use rustymath::{error::{Error, Kind as Failure}, reduce::{Direction, Piece, Seed, Total}}; let kernel = fold.kernel.as_ref()?; - let mut out = Buffer::with_capacity(fold.dtype, 0); + let mut out = Buffer::with_capacity(fold.desc.dtype(), 0); let pieces = |range: Range, give: &mut dyn FnMut(usize, Piece<'_>)| -> Result<(), Error> { let ran = self.range(range, run, outs, &mut Last::Fold(&mut |start, block: Folded<'_>| { give(start, match block { Folded::Held(held) => Piece::Items(held.view()), Folded::Total(items, bits) => Piece::Total(Total { items, bits }) }) @@ -648,25 +652,29 @@ impl Blocks { if ran { Ok(()) } else { Err(Error::new(Failure::Domain, "a block stage's step failed")) } }; kernel.fold_pieces(fold.dims, Seed::Identity, Direction::Right, fold.settings, &pieces, &mut out).ok()?; - let value = built(out, &fold.layout, fold.fixed)?; - let value = if fold.layout.shape().is_empty() { value.at(0) } else { value }; + let value = built(out, &fold.desc)?; let Some(seed) = seed else { return Some(Ok(value)) }; let seed = Value::Number(seed.clone()); let agreement = Agreement::new(value.layout(), seed.layout()).ok()?; - pervasive::map(&crate::plan::pervasive_on(fold.p, Some(&value), &seed, fold.numeric), Some(&value), &seed, &agreement) + let result = pervasive::map(&crate::plan::pervasive_on(fold.p, Some(&value), &seed, fold.numeric, Some(&agreement)), Some(&value), &seed, &agreement)?; + Some(result.map(|value| published(value, &fold.desc))) } - /// Runs the stage on each block of `range` in turn, in runs that end at each repeat or long cycle of an input that the run reads in place (`Reading::ends`). Each run places its inputs' items once (`placed`), then computes the stage's parts in turn. A compiled part computes its steps in one call (`called`). Otherwise each step writes its buffer for the run, a stored step copies its items to its output in `outs` as it finishes, and the last step writes to `last`: its value's slots, or the block that the fold reads, which the fold gets after each block with the block's position. A stage of one compiled part can compute a stretch of whole blocks in one call instead (`over`). Gives false where a kernel fails. + /// Run the stage block by block, splitting compiled runs at input repeats. Native steps bind RM reads; compiled parts prepare ABI spans. Stored steps copy to their outputs. The last step writes directly to its final output or the fold's buffer. fn range(&self, range: Range, run: &Run<'_>, outs: &[Option], last: &mut Last<'_, '_>) -> bool { let (block, tally) = (run.block, run.tally); + let mut scratch = Scratch::default(); + if self.one_operation() && !run.compiled && !range.is_empty() { + tally.block(); + tally.run(); + let ok = self.step(&self.steps[0], run, &[], &run.constants[0], &mut scratch, range.start, range.len(), last.output()); + tally.copied(scratch.copied()); + return ok; + } let mut bufs: Vec = Vec::new(); let mut held = matches!(last, Last::Fold(_)).then(|| Buffer::with_capacity(self.steps[self.steps.len() - 1].made(), block)); - // Only an input read through a map other than a repeated item or the identity can need a copy. - let mut copies: Vec = if self.plain { Vec::new() } else { run.inputs.iter().map(|d| Buffer::with_capacity(d.dtype(), 0)).collect() }; - // A compiled run reads a tabled input in place, except in a part that runs on its precompiled kernels, which reads it from a copy in `spare`. - let tabled = run.compiled && run.reads.iter().any(|r| matches!(r, Reading::Tabled(_))); - let mut spare: Vec = if tabled { run.inputs.iter().map(|d| Buffer::with_capacity(d.dtype(), 0)).collect() } else { Vec::new() }; - let mut scratch: Option = None; + #[cfg(not(web))] + let mut copies: Vec = if run.compiled { run.inputs.iter().map(|d| Buffer::with_capacity(d.view.dtype(), 0)).collect() } else { Vec::new() }; let mut ok = true; let mut start = range.start; while start < range.end { @@ -683,17 +691,13 @@ impl Blocks { let to = run.reads.iter().fold(end, |to, r| r.ends(run.compiled).map_or(to, |r| to.min((from / r + 1) * r))); let n = to - from; tally.run(); - let got = self.placed(run, &mut copies, from, n, run.compiled, &[]); + #[cfg(not(web))] + let got = if run.compiled { self.prepared(run, &mut copies, from, n) } else { None }; if bufs.len() < self.kinds.len() { bufs = self.kinds.iter().map(|&k| Buffer::with_capacity(k, block)).collect(); } for part in &self.parts { #[cfg(not(web))] - // A part whose kernel can't take this run's items runs its steps on their precompiled kernels instead. - if part.lowered.as_ref() - .is_some_and(|lowered| run.compiled && self.called(lowered, run, &got, &mut bufs, from, n, outs, last, held.as_mut())) { continue; } - // A part that runs on its precompiled kernels in a compiled run reads each tabled input from a copy, and the rest as the run placed them. - let copied; - let got = if tabled { copied = self.placed(run, &mut spare, from, n, false, &got); &copied[..] } else { &got[..] }; - let scratch = scratch.get_or_insert_with(Scratch::default); + if part.lowered.as_ref().is_some_and(|lowered| got.as_ref() + .is_some_and(|got| self.called(lowered, run, got, &mut bufs, from, n, outs, last, held.as_mut()))) { continue; } for s in part.steps.clone() { let step = &self.steps[s]; let out = outs[s].as_ref(); @@ -701,19 +705,19 @@ impl Blocks { Some(b) => { let mut buffer = std::mem::replace(&mut bufs[b], Buffer::Booleans(Vec::new())); buffer.clear(); - ok &= self.step(step, &got, &bufs, &run.constants[s], scratch, n, Dest::Buffer(&mut buffer)); - // SAFETY: each output has room for the stage's `n` items, and `parallel::extended` and `reduce::fold_pieces` each give this call a range of its own. + ok &= self.step(step, run, &bufs, &run.constants[s], &mut scratch, from, n, MapOutput::Buffer(&mut buffer)); + // SAFETY: every output has room for the stage's items, and each parallel call owns its range. if let Some(out) = out { unsafe { write(&buffer, 0, out, from, n) } } bufs[b] = buffer; } None => match (&mut *last, &mut held) { (Last::Fold(_), Some(held)) => { let at = held.len(); - ok &= self.step(step, &got, &bufs, &run.constants[s], scratch, n, Dest::Buffer(held)); + ok &= self.step(step, run, &bufs, &run.constants[s], &mut scratch, from, n, MapOutput::Buffer(held)); // SAFETY: as for the other steps. if let Some(out) = out { unsafe { write(held, at, out, from, n) } } } - (last, _) => ok &= self.step(step, &got, &bufs, &run.constants[s], scratch, n, Dest::Last(last)), + (last, _) => ok &= self.step(step, run, &bufs, &run.constants[s], &mut scratch, from, n, last.output()), }, } } @@ -726,6 +730,7 @@ impl Blocks { } start = end; } + tally.copied(scratch.copied()); ok } @@ -747,8 +752,8 @@ impl Blocks { while to < range.end && (self.plain || to == range.start) && takes(to) { to = range.end.min(to + block); } let n = to - range.start; if n == 0 { return None; } - let got = self.placed(run, copies, range.start, n, true, &[]); - let (arrays, scalars, places, a, c) = self.arguments(lowered, run, &got, &mut [], range.start, outs)?; + let got = self.prepared(run, copies, range.start, n)?; + let (arrays, scalars, places, a, c) = self.arguments(lowered, run, &got, &mut [], range.start, n, outs)?; let mut totals = vec![0; n.div_ceil(block)]; // SAFETY: as in `called`, with no place for the last step, which the kernel folds. unsafe { code.run(n, block, &arrays[..a], &scalars[..c], &places[..lowered.stores.len() - 1], &mut totals) }; @@ -760,7 +765,7 @@ impl Blocks { Some(to) } last if self.plain => { - let got = self.placed(run, copies, range.start, range.len(), true, &[]); + let got = self.prepared(run, copies, range.start, range.len())?; self.called(lowered, run, &got, &mut [], range.start, range.len(), outs, last, None).then_some(range.end) } _ => None, @@ -770,10 +775,11 @@ impl Blocks { /// Runs compiled part `lowered` on the run of `n` items from item `from`, and gives true. Its kernel reads its inputs' items in `got`, the run's constants and earlier parts' buffers in `bufs`. It writes each value that leaves the part (`Store`): to its buffer, whose stored step then goes to its output in `outs`, to its output, or as the stage's last step to `last`'s slots or to `held`, the block that the fold reads. Gives false, having written nothing, where an input's items don't come as the kernel reads them. #[cfg(not(web))] fn called( - &self, lowered: &Lowered, run: &Run<'_>, got: &[Got<'_>], bufs: &mut [Buffer], from: usize, n: usize, outs: &[Option], last: &mut Last<'_, '_>, + &self, lowered: &Lowered, run: &Run<'_>, got: &[View<'_>], bufs: &mut [Buffer], from: usize, n: usize, outs: &[Option], last: &mut Last<'_, '_>, held: Option<&mut Buffer>) -> bool { + use rustymath::buffer::OutputItem; let Some(code) = lowered.code.code() else { return false }; - let Some((arrays, scalars, mut places, a, c)) = self.arguments(lowered, run, got, bufs, from, outs) else { return false }; + let Some((arrays, scalars, mut places, a, c)) = self.arguments(lowered, run, got, bufs, from, n, outs) else { return false }; let k = lowered.stores.len(); let ends = matches!(lowered.stores.last(), Some(Store::Last)); let mut call = |to: *mut u8| { @@ -793,10 +799,13 @@ impl Blocks { } (true, Last::Fold(_), None) => return false, // SAFETY: the compiled kernel initializes every one of its `n` output items. - (true, last, _) => with_item!(self.steps[self.steps.len() - 1].made(), A => match A::out(last) { - Out::Items(slots) => { unsafe { slots.fill(n, |s| { call(s.as_mut_ptr().cast()); true }) }; } - Out::Bools(slots) => { unsafe { slots.fill(n, |s| { call(s.as_mut_ptr().cast()); true }) }; } - }), + (true, last, _) => { + let mut target = last.output(); + with_item!(self.steps[self.steps.len() - 1].made(), A => { + let slots = A::slots(&mut target).expect("the stage's last slots have its result type"); + unsafe { slots.fill(n, |s| { call(s.as_mut_ptr().cast()); true }) }; + }); + } } for &store in &lowered.stores { let Store::Buffer(s) = store else { continue }; @@ -812,43 +821,43 @@ impl Blocks { /// A compiled part's arguments for the run of items from item `from`: each array, from an input's items in `got`, a tabled input's item 0 and table, or an earlier part's buffer in `bufs`, then each scalar, from an input's one item in `got`, a tabled input's stride or the run's constants, in its 8-byte slot (`Item::slot`), with the runs' length and the items left in the first run after them where the part reads runs, then the place of each value that leaves the part, emptying each buffer that it writes, and the counts of array and scalar slots. The stage's last step's place is left for the caller. `None` where an input's items don't come as the kernel reads them. #[cfg(not(web))] fn arguments( - &self, lowered: &Lowered, run: &Run<'_>, got: &[Got<'_>], bufs: &mut [Buffer], from: usize, outs: &[Option] + &self, lowered: &Lowered, run: &Run<'_>, got: &[View<'_>], bufs: &mut [Buffer], from: usize, n: usize, outs: &[Option] ) -> Option<([*const u8; MOST], [u64; SCALARS], [*mut u8; MOST], usize, usize)> { let (mut arrays, mut a, mut runs) = ([std::ptr::null::(); MOST], 0, None); for (&operand, t) in lowered.arrays.iter().zip(lowered.code.kernel.inputs.iter().filter_map(|i| match *i { jit::Input::Array(t, _) => Some(t), _ => None })) { - // A tabled input takes its item 0, then its table of runs' first items from the run that holds item `from`. + if let Operand::Input(k) = operand && jit::Ty::of(run.inputs[k].view.dtype()) != Some(t) { return None; } + // Tabled reads still point at the source buffer, so its actual type must be checked before either ABI branch. if let jit::Read::Runs(_) = self.read(operand) { let Operand::Input(k) = operand else { return None }; - let (Reading::Tabled(_), Some((len, _, starts))) = (run.reads[k], self.tables[k].as_ref()) else { return None }; - *arrays.get_mut(a)? = run.inputs[k].as_ptr(); + let (Reading::Tabled, Some((len, _, starts))) = (run.reads[k], self.tables[k].as_ref()) else { return None }; + *arrays.get_mut(a)? = run.inputs[k].view.as_ptr(); *arrays.get_mut(a + 1)? = starts.get(from / len..)?.as_ptr().cast::(); runs = Some([*len as u64, (len - from % len) as u64]); a += 2; continue; } - *arrays.get_mut(a)? = match operand { - Operand::Input(k) if jit::Ty::of(got[k].dtype()) != Some(t) => return None, - Operand::Input(k) => typed!(got[k], Got, |x| match x { - Arg::Many(x) => x.as_ptr().cast::(), - Arg::One(_) => return None, - }), - Operand::Step(r) => bufs[self.slots[r]?].view().as_ptr(), + let view = match operand { + Operand::Input(k) => got[k], Operand::Step(r) => bufs[self.slots[r]?].view(), Operand::Constant(_) | Operand::Identity(_) => return None, }; + if jit::Ty::of(view.dtype()) != Some(t) || view.len() < n { return None; } + *arrays.get_mut(a)? = view.as_ptr(); a += 1; } let mut scalars = [0; SCALARS]; - for (slot, &scalar) in scalars.iter_mut().zip(&lowered.scalars) { + let types = lowered.code.kernel.inputs.iter().filter_map(|input| match *input { + jit::Input::Scalar(t) => Some(Some(t)), jit::Input::Array(_, jit::Read::Runs(jit::Within::Strided)) => Some(None), _ => None + }); + for ((slot, &scalar), ty) in scalars.iter_mut().zip(&lowered.scalars).zip(types) { let item = match scalar { - Scalar::Input(k) => got[k], Scalar::Constant(s, o) => run.constants[s][o]?, + Scalar::Input(k) => got[k], Scalar::Constant(s, o) => run.constants[s][o].as_ref()?.view()?, Scalar::Stride(k) => { *slot = self.tables[k].as_ref()?.1 as u64; continue; } }; - *slot = typed!(item, Got, |x| match x { - Arg::One(x) => Item::slot(x), - Arg::Many(_) => return None, - }); + if item.len() != 1 { return None; } + if matches!(scalar, Scalar::Input(_)) && jit::Ty::of(item.dtype()) != ty { return None; } + *slot = with_item!(item.dtype(), A => A::of(item).expect("a scalar slot has its argument's dtype")[0].slot()); } let mut c = lowered.scalars.len(); if let Some(runs) = runs { @@ -875,78 +884,48 @@ impl Blocks { Some((arrays, scalars, places, a, c)) } - /// Each input's items for the run of `n` items from item `from`: where `read_placed` places them, or copied into `copies` (`copy_into`), which `•explain` counts. Where `tabled` holds, the run's compiled parts read each tabled input run by run (`arguments`), and its entry holds no items. Each input that isn't tabled comes from `got`, a placement of the same run, where it has an entry. - fn placed<'a>(&self, run: &'a Run<'_>, copies: &'a mut [Buffer], from: usize, n: usize, tabled: bool, got: &[Got<'a>]) -> Vec> { - let placed: Vec>> = run.inputs.iter().zip(&self.inputs).enumerate() - .map(|(k, (&d, x))| got.get(k).copied().filter(|_| !matches!(run.reads[k], Reading::Tabled(_))).or_else(|| read_placed(d, &x.map, from, n))) - .collect(); - let skips = |k: usize| tabled && matches!(run.reads[k], Reading::Tabled(_)); - for (k, p) in placed.iter().enumerate() { - if p.is_none() && !skips(k) { - copy_into(run.inputs[k], &self.inputs[k].map, from, n, &mut copies[k]); - run.tally.copied(n); - } - } - let copies: &'a [Buffer] = copies; - placed.into_iter().enumerate().map(|(k, p)| p.unwrap_or_else(|| Got::of(&copies[k], if skips(k) { 0 } else { n }))).collect() + /// Geometry-normalized spans for the compiled ABI, at their actual storage dtype. Native steps use the same reads directly. + #[cfg(not(web))] + fn prepared<'a>(&self, run: &'a Run<'_>, copies: &'a mut [Buffer], from: usize, n: usize) -> Option>> { + run.inputs.iter().zip(run.plans).zip(copies).enumerate().map(|(k, ((source, plan), copy))| { + if matches!(run.reads[k], Reading::Tabled) { return Some(source.view); } + let read = rustymath::agreement::Read { sources: std::slice::from_ref(source), plan }; + let range = from..from + n; + let (view, copied) = read.argument(range, false, copy).ok()?; + run.tally.copied(copied); + Some(view) + }).collect() } - /// Runs `step` on a run's `n` items into `dest`, where `got` holds the run's input items, `bufs` the earlier steps' items and `constants` the step's constants at its type. Gives false where its kernel fails. - #[inline] + /// Bind this run's input geometry, constant items and earlier chunk windows; RM reads, converts and writes them. fn step( - &self, step: &BlockStep, got: &[Got<'_>], bufs: &[Buffer], constants: &[Option>; 3], scratch: &mut Scratch, n: usize, dest: Dest<'_, '_, '_> - ) -> bool { with_item!(step.at, A => self.step_at::(step, got, bufs, constants, scratch, n, dest)) } - - /// `step` at its type `A`, with each operand read as `A`, converted into `scratch` where it has another type. - fn step_at( - &self, step: &BlockStep, got: &[Got<'_>], bufs: &[Buffer], constants: &[Option>; 3], scratch: &mut Scratch, n: usize, dest: Dest<'_, '_, '_> + &self, step: &BlockStep, run: &Run<'_>, bufs: &[Buffer], constants: &[Option; 3], scratch: &mut Scratch, + from: usize, n: usize, dest: MapOutput<'_, '_> ) -> bool { - let buffers = A::scratch(scratch); - let [b0, b1, b2] = buffers.each_mut(); - let read = |k: usize, buf| step.operands[k].map(|o| self.arg::(o, constants[k], got, bufs, n, buf, step.tolerance)); - let (x, y, m) = (read(0, b0), read(1, b1).expect("every step reads its last operand"), read(2, b2)); - if let (Op::Map(plan), Some(kernel)) = (&step.op, &step.kernel) { - return mapped(kernel, x, y, n, plan.numeric.settings_for(plan.computes().expect("a step with a kernel has its computing kind")), dest); - } - match dest { - Dest::Buffer(Buffer::Booleans(v)) => Slots::append(v, n, |s| wrote(&step.op, x, y, m, n, Out::Bools(s))), - Dest::Buffer(buffer) => Slots::append(A::items(buffer), n, |s| wrote(&step.op, x, y, m, n, Out::Items(s))), - Dest::Last(last) => wrote(&step.op, x, y, m, n, A::out(last)), - } - } - - /// Operand `o` as `A` for a run of `n` items: the constant that `constant` holds at type `A`, an input's items, or an earlier step's buffer, converted into `buf` where they have another type, within `t`, the tolerance of a float's floor. - fn arg<'a, A: Item>( - &self, o: Operand, constant: Option>, got: &[Got<'a>], bufs: &'a [Buffer], n: usize, buf: &'a mut Vec, t: Tolerance) -> Arg<'a, A> { - match o { - Operand::Constant(_) | Operand::Identity(_) => A::read(constant.expect("a run converts each constant to its step's type"), n, buf, t), - Operand::Input(k) => A::read(got[k], n, buf, t), - Operand::Step(s) => A::read(Got::of(&bufs[self.slots[s].expect("a step that another reads has a buffer")], n), n, buf, t), + let mut bindings = [None; 3]; + let mut reads = [rustymath::agreement::Read { sources: &[], plan: run.direct }; 3]; + let mut count = 0; + for ((binding, operand), constant) in bindings.iter_mut().zip(&step.operands).zip(constants) { + let Some(operand) = operand else { continue }; + let (source, plan): (&Binding<'_>, &ReadPlan) = match *operand { + Operand::Input(i) => (&run.inputs[i], &run.plans[i]), + Operand::Step(s) => { + let buffer = &bufs[self.slots[s].expect("an earlier step that is read has a buffer")]; + let Ok(source) = Binding::window(buffer.view().slice(0..n), from, self.n) else { return false }; + (binding.insert(source), run.direct) + } + Operand::Constant(_) | Operand::Identity(_) => { + let value = constant.as_ref().expect("a run prepares its constants").view().expect("a prepared scalar has stored items"); + (binding.insert(Binding::full(value)), run.single) + } + }; + reads[count] = rustymath::agreement::Read { sources: std::slice::from_ref(source), plan }; + count += 1; } + step.map.run(&reads[count - step.map.arity()..count], from..from + n, scratch, dest).is_ok() } } -/// Writes the results of a conversion or select step on its operands' `n` items to `out`. -fn wrote(op: &Op, x: Option>, y: Arg<'_, A>, m: Option>, n: usize, out: Out<'_, '_, A>) -> bool { - match (op, y) { - (Op::Convert, Arg::Many(items)) => out.items().is_some_and(|o| { o.copy(&items[..n]); true }), - (Op::Convert, c @ Arg::One(_)) => out.items().is_some_and(|o| o.pairs(c, Arg::One(()), n, |c, ()| (c, true))), - (Op::Select, y) => out.items().is_some_and(|o| A::choose(x.expect("a select reads two cases"), y, m.expect("a select reads its mask"), n, o)), - (Op::Map(_), _) => unreachable!("a map step runs its kernel (`mapped`)"), - } -} - -/// Runs `kernel` on its arguments' `n` items into `dest`: a buffer it appends them to, or the stage's last slots. The arguments are the last of `x` and `y` that the kernel takes, as a code-picked `○` takes `y` alone. Each reads its run in place, or its one item for every position. The kernel casts its results to `dest`'s type, so a comparison writes 1 and 0 where `dest` holds numbers. Gives false where the kernel fails. -fn mapped(kernel: &Kernel, x: Option>, y: Arg<'_, A>, n: usize, settings: Settings, dest: Dest<'_, '_, '_>) -> bool { - use rustymath::operation::MapOutput; - if kernel.arity() > 1 + usize::from(x.is_some()) { return false; } - let args = [x.unwrap_or(y), y]; - let view = |k: usize| match &args[k] { Arg::Many(items) => A::view(&items[..n]), Arg::One(v) => A::view(std::slice::from_ref(v)) }; - let inputs = [view(0), view(1)]; - let output = match dest { Dest::Buffer(buffer) => MapOutput::Buffer(buffer), Dest::Last(last) => last.output() }; - kernel.map_ready(&inputs[2 - kernel.arity()..], n, settings, output).is_ok() -} - /// The buffer that each step writes for a run, by liveness, the type of each buffer, and the number of buffers that the steps would take if every buffer had one type, which sizes the blocks. A step's buffer holds a later step's items, of the same type, once every step that reads it has run. The last step writes straight into its value, or into the block that the fold reads, and has none. A line of steps therefore takes two buffers. fn buffers(steps: &[BlockStep]) -> (Vec>, Vec, usize) { let mut last = vec![0; steps.len()]; @@ -974,23 +953,23 @@ fn buffers(steps: &[BlockStep]) -> (Vec>, Vec, usize) { /// How a block stage reads an input in blocks of a given size. #[derive(Clone, Copy)] enum Reading { - /// In place, run by run. A run ends at each multiple of the period, where there is one. + /// Compiled runs read in place and end at each multiple of the period, where there is one. Runs(Option), /// From its period, written out once to cover a block at any phase: a cycle shorter than a block. Cycle, /// Copied into a buffer, run by run. Copied, - /// In place, run by run, where compiled parts read each run from a table of the runs' first items, in one call for each block (`Mapping::runs`). A run that no compiled part takes copies, except where the input repeats each item over `Some(r)` positions: such a run ends at each repeat and reads its item there, as `Runs` does. + /// In place, run by run, where compiled parts read each run from a table of its first items, in one call for each block (`Mapping::runs`). Native kernels read through their maps. #[cfg_attr(web, expect(dead_code, reason = "the browser build compiles nothing"))] - Tabled(Option), + Tabled, } impl Reading { - /// The period at each multiple of which a run ends, in a run that uses compiled parts where `compiled` holds: a repeat or long cycle that runs read in place (`Runs`), and a tabled repeat where no compiled part reads it. - fn ends(self, compiled: bool) -> Option { match self { Self::Runs(r) => r, Self::Tabled(r) if !compiled => r, _ => None } } + /// The period at which a compiled run ends to read a repeat or long cycle in place. Native kernels split their own reads. + fn ends(self, compiled: bool) -> Option { match self { Self::Runs(r) if compiled => r, _ => None } } } -/// How a block stage reads an input through `m` in blocks of `block` items. Runs end at each repeat of at least `run` items (`Mapping::repeats`) and each cycle of at least `block` items, so that each run reads one repeat, or a stretch of one cycle, in place. A shorter repeat copies. A map of steps copies where `Mapping::copies` says most blocks would. +/// How a block stage reads an input through `m` in blocks of `block` items. Compiled runs end at each repeat of at least `run` items (`Mapping::repeats`) and each cycle of at least `block` items, so that each reads one repeat, or a stretch of one cycle, in place. A shorter repeat copies. A map of steps copies where `Mapping::copies` says most blocks would. fn reading(m: &Mapping, block: usize, run: usize) -> Reading { if let Some(r) = m.repeats().filter(|&r| r >= run) { return Reading::Runs(Some(r)); } match *m { @@ -1000,8 +979,11 @@ fn reading(m: &Mapping, block: usize, run: usize) -> Reading { } /// What every range of a stage's run reads: the inputs' items, each step's constants at its type, how each input reads a block (`reading`), the block size, the tally that `•explain` reads, and whether the run can use the stage's compiled parts (`Blocks::compiles`). -struct Run<'a> { inputs: Vec>, constants: Cow<'a, [[Option>; 3]]>, reads: &'a [Reading], block: usize, tally: &'a Tally, compiled: bool } +struct Run<'a> { + inputs: Vec>, plans: &'a [ReadPlan], direct: &'a ReadPlan, single: &'a ReadPlan, constants: Cow<'a, [[Option; 3]]>, + reads: &'a [Reading], block: usize, tally: &'a Tally, compiled: bool +} -/// How a stage splits its items under `sizes`: its block size (`Blocks::block`), and how each input reads a block of that size (`reading`). +/// How a stage splits its items under `sizes`, with input reads planned for that block size. #[derive(Clone)] -struct Laid { sizes: crate::settings::Sizes, block: usize, reads: Vec } +struct Laid { sizes: crate::settings::Sizes, block: usize, reads: Vec, plans: Vec } diff --git a/src/program/blocks/grouping.rs b/src/program/blocks/grouping.rs index 246eaaaa..b68b16e4 100644 --- a/src/program/blocks/grouping.rs +++ b/src/program/blocks/grouping.rs @@ -92,11 +92,11 @@ impl Group { let items = node.desc.layout.count(); if node.desc.atom || self.items.is_some_and(|n| n != items) { return false; } let fits = match step { - Step::Map { left, right, plan } => computes(plan, node.desc.dtype()).is_some() && self.reads_paired(program, left.as_ref(), right), - Step::Convert { of, .. } => readable(node.desc.dtype()) && self.reads(program, of, &Mapping::Linear(1)) && !read_through(program, readers, j), + Step::Map { plan, .. } => computes(plan, node.desc.dtype()).is_some() && step.operands().all(|r| self.reads(program, r)), + Step::Convert { of, .. } => readable(node.desc.dtype()) && self.reads(program, of) && !read_through(program, readers, j), Step::Select { mask, yes, no } => { readable(node.desc.dtype()) && program.nodes[mask.of].desc.dtype() == DType::Boolean - && [mask, yes, no].into_iter().all(|r| self.reads(program, r, &Mapping::Linear(1))) + && [mask, yes, no].into_iter().all(|r| self.reads(program, r)) } Step::Copy { of, .. } => !self.nodes.contains(of) && input(program, *of), _ => false, @@ -108,15 +108,10 @@ impl Group { } fits } - /// Whether a step of the group can read `read` through `map`, as `Blocks::operand` reads it: a step of the group only item for item, and a copy in the group, an atom or an array of a type that a stage holds (`readable`) through any map. - fn reads(&self, program: &Program, read: &Read, map: &Mapping) -> bool { + /// Whether a normalized pointwise operand reads an earlier step, an atom or an array of a type that a stage holds. + fn reads(&self, program: &Program, read: &Read) -> bool { let node = &program.nodes[read.of]; - if !self.nodes.contains(&read.of) { return node.desc.atom || readable(node.desc.dtype()); } - match node.origin { Origin::Step(Step::Copy { .. }) => true, _ => { let n = node.desc.layout.count(); map.in_order(n, &[n]) } } - } - /// Whether a step of the group can read `left`, where it's dyadic, and `right` through the maps that agreement gives them (`reads`). - fn reads_paired(&self, program: &Program, left: Option<&Read>, right: &Read) -> bool { - agreement(left, right).is_some_and(|a| left.is_none_or(|r| self.reads(program, r, &a.left)) && self.reads(program, right, &a.right)) + self.nodes.contains(&read.of) || node.desc.atom || readable(node.desc.dtype()) } /// Adds the group's nodes to `units`, leaving the group empty: a block stage where the group forms one (`stages`), or each node on its own. A copy that a node outside the stage reads also runs on its own before the stage, for that node. A shape with no items takes its nodes one at a time, where a fold of no items gives the reduction's unit, such as 0 for a sum, rather than its identity, -0.0, at which a stage's fold starts. fn finish(&mut self, program: &Program, readers: &[Vec], units: &mut Vec) { @@ -141,11 +136,6 @@ pub(crate) fn readable(dtype: DType) -> bool { matches!(dtype, DType::Boolean | /// Whether node `i` is an array that a block stage reads as an input. fn input(program: &Program, i: usize) -> bool { !program.nodes[i].desc.atom && readable(program.nodes[i].desc.dtype()) } -/// How the operands of a map step read over its result: `left`, where the step is dyadic, and `right`. -pub(super) fn agreement(left: Option<&Read>, right: &Read) -> Option { - Agreement::new(left.map_or(&Layout::default(), |r| &r.layout), &right.layout).ok() -} - /// Whether node `j` is a conversion that only inner products read, each of which reads its operand through it (`pervasive::reads_widened`). A replay leaves it pending for them (`Unit::Pending`), so that its converted items are never stored. fn read_through(program: &Program, readers: &[Vec], j: usize) -> bool { let Origin::Step(Step::Convert { ref of, .. }) = program.nodes[j].origin else { return false }; @@ -156,16 +146,7 @@ fn read_through(program: &Program, readers: &[Vec], j: usize) -> bool { /// Whether a block stage folds items of `dtype` by `p` a block at a time (`reduction`): floats by `+`, `×`, `⌈` and `⌊`, integers by `⌈` and `⌊`, and by `+` where `result`'s integers wrap (`Numeric::wraps`), and Booleans by the reductions their truth tables name. fn folds(p: Primitive, dtype: DType, result: &Desc) -> bool { - let fold = reduction(p, dtype); - match dtype { - DType::Float(FloatWidth::F32 | FloatWidth::F64) => matches!(fold, Some(Reduce::Sum | Reduce::Product | Reduce::Max | Reduce::Min)), - DType::Integer(_) => { - matches!(fold, Some(Reduce::Max | Reduce::Min)) - || Numeric::current().wraps(result.kind.dtype, result.kind.fixed) && matches!(fold, Some(Reduce::Sum)) - } - DType::Boolean => matches!(fold, Some(Reduce::Sum | Reduce::Min | Reduce::Max | Reduce::Xor | Reduce::Xnor)), - _ => false, - } + reduction(p, dtype).is_some_and(|f| f.stage(dtype, Numeric::current().wraps(result.kind.dtype, result.kind.fixed))) } /// Whether a block stage folds items of `dtype` by `p` through rustymath's `Alternated`, a block at a time: floats by `-`, and by `÷` through `ScaledProduct`, whose running quotient keeps its exponent apart, so it stays in range however many blocks it takes. No compiled part folds this way, and `short_window` takes only the reductions of `folds`. @@ -176,14 +157,7 @@ fn alternates(p: Primitive, dtype: DType) -> bool { /// Whether a stage's fold by `p` of items of `items` takes a seed of type `seed` into a result of type `result`, combining the seed with each total at the end (`Blocks::folded`): a fold of floats with a float seed, whose result keeps the items' width, of integers with an integer seed, by `⌈` or `⌊`, or by `+` into a result of the items' width, and a count of Booleans by `+` with an integer seed. A sum of integers into a wider integer folds after the stage, because a compiled part sums integers at the items' width. fn seeds(p: Primitive, items: DType, seed: DType, result: DType) -> bool { - match (items, seed, result) { - (DType::Float(a), DType::Float(_), DType::Float(b)) => a == b, - (DType::Integer(_), DType::Boolean | DType::Integer(_), DType::Integer(_)) => match reduction(p, items) { - Some(Reduce::Max | Reduce::Min) => true, Some(Reduce::Sum) => items == result, _ => false - }, - (DType::Boolean, DType::Boolean | DType::Integer(_), DType::Integer(_)) => matches!(reduction(p, items), Some(Reduce::Sum)), - _ => false, - } + reduction(p, items).is_some_and(|f| f.seeds(items, seed, result)) } /// The reduction by which a stage folds items of `dtype` by `p`: the fold of `p`'s kernel there (`Kernel::fold`), which for Booleans is the one their truth table names. diff --git a/src/program/blocks/items.rs b/src/program/blocks/items.rs index d122624b..1b637e4c 100644 --- a/src/program/blocks/items.rs +++ b/src/program/blocks/items.rs @@ -1,79 +1,21 @@ -//! The typed items of a stage's runs: what a run reads (`Got`), where a step writes (`Dest`, `Last`, `Out`), and the types a stage computes at (`Item`). +//! Stage output bindings and compiled scalar-slot packing. use super::*; /// Where a stage's last step writes each run: the slots of its stored value, of the value's type, or the fold, which gets each block with the block's position. pub(super) enum Last<'a, 'b> { - F32(&'a mut Slots<'b, f32>), F64(&'a mut Slots<'b, f64>), Bool(&'a mut Slots<'b, bool>), U8(&'a mut Slots<'b, u8>), I16(&'a mut Slots<'b, i16>), - I32(&'a mut Slots<'b, i32>), I64(&'a mut Slots<'b, i64>), Fold(&'a mut dyn FnMut(usize, Folded<'_>)) + Output(MapOutput<'a, 'b>), Fold(&'a mut dyn FnMut(usize, Folded<'_>)) } impl<'a, 'b> Last<'a, 'b> { - pub(super) fn output(&mut self) -> rustymath::operation::MapOutput<'_, 'b> { - use rustymath::operation::MapOutput; - match self { - Last::F32(s) => MapOutput::F32(s), Last::F64(s) => MapOutput::F64(s), Last::Bool(s) => MapOutput::Bool(s), Last::U8(s) => MapOutput::U8(s), - Last::I16(s) => MapOutput::I16(s), Last::I32(s) => MapOutput::I32(s), Last::I64(s) => MapOutput::I64(s), - Last::Fold(_) => unreachable!("a step writes a fold's block into a buffer") - } + pub(super) fn output(&mut self) -> rustymath::buffer::MapOutput<'_, 'b> { + match self { Last::Output(output) => output.reborrow(), Last::Fold(_) => unreachable!("a step writes a fold's block into a buffer") } } } -/// Where a conversion or select step writes the stage's last slots: items of its type, or Booleans. -pub(super) enum Out<'a, 'b, A> { Items(&'a mut Slots<'b, A>), Bools(&'a mut Slots<'b, bool>) } - -impl<'a, 'b, A: Item> Out<'a, 'b, A> { - /// The slots as items of type `A`: its own items, or the Booleans of a step on Booleans (`Item::truths`). - pub(super) fn items(self) -> Option<&'a mut Slots<'b, A>> { match self { Out::Items(o) => Some(o), Out::Bools(o) => A::truths(o) } } -} - /// What a stage's fold gets for each block: the block that the last step wrote, or the total of the block's `n` items as its 8-byte slot holds it, which a compiled part folded as the fold folds a block (`Blocks::over`). pub(super) enum Folded<'a> { Held(&'a Buffer), #[cfg_attr(web, expect(dead_code, reason = "the browser build compiles nothing"))] Total(usize, u64) } -/// Where a step writes a run's items: appended to a buffer, or to the stage's last slots. -pub(super) enum Dest<'a, 'b, 'c> { Buffer(&'a mut Buffer), Last(&'a mut Last<'b, 'c>) } - -/// The items of `input` that `map` gathers once before a stage's run (`Input::gather`): its cycle, written out over `cover` positions where it's shorter (`tiled_cycle`), and otherwise every item, in one walk, which reads a map that tiles whole through the tiled transpose (`Steps::tiles_whole`). -pub(super) fn gathered_once(input: View<'_>, map: &Walked<'_>, cover: usize) -> Buffer { - with_item!(input.dtype(), A => { - let mut items = Vec::new(); - if tiled_cycle(A::held(input), map, cover, &mut items).is_none() && let Mapping::Steps { steps, .. } = &**map { - let n = Steps::count(steps); - items.reserve(n); - expanded_items(map, A::held(input), 0, n, &mut items); - } - A::buffer(items) - }) -} - -/// Items `from..from + n` of `input` that `map` lays over the stage's items, where they need no copy (`in_place`): in place, or from a cycle that `gathered_once` wrote out. `None` where they must be copied (`copy_into`). -pub(super) fn read_placed<'a>(input: View<'a>, map: &Walked<'_>, from: usize, n: usize) -> Option> { - with_item!(input.dtype(), A => in_place(map, A::held(input), from, n).map(|arg| A::got(arg))) -} - -/// Copies items `from..from + n` of `input` that `map` lays over the stage's items into `copy`, a buffer of their type. -pub(super) fn copy_into(input: View<'_>, map: &Walked<'_>, from: usize, n: usize, copy: &mut Buffer) { - with_item!(input.dtype(), A => { let _ = expanded(map, A::held(input), from, n, A::items(copy)); }) -} - -/// A run's items of an input or a buffer, at their own type. -#[derive(Clone, Copy)] -pub(super) enum Got<'a> { F32(Arg<'a, f32>), F64(Arg<'a, f64>), Bool(Arg<'a, bool>), U8(Arg<'a, u8>), I16(Arg<'a, i16>), I32(Arg<'a, i32>), I64(Arg<'a, i64>) } - -impl<'a> Got<'a> { - /// The first `n` items of `buffer`. - pub(super) fn of(buffer: &'a Buffer, n: usize) -> Self { with_item!(buffer.dtype(), A => A::got(Arg::Many(&A::held(buffer.view())[..n]))) } - /// The items' type. - #[cfg(not(web))] - pub(super) fn dtype(&self) -> DType { - match self { - Got::F32(_) => DType::Float(FloatWidth::F32), Got::F64(_) => DType::Float(FloatWidth::F64), Got::Bool(_) => DType::Boolean, - Got::U8(_) => DType::Integer(Width::U8), Got::I16(_) => DType::Integer(Width::I16), Got::I32(_) => DType::Integer(Width::I32), - Got::I64(_) => DType::Integer(Width::I64) - } - } -} - /// Writes items `at..at + n` of `buffer` to `out`, from position `start`, as bytes of their type. SAFETY: `out` must have room for `start + n` items of the buffer's type, and no other call may write those items meanwhile. pub(super) unsafe fn write(buffer: &Buffer, at: usize, out: &Shared, start: usize, n: usize) { let size = buffer.dtype().bytes(); @@ -81,9 +23,14 @@ pub(super) unsafe fn write(buffer: &Buffer, at: usize, out: &Shared, start: usiz unsafe { std::ptr::copy_nonoverlapping(buffer.view().as_ptr().add(at * size), out.0.add(start * size), n * size) } } -/// The array of `shape` that `buffer` holds, as fixed integers where `fixed` says. -pub(super) fn built(buffer: Buffer, layout: &Layout, fixed: bool) -> Option { - Value::from_storage(layout.clone(), Storage::Compact(buffer).fixed(fixed)).ok() +/// Publishes the buffer with its node's atom, kind and layout. +pub(super) fn built(buffer: Buffer, desc: &Desc) -> Option { + let value = Value::from_storage(desc.layout.clone(), Storage::Compact(buffer).fixed(desc.kind.fixed.is_some())).ok()?; + Some(published(value, desc)) +} + +pub(super) fn published(value: Value, desc: &Desc) -> Value { + if desc.atom { Value::Number(value.as_number().expect("a described atom is numeric").marked(desc.kind.weak)) } else { value } } /// A stored step's room for the stage's items, which the stage's ranges write in parallel, each its own items. @@ -93,87 +40,21 @@ unsafe impl Send for Shared {} // SAFETY: as for `Send`. unsafe impl Sync for Shared {} -/// The buffers into which a step converts its operands, for each type that a step computes at. -#[derive(Default)] -pub(super) struct Scratch { - f32s: [Vec; 3], f64s: [Vec; 3], bools: [Vec; 3], u8s: [Vec; 3], i16s: [Vec; 3], i32s: [Vec; 3], i64s: [Vec; 3] -} - -/// A type that a stage's buffers hold and its steps compute at: floats of 32 or 64 bits, Booleans, or integers. -pub(super) trait Item: Stored + Sync { - /// The items of `view`, which have this type. - fn held(view: View<'_>) -> &[Self] { Self::of(view).expect("a stage's items have their step's type") } - /// The vector of `buffer`, which holds this type. - fn items(buffer: &mut Buffer) -> &mut Vec { Self::vec_mut(buffer).expect("a buffer holds its step's type") } - /// `arg` as a run's items. - fn got(arg: Arg<'_, Self>) -> Got<'_>; - /// The buffers into which this type converts its operands. - fn scratch(s: &mut Scratch) -> &mut [Vec; 3]; - /// `slots`, the slots of a stage's value of this type, as where its last step writes. +/// Types held by a stage's buffers and final slots. +pub(super) trait Item: Stored + rustymath::buffer::OutputItem + Sync { fn last<'a, 'b>(slots: &'a mut Slots<'b, Self>) -> Last<'a, 'b>; - /// Where a step of this type writes the stage's last slots: items of this type, or a comparison's Booleans. - fn out<'a, 'b>(last: &'a mut Last<'_, 'b>) -> Out<'a, 'b, Self>; - /// `got`'s `n` items as this type, converted into `buf` where they have another type, as `Converts` converts them within `t`, the tolerance of a float's floor. - fn read<'a>(got: Got<'a>, n: usize, buf: &'a mut Vec, t: Tolerance) -> Arg<'a, Self>; - /// The number `n` as this type, where it converts. - fn constant(n: &Number) -> Option; - /// The item as a compiled kernel's 8-byte slot holds it (`jit::Compiled::run`), as a scalar or a fold's total, which `rustymath::reduce::Total` reads back: a float as an f64, an integer extended to 64 bits, and a Boolean as 0 or 1. #[cfg(not(web))] fn slot(self) -> u64; - /// Writes `x` where `mask` isn't zero and `y` where it is into the next `n` slots of `out`, through rustymath's `Choose`, which has a kernel at every type. - fn choose(x: Arg<'_, Self>, y: Arg<'_, Self>, mask: Arg<'_, Self>, n: usize, out: &mut Slots<'_, Self>) -> bool; - /// The slots of a Boolean result as slots of this type, which only `bool` gives: a step on Booleans writes a comparison's results as its own items. - fn truths<'a, 'b>(_slots: &'a mut Slots<'b, bool>) -> Option<&'a mut Slots<'b, Self>> { None } } -/// Writes `Item` for each number type listed, with its variant of `Got` and `Last`, its field of `Scratch`, how a constant converts to it, and how it fills a kernel's slot, as a float or an integer (`@slot`). A step of a number type reads items of every other type that a stage holds through `Converts`. `@common` writes the methods that Booleans share. macro_rules! items { - (@common $v:ident, $s:ident) => { - fn got(arg: Arg<'_, Self>) -> Got<'_> { Got::$v(arg) } - fn scratch(s: &mut Scratch) -> &mut [Vec; 3] { &mut s.$s } - fn last<'a, 'b>(slots: &'a mut Slots<'b, Self>) -> Last<'a, 'b> { Last::$v(slots) } - fn choose(x: Arg<'_, Self>, y: Arg<'_, Self>, mask: Arg<'_, Self>, n: usize, out: &mut Slots<'_, Self>) -> bool { rustymath::Ternary::many_into(&rustymath::Choose, x, y, mask, n, out) } - }; - (@slot float) => { - #[cfg(not(web))] - fn slot(self) -> u64 { jit::float_slot(f64::from(self)) } - }; - (@slot int) => { - #[cfg(not(web))] - fn slot(self) -> u64 { jit::integer_slot(i64::from(self)) } - }; - ($($t:ty => $v:ident, $s:ident, |$n:ident| $constant:expr, $k:ident);+) => {$( + (@slot float) => { #[cfg(not(web))] fn slot(self) -> u64 { jit::float_slot(f64::from(self)) } }; + (@slot int) => { #[cfg(not(web))] fn slot(self) -> u64 { jit::integer_slot(i64::from(self)) } }; + ($($t:ty => $k:ident);+ $(;)?) => {$( impl Item for $t { - items!(@common $v, $s); - fn out<'a, 'b>(last: &'a mut Last<'_, 'b>) -> Out<'a, 'b, Self> { - match last { Last::$v(s) => Out::Items(&mut **s), Last::Bool(s) => Out::Bools(&mut **s), _ => unreachable!("a step writes items of its type or Booleans") } - } - fn read<'a>(got: Got<'a>, n: usize, buf: &'a mut Vec, t: Tolerance) -> Arg<'a, Self> { - if let Got::$v(arg) = got { return arg; } - rustymath::with_tolerance!(t, |t| typed!(got, Got, |x| x.converted(0, n, buf, |v| v.converted(t)))) - } - fn constant($n: &Number) -> Option { $constant } + fn last<'a, 'b>(slots: &'a mut Slots<'b, Self>) -> Last<'a, 'b> { Last::Output(slots.into()) } items!(@slot $k); } )+}; } -items!( - f32 => F32, f32s, |n| n.to_float().ok().map(|x| x.converted(Exactly)), float; f64 => F64, f64s, |n| n.to_float().ok(), float; - u8 => U8, u8s, |n| n.marked_exact()?.as_integer().map(|i| i.converted(Exactly)), int; - i16 => I16, i16s, |n| n.marked_exact()?.as_integer().map(|i| i.converted(Exactly)), int; - i32 => I32, i32s, |n| n.marked_exact()?.as_integer().map(|i| i.converted(Exactly)), int; i64 => I64, i64s, |n| n.marked_exact()?.as_integer(), int -); -impl Item for bool { - items!(@common Bool, bools); - fn out<'a, 'b>(last: &'a mut Last<'_, 'b>) -> Out<'a, 'b, Self> { - match last { Last::Bool(s) => Out::Bools(&mut **s), _ => unreachable!("a step on Booleans writes Booleans") } - } - /// A step on Booleans reads only Booleans: its plan computes at Booleans only where every operand is Boolean, and a comparison that it reads keeps its Booleans (`Blocks::new`). - fn read<'a>(got: Got<'a>, _: usize, _: &'a mut Vec, _: Tolerance) -> Arg<'a, Self> { - match got { Got::Bool(arg) => arg, _ => unreachable!("a step on Booleans reads Booleans") } - } - fn constant(n: &Number) -> Option { n.as_bool() } - fn truths<'a, 'b>(slots: &'a mut Slots<'b, bool>) -> Option<&'a mut Slots<'b, Self>> { Some(slots) } - #[cfg(not(web))] - fn slot(self) -> u64 { jit::integer_slot(self.into()) } -} +items!(f32 => float; f64 => float; u8 => int; i16 => int; i32 => int; i64 => int; bool => int); diff --git a/src/program/blocks/record.rs b/src/program/blocks/record.rs index 930bc25c..8c90526c 100644 --- a/src/program/blocks/record.rs +++ b/src/program/blocks/record.rs @@ -150,7 +150,7 @@ impl rustymath::facts::Visit for Emitted<'_, '_, G> { } } -/// Whether a compiled kernel converts items of type `from` to `to` as a step whose tolerance is `tolerance` reads them (`Converts`): to any type but Booleans, and from a float to an integer only where the tolerance is exact, since the kernel floors exactly. +/// Whether a compiled kernel converts items of type `from` to `to` as a step whose tolerance is `tolerance` reads them (`Buffer::floored`): to any type but Booleans, and from a float to an integer only where the tolerance is exact, since the kernel floors exactly. pub(super) fn converts(from: DType, to: DType, tolerance: Tolerance) -> bool { from == to || (to != DType::Boolean && (matches!(to, DType::Float(_)) || !matches!(from, DType::Float(_)) || tolerance.is_exact())) } diff --git a/src/program/contraction.rs b/src/program/contraction.rs index 40e5551d..18db333b 100644 --- a/src/program/contraction.rs +++ b/src/program/contraction.rs @@ -1,7 +1,7 @@ //! Folds that read their argument's sources instead of computing it. A `+` fold of a `×` whose operands read their sources through affine maps runs as one product, which never computes the grid of products (`contracted`): index notation's level 3. A fold of a copy that repeats items, as a broadcast does, folds the copy's source, which never computes the copy (`fold_of_copy`). Fusion's pending programs and the trace both call each, so the two routes find the same folds. -use super::{Laid, Node, Nodes, Origin, Read, Step, along, laid_out, stepped}; -use crate::{agreement::{Agree, Agreement}, array::Layout, number::{Arithmetic, Numeric}, plan::{Desc, Operand}, primitive::{FoldKind, Primitive}}; +use super::{Laid, Node, Nodes, Origin, Read, Step, along, laid_out, forward_axes}; +use crate::{array::Layout, number::{Arithmetic, Numeric}, plan::{Desc, Operand}, primitive::{FoldKind, Primitive}}; use rustymath::{agreement::Mapping, gather::Steps, layout::{count, strides}}; /// The read that an unseeded fold by `p` along `axes` of `of` folds through in place of a `Step::Fold`: one product where the fold is a `+` of a `×` (`contracted`), or the source of a copy that repeats items, along `axes[0]` (`fold_of_copy`). `desc` is the fold's description along one axis. Recording and fusion both route a fold through here. `None`, adding nothing, where neither applies. @@ -17,7 +17,7 @@ pub(crate) fn contracted(nodes: &mut Nodes, p: Primitive, kind: FoldKind, of: &R let layout = of.layout.axes((0..of.layout.shape().len()).filter(|a| !axes.contains(a))); let result = Desc { atom: layout.shape().is_empty(), layout, ..one.clone() }; product(nodes, &x, &y, numeric, kind, of, axes, &result) - .inspect_err(|reason| rustymath::events::declined(rustymath::events::Route::Contraction, reason, items)).ok() + .inspect_err(|reason| crate::events::declined(crate::events::Route::Contraction, reason, items)).ok() } /// The read of new nodes in `nodes` that compute a `+` fold along `axes` of `of`, which reads `x×y` planned under `numeric`, as one product, or the reason that no product computes it. The fold runs along the axes of the `×`'s frame that step within `axes` (`within`): each is one axis of the frame, or where `of` is a view, a run of adjacent axes that it merges, as a fold along several adjacent axes reads them (`eval::folded_along`). The view merges no other axes. Both operands' sources must step along each folded axis through affine maps, except that an operand that holds one item along each folded axis factors out of the fold (`factored`). Each other axis of the frame is a batch axis where both operands step along it, a row where only the left one does, and a column where only the right one does. An axis of one item is a row. The rows or the columns must hold more than one item, because a product reuses items only along them: a fused multiply and fold, which reads each item once, costs less for `+/a×b` on arrays of one shape. A copy lays out the left operand's source as batch, row and folded axes, and the right operand's as batch, folded and column axes, or a view does where the map reads the source in order. The operands swap, and the rows with the columns, where the swapped order needs fewer copies. A product contracts them (`Step::Product`), stacked where batch axes remain, with the rows as one axis, the folded axes as one and the columns as one, and `•explain` lists it as a fast combination. A copy lays the product out as `result`, the fold's description, whose type the product's must be. @@ -31,10 +31,8 @@ fn product(nodes: &mut Nodes, x: &Read, y: &Read, numeric: Numeric, kind: FoldKi // A view merges only folded axes, so that the fold's result keeps the frame's other axes as they are. let kept: Vec = (0..frame.len()).filter(|a| !folded.contains(a)).collect(); if result.layout.shape() != lens(&kept).as_slice() { return Err("a view of the products"); } - let agreement = - Agreement::new(&x.layout, &y.layout).ok().filter(|a| a.layout.shape() == frame.as_slice()).ok_or("operands that don't pair as the products do")?; - let (mut left, mut right) = - Source::of(nodes, x, &agreement.left, &frame).zip(Source::of(nodes, y, &agreement.right, &frame)).ok_or("a read that isn't affine")?; + let (mut left, mut right) = Source::of(nodes, x, &x.pointwise(nodes), &frame) + .zip(Source::of(nodes, y, &y.pointwise(nodes), &frame)).ok_or("a read that isn't affine")?; // An operand that holds one item along each folded axis factors out of the fold (`factored`). let constant = |s: &Source| folded.iter().all(|&a| frame[a] == 1 || s.strides[a] == 0); match (constant(&left), constant(&right)) { @@ -122,18 +120,18 @@ fn factored( let (xd, yd) = (x.desc(nodes), y.desc(nodes)); let held = match &nodes[y.of].origin { Origin::Held(v) if yd.atom => Some(v.clone()), _ => None }; let yo = match &held { Some(v) => Operand::Held(v), None => Operand::Traced(&yd) }; - let mapped = crate::plan::map(Primitive::Arithmetic(Arithmetic::Times), Some(&Operand::Traced(&xd)), &yo, false, numeric)?; + let mapped = crate::plan::map(Primitive::Arithmetic(Arithmetic::Times), Some(&Operand::Traced(&xd)), &yo, false, numeric, None)?; if mapped.desc.dtype() != result.dtype() || mapped.desc.kind.fixed != result.kind.fixed { return Err("a product of another type than the fold's"); } - let at = nodes.add(Node { desc: mapped.desc, origin: Origin::Step(Step::Map { left: Some(x), right: y, plan: mapped.plan }) }); - Ok(Read::new(at, result.layout.clone())) + let read = nodes.mapped(mapped, Some(x), y); + Ok(Read::new(read.of, result.layout.clone())) } -/// The read of new nodes in `nodes` that compute the fold by `p` along `axis` of `of`, a read of a copy under any shape, from the copy's source: the source's fold along the axis that `axis` steps along, spread over the copy's other axes (`spread`). The copy must step along its source from its first item, as `stepped` describes, read the whole of that axis, and read more items than its source holds, as a broadcast does. A fold of a broadcast then folds the broadcast's source once, and never computes the broadcast. A copy that reads each item once folds its source in place when it runs (`Mapping::block` in `pervasive::mapped_fold`). `None`, adding nothing, for a scan, a seeded fold and any other read. +/// The read of new nodes in `nodes` that compute the fold by `p` along `axis` of `of`, a read of a copy under any shape, from the copy's source: the source's fold along the axis that `axis` steps along, spread over the copy's other axes (`spread`). The copy must step along its source from its first item, as `forward_axes` describes, read the whole of that axis, and read more items than its source holds, as a broadcast does. A fold of a broadcast then folds the broadcast's source once, and never computes the broadcast. A copy that reads each item once folds its source in place when it runs (`Mapping::block` in `pervasive::mapped_fold`). `None`, adding nothing, for a scan, a seeded fold and any other read. pub(crate) fn fold_of_copy(nodes: &mut Nodes, p: Primitive, kind: FoldKind, of: &Read, axis: usize, result: &Desc, numeric: Numeric) -> Option { let Origin::Step(Step::Copy { of: source, map }) = &nodes[of.of].origin else { return None }; let (source, frame, shape) = (*source, of.layout.shape(), nodes[*source].desc.shape().to_vec()); if kind.scan || count(&shape) >= count(frame) { return None; } - let along = stepped(map, frame, &shape)?; + let along = forward_axes(map, frame, &shape)?; let b = along[axis].filter(|&b| frame[axis] == shape[b])?; let items = nodes[source].desc.clone(); let (desc, b) = crate::plan::fold_desc(p, kind, Some(b), None, &items, numeric).ok()?; diff --git a/src/program/layout.rs b/src/program/layout.rs index b332dbf7..090e86f7 100644 --- a/src/program/layout.rs +++ b/src/program/layout.rs @@ -1,6 +1,6 @@ //! A traced program's maps in the storage order that their operands and readers need (`ordered`). -use super::{Node, Nodes, Origin, Program, Read, Step, along, copy_of_copy, laid_out, stepped, through_copies}; +use super::{Node, Nodes, Origin, Program, Read, Step, along, copy_of_copy, laid_out, forward_axes, through_copies}; use crate::plan::Desc; use crate::{array::Layout, number::Numeric}; use rustymath::{agreement::{Agreement, Mapping}, gather::counts_up, layout::count, search::inverse_permutation}; @@ -53,10 +53,10 @@ fn reads(program: &Program) -> (Vec, Vec) { let nodes = &program.nodes; let (mut links, mut wants) = (Vec::new(), Vec::new()); for (j, node) in nodes.iter().enumerate() { - if let Some((left, right)) = map(program, j) && let Ok(agreement) = Agreement::new(left.map_or(&Layout::default(), |r| &r.layout), &right.layout) { - for (read, through) in left.map(|l| (l, &agreement.left)).into_iter().chain([(right, &agreement.right)]) { + if let Some((left, right)) = map(program, j) { + for read in left.into_iter().chain([right]) { if nodes[read.of].desc.shape() != read.layout.shape() { continue; } - let (of, through) = through_copies(nodes, read.of, through, node.desc.shape()); + let (of, through) = through_copies(nodes, read.of, &read.pointwise(nodes), node.desc.shape()); let Some(axes) = permutation(&through, node.desc.shape(), nodes[of].desc.shape()) else { continue }; if map(program, of).is_some() { links.push(Link { reader: j, of, axes }); } else { wants.push(Want { of: j, order: inverse_permutation(axes.iter().copied()) }); } @@ -127,10 +127,10 @@ fn chosen(group: &Group, links: &[Link], wants: &[Want]) -> Option> { (met(best) + crossed > now).then(|| best.clone()) } -/// The axis of an array of `shape` that each axis of `frame` steps along, where `map` reads the array over `frame` as a permutation of its axes: as many items, read from the first, with each axis of more than one item stepping along an axis of its length (`stepped`), and each axis of one item matching one of the array's in order. +/// The axis of an array of `shape` that each axis of `frame` steps along, where `map` reads the array over `frame` as a permutation of its axes: as many items, read from the first, with each axis of more than one item stepping along an axis of its length (`forward_axes`), and each axis of one item matching one of the array's in order. fn permutation(map: &Mapping, frame: &[usize], shape: &[usize]) -> Option> { if frame.len() != shape.len() || frame.contains(&0) || count(frame) != count(shape) { return None; } - let along = stepped(map, frame, shape)?; + let along = forward_axes(map, frame, shape)?; let mut ones = (0..shape.len()).filter(|&b| shape[b] == 1); along.iter().zip(frame).map(|(b, &len)| match *b { Some(b) if shape[b] == len => Some(b), None if len == 1 => ones.next(), _ => None }).collect() } @@ -178,15 +178,18 @@ impl<'a> Reordering<'a> { fn map(&mut self, node: &Node, o: &[usize]) -> Option { let Origin::Step(Step::Map { left, right, plan }) = &node.origin else { return None }; let (old, new) = (&node.desc.layout, node.desc.layout.axes(o.iter().copied())); - let agreement = Agreement::new(left.as_ref().map_or(&Layout::default(), |l| &l.layout), &right.layout).ok()?; let shift = along(old.shape(), o); - let mut operand = |read: &Read, through: &Mapping| -> Option { + let mut operand = |read: &Read| -> Option { if self.nodes[read.of].desc.atom { return Some(Read::new(self.at[read.of], read.layout.clone())); } - Some(Read::new(self.lay(read.of, through.compose(&shift, old.shape(), new.shape())?, &new)?, new.clone())) + let through = read.pointwise(self.nodes).compose(&shift, old.shape(), new.shape())?; + Some(Read::new(self.lay(read.of, through, &new)?, new.clone())) }; - let left = match left { Some(l) => Some(operand(l, &agreement.left)?), None => None }; - let right = operand(right, &agreement.right)?; - Some(self.out.add(Node { desc: Desc { layout: new, ..node.desc.clone() }, origin: Origin::Step(Step::Map { left, right, plan: plan.clone() }) })) + let left = match left { Some(l) => Some(operand(l)?), None => None }; + let right = operand(right)?; + let agreement = Agreement { layout: new.clone(), len: new.count(), + left: left.as_ref().map_or(Mapping::Single, |r| r.pointwise(&self.out)), right: right.pointwise(&self.out) }; + let mapped = crate::plan::Mapped { desc: Desc { layout: new, ..node.desc.clone() }, plan: plan.clone(), agreement }; + Some(self.out.mapped(mapped, left, right).of) } /// Fold step `node`, which folds a moved map in its old layout, folding the map's new node along the moved axis instead, with a copy that puts the kept axes back in the fold's order where they differ. fn fold(&mut self, node: &Node) -> Option { diff --git a/src/program/run.rs b/src/program/run.rs index cb093690..99029a89 100644 --- a/src/program/run.rs +++ b/src/program/run.rs @@ -2,8 +2,8 @@ use super::{Node, Origin, Program, Read, Step, Writes}; use crate::plan::Desc; -use crate::{At, Described, Error, Span, Value, primitive::{Operation, Primitive}, transform::Pairing}; -use rustymath::structure::IndexPart; +use crate::{At, Described, Error, Span, Value, agreement::Agreement, primitive::{Operation, Primitive}, transform::Pairing}; +use rustymath::{agreement::Mapping, structure::IndexPart}; use std::borrow::Cow; /// How a replay runs a program: one node on its own, a conversion that it leaves pending for the products that read through it, or a block stage that computes several a block of items at a time, through compiled parts where it has them (`blocks`). `blocks::plan` chooses. @@ -36,15 +36,41 @@ pub(crate) fn values(program: &Program, plan: &Plan, inputs: &[&Value], span: &S Ok(r.values) } -/// The value of `program`'s result where its only step gives it and it has no outputs, computed by that step on the program's held values, as a plan of each node on its own computes it (`blocks::plan`), without making the plan. `None` for any other program. -pub(crate) fn lone(program: &Program, span: &Span) -> Option> { +/// The program's result. A plan of ordinary nodes can run a lone step on its bound operands directly. +pub(crate) fn replay(program: &Program, plan: &Plan, inputs: &[&Value], span: &Span) -> Result { + if plan.units.iter().all(|u| matches!(u, Unit::Node(_))) { + if let Some(value) = lone(program, inputs, span) { return value; } + } + Ok(taken(program, &mut values(program, plan, inputs, span)?, &program.result)) +} + +/// The result of one step whose operands are held values or bound inputs, without making a block plan. No intermediate computation or side outputs enter this route. +pub(crate) fn lone(program: &Program, inputs: &[&Value], span: &Span) -> Option> { let j = program.result.of; - let alone = program.outputs.is_empty() && matches!(program.nodes[j].origin, Origin::Step(_)); - if !alone || !program.nodes.iter().enumerate().all(|(i, n)| i == j || matches!(n.origin, Origin::Held(_))) { return None; } + let node = &program.nodes[j]; + let Origin::Step(step) = &node.origin else { return None }; + if !program.outputs.is_empty() { return None; } + let leaf = |i: usize| match &program.nodes[i].origin { Origin::Input(k) => Some(inputs[*k]), Origin::Held(v) => Some(v), _ => None }; + if !program.nodes.iter().enumerate().all(|(i, _)| i == j || leaf(i).is_some()) { return None; } if crate::settings::verifies() { super::verify::check(program); } - let values = program.nodes.iter().map(|n| match &n.origin { Origin::Held(v) => Some(v.clone()), _ => None }).collect(); - let mut r = Replay { program, values, inputs: &[] }; - Some(r.node(j, &[], span).map(|()| taken(program, &mut r.values, &program.result))) + let result = run(step, &program.nodes, &|i| leaf(i).expect("a lone step reads inputs or held values"), None, &node.desc, span); + Some(result.and_then(|value| checked(node, j, value, span)).map(|value| + if program.result.view(&program.nodes) { through(&value, &program.result, &program.nodes).into_owned() } else { value })) +} + +/// Stores a completed step at its declared kind and checks the value against its description. +fn checked(node: &Node, j: usize, value: Value, span: &Span) -> Result { + let value = value.stored_at(node.desc.dtype()); + if !node.desc.matches(&value) { + let Origin::Step(step) = &node.origin else { unreachable!("only a step's result is checked"); }; + let (planned, gave) = (&node.desc.kind, crate::plan::Kind::value(&value)); + let atom = |a: bool| if a { " atom" } else { "" }; + return Err(Error::domain(format!( + "a program's step gave a value its plan didn't describe: v{j} {} planned {:?} {:?}{}, gave {:?} {:?}{}", step.glyph(), planned.dtype, + planned.fixed, atom(node.desc.atom), gave.dtype, gave.fixed, atom(value.is_atom()) + )).at(span)); + } + Ok(value) } impl Replay<'_> { @@ -61,16 +87,10 @@ impl Replay<'_> { } _ => None, }; - let value = run(step, &self.program.nodes, &self.values, owned, &node.desc, span)?.stored_at(node.desc.dtype()); - if !node.desc.matches(&value) { - let (planned, gave) = (&node.desc.kind, crate::plan::Kind::value(&value)); - let atom = |a: bool| if a { " atom" } else { "" }; - return Err(Error::domain(format!( - "a program's step gave a value its plan didn't describe: v{j} {} planned {:?} {:?}{}, gave {:?} {:?}{}", step.glyph(), planned.dtype, - planned.fixed, atom(node.desc.atom), gave.dtype, gave.fixed, atom(value.is_atom()) - )).at(span)); - } - value + let value = |i: usize| self.values[i].as_ref().expect("a step's operands have their values"); + let result = if !self.program.takes(j) && let Some(copy) = copy_read(step, &value, &node.desc, span) { copy } + else { run(step, &self.program.nodes, &value, owned, &node.desc, span) }; + checked(node, j, result?, span)? } }; self.values[j] = Some(value); @@ -87,29 +107,59 @@ impl Replay<'_> { } } } +/// A Copy has an explicit result category; an atom copied to a rank-0 array is still an array. +fn copied_result(value: Value, desc: &Desc) -> Result { + if desc.atom { return Ok(value.at(0)); } + if !value.is_atom() { return Ok(value); } + if value.carrier().is_none() && let Some(view) = value.view() { + let buffer = rustymath::buffer::Buffer::cast(view, desc.dtype())?; + return Value::from_storage(desc.layout.clone(), crate::array::Storage::Compact(buffer).fixed(desc.kind.fixed.is_some())); + } + value.relaid(desc.layout.clone()) +} + + +/// An operand Copy is a logical read, not a demand for its expanded storage. Terminal program outputs use `run`, including when a pending copy is forced. +pub(crate) fn copy_read<'a>(step: &Step, node: &dyn Fn(usize) -> &'a Value, desc: &Desc, span: &Span) -> Option> { + let Step::Copy { of, map } = step else { return None; }; + Some(crate::fusion::copied(node(*of), desc.layout.clone(), map, None) + .and_then(|value| copied_result(value, desc)).described("a program's copy").at(span)) +} -/// The value of `step`, on `values`, laid out as `desc` describes. A step whose operands carry a running level's data goes through the call boundary, which that level takes (`transform::call`), as a tape's steps do when an outer `∂` or trace runs. A large copy stays pending (`fusion::copy`), which a product or a fold reads in place. A copy that `desc` describes as an atom gives its one item (`plan::item`). `owned` is an accumulation's base, where the replay gives up its value, so that the accumulation writes into it with no copy. -pub(crate) fn run(step: &Step, nodes: &[Node], values: &[Option], owned: Option, desc: &Desc, span: &Span) -> Result { - let node = |i: usize| values[i].as_ref().expect("a step's operands have their values"); +/// Executes `step` on its operands. A demanded Copy produces stored items; internal copies keep their logical read (`copy_read`). Carried operands go through their call boundary. `owned` gives an accumulation a base whose old value the replay no longer needs. +pub(crate) fn run<'a>( + step: &Step, nodes: &[Node], node: &dyn Fn(usize) -> &'a Value, owned: Option, desc: &Desc, span: &Span +) -> Result { let value = |r: &Read| through(node(r.of), r, nodes); match step { Step::Map { left, right, plan } => { - let (left, right) = (left.as_ref().map(value), value(right)); - if crate::transform::carried(left.as_deref(), &right) { return plan.p.call(left.as_deref(), &right, span); } - plan.p.pervasive_apply(Some(plan), left.as_deref(), &right, span, false).at(span) + let (x, y) = (left.as_ref().map(|r| node(r.of)), node(right.of)); + let carried = crate::transform::carried(x, y); + if desc.atom || carried { + let (left, right) = (left.as_ref().map(value), value(right)); + return if carried { plan.p.call(left.as_deref(), right.as_ref(), span) } + else { plan.p.pervasive_apply(Some(plan), left.as_deref(), right.as_ref(), span, false).at(span) }; + } + let agreement = Agreement { layout: desc.layout.clone(), len: desc.layout.count(), + left: left.as_ref().map_or(Mapping::Single, |r| r.pointwise(nodes)), right: right.pointwise(nodes) }; + crate::primitive::weakened(x, y, |left, right| Primitive::planned(plan, left, right, span, false, agreement).at(span)) } Step::Convert { of, numeric } => { let of = value(of); if crate::transform::carried(None, &of) { return crate::derivative::converted(&of, desc.dtype(), span); } of.converted(desc.dtype(), *numeric).described("a program's conversion").at(span) } - Step::Copy { of, map } => crate::fusion::copied(node(*of), desc.layout.clone(), map, None).described("a program's copy").at(span) - .map(|copied| if desc.atom { copied.at(0) } else { copied }), - Step::Structural { inputs, form, params, fill } => { - let (inputs, params): (Vec, Vec) = - (inputs.iter().map(|r| value(r).into_owned()).collect(), params.iter().map(|r| value(r).into_owned()).collect()); + Step::Copy { of, map } => crate::fusion::copied_now(node(*of), desc.layout.clone(), map, None) + .and_then(|value| copied_result(value, desc)).described("a program's copy").at(span), + Step::Structural { inputs, form, params, fill, plan } => { + let inputs: Vec = inputs.iter().map(|r| value(r).into_owned()).collect(); + let params: Vec = params.iter().map(|r| value(r).into_owned()).collect(); let fill = fill.as_ref().map(|r| value(r).into_owned()); - crate::fusion::copy(form, ¶ms, &inputs.iter().collect::>(), fill.as_ref()) + let refs: Vec<&Value> = inputs.iter().collect(); + if let Some(carrier) = crate::transform::newest_carrier(params.iter().chain(refs.iter().copied())) { + return carrier.structural(form, ¶ms, &refs, fill.as_ref()); + } + crate::fusion::structured(desc, plan, &refs, fill.as_ref()) } Step::Gather { of, by } => { let (source, parts) = (value(of), by.parts(|r| value(r).into_owned())); @@ -124,7 +174,7 @@ pub(crate) fn run(step: &Step, nodes: &[Node], values: &[Option], owned: } Step::Product { f, g, left, right, stacked } => { Operation::Product(*f, *g).call(Pairing::stacked(*stacked), Some(&value(left)), &value(right), span).inspect(|_| { - if rustymath::events::running() { crate::explain::fast(rustymath::events::Route::Contraction, step.glyph()); } + if crate::events::running() { crate::explain::fast(crate::events::Route::Contraction, step.glyph()); } }) } Step::At { p, base, items, writes } => { diff --git a/src/program/trace.rs b/src/program/trace.rs index a3e66288..98dd2b7d 100644 --- a/src/program/trace.rs +++ b/src/program/trace.rs @@ -7,8 +7,8 @@ use crate::{ number::Numeric, plan::{self, Operand}, primitive::{FoldKind, Operation, Primitive, structural::Form}, session, transform::{Carrier, Ended, Kind, Op, Pairing} }; -use rustymath::{agreement::Mapping, buffer::{Buffer, DType}, domain::Range, structure::IndexPart}; -use std::sync::{Arc, Mutex, OnceLock, PoisonError, atomic::{AtomicBool, Ordering}}; +use rustymath::{agreement::Mapping, buffer::DType, domain::Range, structure::IndexPart}; +use std::sync::{Arc, Mutex, OnceLock, PoisonError, atomic::{AtomicUsize, Ordering}}; /// The node of a traced value that isn't in the program, such as an item that code with no rule for traces read (`Carrier::storage`). Any call on it stops the trace. const OUTSIDE: usize = usize::MAX; @@ -49,10 +49,6 @@ impl Tracer { fn stopping(&self, reason: &'static str) { self.builder().stopped.get_or_insert(reason); } /// Keeps a name that `•index` drew during the trace, which each replay renames in its result (`Recording::renamed`). pub(crate) fn named(&self, name: Arc) { self.builder().named.push(name); } - /// The step that copies node `of` through `map` for a result of `shape`: one copy of the source where `of` is itself a copy whose map composes with `map` (`copy_of_copy`). - fn copy(&self, of: usize, map: &Mapping, shape: &[usize]) -> Step { - super::copy_of_copy(&self.builder().nodes, of, map, shape).unwrap_or_else(|| Step::Copy { of, map: map.split(shape) }) - } /// The value of node `node`, computed from the free names that it reads, each of which this marks as held: a replay needs the same value there (`Expect::Held`). A free name's node gives the name's value. A step gives what the steps that it reads compute from the names' values, run through the program's block stages (`run::values`). `None` at once in a trace with no free names and in one that has stopped, and otherwise where node `node` reads any other input, such as an argument, or where that run fails. fn hold(&self, node: usize) -> Option { let mut guard = self.builder(); @@ -129,14 +125,16 @@ impl Traced { if self.role == Role::Tape { panic!("a tape value has no items: code that reads a tangent missed a tape value, at\n{}", std::backtrace::Backtrace::force_capture()); } - Carrier::built(&self.items, rustymath::events::Route::TracedItems, self.read.layout.count(), |_| self.outside(Shape::EMPTY)) + Carrier::built(&self.items, crate::events::routes::TracedItems, self.read.layout.count(), |_| self.outside(Shape::EMPTY)) } /// Whether code with no rule for traces has read the items (`storage`). pub(crate) fn items_built(&self) -> bool { self.items.get().is_some() } /// Item `i`, as `Value::at` reads one: a copy of that one item, an atom (`plan::item`). pub(crate) fn item(&self, i: usize) -> Value { let desc = plan::item(&self.desc()).expect("an atom's layout has no renderer"); - self.stepped(desc, |of| self.tracer.copy(of.of, &Mapping::Steps { base: i, steps: Default::default() }, &[])) + if self.read.of == OUTSIDE { return self.outside(Shape::EMPTY); } + let read = self.tracer.builder().nodes.copied(self.read.clone(), Mapping::Steps { base: i, steps: Default::default() }, desc.clone()); + traced(self.level, self.role, read, &desc, &self.tracer) } /// Item `i` as bulk iteration reads it (`Carrier::element`): a tape value's item (`item`), or for a trace an atom outside the program, on which any call stops the trace. pub(crate) fn element(&self, i: usize) -> Value { match self.role { Role::Tape => self.item(i), Role::Trace => self.outside(Shape::EMPTY) } } @@ -158,8 +156,10 @@ impl Traced { if self.role == Role::Tape && !matches!((self.desc().kind.dtype, to), (Some(DType::Float(_)), DType::Float(_))) { return Err(Error::domain("a tangent converts only to floats").at(span)); } - Ok(match plan::convert(&self.desc(), to) { - Ok(desc) => self.stepped(desc, |of| Step::Convert { of: of.clone(), numeric: Numeric::current() }), + if self.read.of == OUTSIDE { return Ok(self.outside(self.read.layout.shape().into())); } + let read = self.tracer.builder().nodes.converted(self.read.clone(), to, Numeric::current()); + Ok(match read { + Ok(read) => { let desc = read.desc(&self.tracer.builder().nodes); traced(self.level, self.role, read, &desc, &self.tracer) } Err(_) => self.outside(self.read.layout.shape().into()), }) } @@ -174,11 +174,16 @@ impl Traced { /// The items laid out by `layout`, read through `map`: a copy step, which holds the map. fn copy(&self, layout: Layout, map: &Mapping) -> Value { let shape: Shape = layout.shape().into(); - match crate::plan::copy(&self.desc(), &layout) { - Ok(desc) => self.stepped(desc, |of| self.tracer.copy(of.of, map, &shape)), Err(_) => self.outside(shape) + if self.read.of == OUTSIDE { return self.outside(shape); } + match plan::copy(&self.desc(), &layout) { + Ok(desc) => { + let read = self.tracer.builder().nodes.copied(self.read.clone(), map.clone(), desc.clone()); + traced(self.level, self.role, read, &desc, &self.tracer) + } + Err(_) => self.outside(shape), } } - /// `form`'s result on `inputs`, with `params` and `fill` read at each run (`structural::plan`): a step that holds the recipe (`Step::Structural`), whose layout each run plans from its inputs. A parameter that this level carries must be one that the trace holds (`observed`) or a count by `≢`, which each run plans from (`count`), since its value decides the layout. The result takes the join of the kinds of the inputs and fill that hold items, or the first input's kind where none holds an item (`array::items_kind`), as `fusion::copy` joins them at each run. A join of numbers with other items stops a trace, or gives UNSUPPORTED ERROR on a tape. + /// Records `form` with geometry resolved from current parameters and input layouts. Observed free parameters retain value guards; shape counts retain exact-layout guards. Inputs and explicit fill bind again on replay. pub(crate) fn structural(&self, form: &Form, params: &[Value], inputs: &[&Value], fill: Option<&Value>) -> Result { let (level, role) = (self.level, self.role); let fail = |reason| failed(role, level, &self.tracer, reason); @@ -191,20 +196,14 @@ impl Traced { let parameters = params.iter().zip(&values).map(|(v, known)| { Arg::of(if crate::transform::nested(v) { known } else { v }, level, role) }).collect::, _>>().map_err(fail)?; - let layout = { - let layouts: Vec<&Layout> = args.iter().map(|a| &a.desc().layout).collect(); - crate::primitive::structural::plan(form, &values, &layouts)?.0 - }; - let kind = crate::array::items_kind(args.iter().chain(&fill).map(|a| (a.desc().layout.count(), a.desc().kind)), crate::array::Widening::Arrays) - .unwrap_or(args[0].desc().kind); - if kind.dtype.is_none() { return Err(fail("a copy that joins numbers with other items")); } - // Atoms copied to no axes stay atoms where the form keeps them, as `fusion::copy` gives them at each run (`Form::keeps_atoms`). - let source = Desc { kind, ..args[0].desc().clone() }; - let atoms = form.keeps_atoms() && layout.shape().is_empty() && args.iter().chain(&fill).all(|a| a.desc().atom); - let desc = if atoms { plan::item(&source) } else { plan::copy(&source, &layout) }.map_err(fail)?; - let step = Step::Structural { inputs: args.into_iter().map(|a| a.read(&self.tracer)).collect(), form: form.clone(), - params: parameters.into_iter().map(|a| a.read(&self.tracer)).collect(), fill: fill.map(|a| a.read(&self.tracer)) }; - Ok(node(level, role, &self.tracer, desc, step)) + let descriptions: Vec = args.iter().map(|a| a.desc().clone()).collect(); + let (desc, plan) = plan::structural(form, &values, &descriptions, fill.as_ref().map(Arg::desc))?; + if desc.kind.dtype.is_none() { return Err(fail("a copy that joins numbers with other items")); } + let inputs = args.iter().map(|a| a.read(&self.tracer)).collect(); + let params = parameters.iter().map(|a| a.read(&self.tracer)).collect(); + let fill = fill.as_ref().map(|a| a.read(&self.tracer)); + let read = self.tracer.builder().nodes.structured(form.clone(), params, inputs, fill, desc.clone(), plan); + Ok(traced(level, role, read, &desc, &self.tracer)) } /// The items of `value`, this value, that `parts` select, where a part holds positions that the program computes (`primitive::select`): a gather step (`gather`). pub(crate) fn selected(&self, value: &Value, parts: &[IndexPart], paired: bool, span: &Span) -> Result { @@ -281,8 +280,8 @@ const HOLDS_TRACED: &str = "a traced value inside a nested array"; /// The traced value of a new node at level `level` for `role`, made by `step` and described by `desc`. fn node(level: u64, role: Role, tracer: &Arc, desc: Desc, step: Step) -> Value { - let node = tracer.add(desc.clone(), Origin::Step(step)); - traced(level, role, Read::new(node, desc.layout.clone()), &desc, tracer) + let read = tracer.builder().nodes.stepped(desc.clone(), step); + traced(level, role, read, &desc, tracer) } /// Each of `parts` as an argument at level `level` for `role`, or the reason that a program can't hold one. @@ -351,33 +350,8 @@ impl<'a> Arg<'a> { Ok(if desc.atom { Value::integer(0) } else { desc.layout.filled(&Value::integer(0)) }) } /// The argument's read: its own, or the read of a new node that holds the value. - fn read(self, tracer: &Tracer) -> Read { - match self { - Arg::Traced(read, _) => read, - Arg::Held(value, desc) => { - let layout = desc.layout.clone(); - // A pending copy enters as a copy of its source, which the run leaves pending for calls that read it in place, so the program never holds the copy's items (`fusion::source`). - if let Some((source, map)) = crate::fusion::source(value) && let Some(d) = Desc::of(&source) { - let of = Arg::Held(&source, d).read(tracer); - let step = tracer.copy(of.of, &map, layout.shape()); - return Read::new(tracer.add(desc, Origin::Step(step)), layout); - } - // An array of one repeated value holds that item, which a broadcast copy reads, so that a program holds no array of ones or zeros. - let one = (!value.carried() && value.len() > 1 && value.pending().is_none() && value.view().is_some_and(Buffer::same)).then(|| value.at(0)); - let copied = one.and_then(|item| { - let d = Desc::of(&item)?; - Some((plan::copy(&d, &layout).ok()?, d, item)) - }); - let node = match copied { - Some((copy, d, item)) => { - let of = tracer.add(d, Origin::Held(item)); - tracer.add(copy, Origin::Step(Step::Copy { of, map: Mapping::Single })) - } - None => tracer.add(desc, Origin::Held(value.clone())), - }; - Read::new(node, layout) - } - } + fn read(&self, tracer: &Tracer) -> Read { + match self { Arg::Traced(read, _) => read.clone(), Arg::Held(value, desc) => tracer.builder().nodes.held(value, desc.clone()) } } } @@ -393,7 +367,7 @@ pub(crate) fn call(op: Op<'_>, left: Option<&Value>, right: &Value, level: u64) let (operation, pairing) = match op { Op::System(..) if role == Role::Trace => return None, Op::Operation(Operation::Primitive(p), _) if role == Role::Trace && !p.scalar(left.is_some()) => return None, - Op::Operation(operation, pairing @ (Pairing::Axis(_) | Pairing::Matrices | Pairing::At(_))) => (operation, pairing), + Op::Operation(operation, pairing @ (Pairing::Axis(_) | Pairing::Agreed(_) | Pairing::Matrices | Pairing::At(_))) => (operation, pairing), _ => return stop("a call that programs don't take yet"), }; let args = match (left.map(|x| Arg::of(x, level, role)).transpose(), Arg::of(right, level, role)) { @@ -401,44 +375,41 @@ pub(crate) fn call(op: Op<'_>, left: Option<&Value>, right: &Value, level: u64) }; let desc = |arg: &Arg| arg.desc().clone(); let numeric = Numeric::current(); - let planned = match (operation, pairing, &args.0) { - (Operation::Primitive(p), Pairing::Axis(None), x) => { - plan::map(p, x.as_ref().map(Arg::operand).as_ref(), &args.1.operand(), left.is_some_and(|l| std::ptr::eq(l, right)), numeric).map(Planned::Map) + let reads = || (args.0.as_ref().map(|x| x.read(&tracer)), args.1.read(&tracer)); + let built = match (operation, pairing, &args.0) { + (Operation::Primitive(p), Pairing::Axis(None) | Pairing::Agreed(_), x) => { + let mapped = match plan::map(p, x.as_ref().map(Arg::operand).as_ref(), &args.1.operand(), left.is_some_and(|l| std::ptr::eq(l, right)), numeric, match pairing { Pairing::Agreed(a) => Some(a), _ => None }) { + Ok(mapped) => mapped, Err(reason) => return stop(reason), + }; + if role == Role::Trace && let Some(kept) = crate::pervasive::unchanged(&mapped.plan, left, right, &mapped.agreement) { return Some(Ok(kept.clone())); } + let (left, right) = reads(); + Ok(tracer.builder().nodes.mapped(mapped, left, right)) } (Operation::Fold(p, kind), Pairing::Axis(axis), x) => { let (seed, y) = (x.as_ref().map(desc), desc(&args.1)); - plan::fold_desc(p, kind, axis, seed.as_ref(), &y, numeric).map(|(d, axis)| Planned::Fold(p, kind, d, axis)) + plan::fold_desc(p, kind, axis, seed.as_ref(), &y, numeric).map(|(desc, axis)| { + let (left, right) = reads(); + tracer.builder().nodes.folded(p, kind, axis, left, right, desc) + }) } (Operation::Product(f, g), Pairing::Axis(None) | Pairing::Matrices, Some(x)) => { let (x, y, stacked) = (desc(x), desc(&args.1), matches!(pairing, Pairing::Matrices)); - plan::product_desc(f, g, &x, &y, stacked, numeric).map(|d| Planned::Product(f, g, d, stacked)) + plan::product_desc(f, g, &x, &y, stacked, numeric).map(|desc| { + let (left, right) = reads(); + tracer.builder().nodes.stepped(desc, Step::Product { f, g, left: left.expect("a product has a left argument"), right, stacked }) + }) } (Operation::Primitive(p), Pairing::At(writes), Some(base)) => { - plan::at(p, &desc(base), &desc(&args.1), numeric).map(|d| Planned::At(p, d, writes.into())) + plan::at(p, &desc(base), &desc(&args.1), numeric).map(|desc| { + let (left, items) = reads(); + tracer.builder().nodes.stepped(desc, Step::At { p, base: left.expect("an accumulation has a base"), items, writes: Writes::Map(writes.into()) }) + }) } _ => Err("a call that programs don't take yet"), }; - let planned = match planned { Ok(planned) => planned, Err(reason) => return stop(reason) }; - if role == Role::Trace && let Planned::Map(m) = &planned && let Some(kept) = crate::pervasive::unchanged(&m.plan, left, right) { - return Some(Ok(kept.clone())); - } - // An unseeded `+` fold of a traced `×` runs as one product where its operands allow (`contraction`), and an unseeded fold of a copy that repeats items folds the copy's source (`fold_of_copy`). A tape records the same steps, and its transpose runs them as products and folds. - if let (Planned::Fold(p, kind, d, axis), None, Arg::Traced(read, _)) = (&planned, &args.0, &args.1) { - let mut builder = tracer.builder(); - if let Some(read) = super::contraction::routed(&mut builder.nodes, *p, *kind, read, &[*axis], d, numeric) { - let desc = read.desc(&builder.nodes); - drop(builder); - return Some(Ok(traced(level, role, read, &desc, &tracer))); - } - } - let (left, right) = (args.0.map(|x| x.read(&tracer)), args.1.read(&tracer)); - let (desc, step) = match planned { - Planned::Map(m) => (m.desc, Step::Map { left, right, plan: m.plan }), - Planned::Fold(p, kind, d, axis) => (d, Step::Fold { p, kind, axis, seed: left, of: right }), - Planned::Product(f, g, d, stacked) => (d, Step::Product { f, g, left: left.expect("a planned product has a left argument"), right, stacked }), - Planned::At(p, d, writes) => (d, Step::At { p, base: left.expect("an accumulation has a base"), items: right, writes: Writes::Map(writes) }), - }; - Some(Ok(node(level, role, &tracer, desc, step))) + let read = match built { Ok(read) => read, Err(reason) => return stop(reason) }; + let desc = read.desc(&tracer.builder().nodes); + Some(Ok(traced(level, role, read, &desc, &tracer))) } /// `≢` or `⍴` of the traced input's layout. A count is recorded as `Step::Shape` and remains available to structural parameters (`Traced::held`). Shape stays constant because its dynamic integer width depends on its lengths. Both are valid while replay's concrete-layout guards hold. @@ -470,12 +441,12 @@ pub(crate) fn chosen(mask: &Value, yes: &Value, no: &Value) -> Option { let [m, y, n] = [mask, yes, no].map(|v| Arg::of(v, level, role).ok()); let (m, y, n) = (m?, y?, n?); let select = plan::select(&m.operand(), &y.operand(), &n.operand()).ok()?; - let step = Step::Select { mask: m.read(&tracer), yes: y.read(&tracer), no: n.read(&tracer) }; - Some(node(level, role, &tracer, select.desc, step)) + let (mask, yes, no) = (m.read(&tracer), y.read(&tracer), n.read(&tracer)); + let read = tracer.builder().nodes.selected(&select, mask, yes, no); + let desc = read.desc(&tracer.builder().nodes); + Some(traced(level, role, read, &desc, &tracer)) } -/// A call's plan, before its arguments have nodes. -enum Planned { Map(plan::Mapped), Fold(Primitive, FoldKind, Desc, usize), Product(Primitive, Primitive, Desc, bool), At(Primitive, Desc, Mapping) } /// The binding read from `place` while trace level `level` runs, where `place` lies outside the traced call's frames: a free name of the call. An array or number becomes an input, which a replay reads again from `place`. An observing read of it, such as a position or a count, sees the name's value. A replay then needs the same value there (`Traced::observed`). A function, or any other value, stays as it is, and a replay checks that `place` still holds it. A place read again gives what its first read gave. pub(crate) fn read(binding: crate::eval::Binding, place: Place, level: u64, tracer: &Arc) -> crate::eval::Binding { @@ -529,22 +500,22 @@ pub(crate) fn build( }; tracer.add(desc.clone(), Origin::Input(k)) }; - let level = session().begin(Kind::Trace(tracer.clone())); - let data: Vec = - data.iter().enumerate().map(|(k, v)| free(Free::Datum(k), v, level, &tracer).expect("a program takes each datum as an input")).collect(); - let traced_leaves = |value: &Value, which: fn(usize) -> Input| { - let mut k = 0; - crate::transform::walk(value, &mut |leaf| { - let desc = Desc::of(leaf).expect("a key describes each leaf"); - let read = Read::new(input(which(k), &desc), desc.layout.clone()); - k += 1; - Ok(traced(level, Role::Trace, read, &desc, &tracer)) - }).expect("an argument's leaves trace") - }; - let x = left.map(|l| traced_leaves(l, Input::Left)); - let y = traced_leaves(right, Input::Right); - let called = call(&data, x.as_ref(), &y); - session().end(); + let (level, called) = session().with_level(Kind::Trace(tracer.clone()), |level| { + let data: Vec = + data.iter().enumerate().map(|(k, v)| free(Free::Datum(k), v, level, &tracer).expect("a program takes each datum as an input")).collect(); + let traced_leaves = |value: &Value, which: fn(usize) -> Input| { + let mut k = 0; + crate::transform::walk(value, &mut |leaf| { + let desc = Desc::of(leaf).expect("a key describes each leaf"); + let read = Read::new(input(which(k), &desc), desc.layout.clone()); + k += 1; + Ok(traced(level, Role::Trace, read, &desc, &tracer)) + }).expect("an argument's leaves trace") + }; + let x = left.map(|l| traced_leaves(l, Input::Left)); + let y = traced_leaves(right, Input::Right); + (level, call(&data, x.as_ref(), &y)) + }); // A trace that ends without a program still gives the free values that it read. let end = |how: Ended| (Err(how), std::mem::take(&mut tracer.builder().captured)); let value = match called { @@ -567,25 +538,29 @@ pub(crate) fn build( let (Some(result), Some(first)) = (result, reads.next()) else { return end(Ended::Fell(fell.unwrap_or("a value that programs can't hold"))) }; let Builder { nodes, inputs, captured, named, .. } = std::mem::take(&mut *tracer.builder()); let program = super::layout::ordered(Program { outputs: reads.map(|r| r.of).collect(), ..Program::new(nodes, first) }.live()); - (Ok(Recording { program, inputs, result, named }), captured) + let plan = super::planned(&program); + (Ok(Recording { program, plan, inputs, result, named }), captured) } -/// A recorded call's program, input sources, result description and generated names. The `Entry` keeps the concrete argument layouts, settings and captured values that it was built for. +/// A recorded call's program and execution plan, input sources, result description and generated names. The `Entry` keeps the concrete argument layouts, settings and captured values that it was built for. #[derive(Debug)] -pub(crate) struct Recording { program: Program, inputs: Vec, result: Descs, named: Vec> } +pub(crate) struct Recording { program: Program, plan: Arc, inputs: Vec, result: Descs, named: Vec> } /// Where an input comes from at a call: a leaf of the left or right argument, by its place in `transform::walk`'s order, or the value of the free name that `Recording::captured` lists at that index. #[derive(Clone, Copy, Debug)] pub(crate) enum Input { Left(usize), Right(usize), Name(usize) } -/// What a program was built for: its arguments' descriptions, the numeric settings, and the environment of the closure it traced, which the free names it read are found through. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum Purpose { Call, Derivative } + +/// What a program was built for: its purpose, arguments' descriptions, the numeric and execution-size settings, and the environment of the closure it traced, which the free names it read are found through. #[derive(Clone, Debug, PartialEq)] -pub(crate) struct Key { pub(crate) left: Option, pub(crate) right: Descs, pub(crate) numeric: Numeric, pub(crate) environment: Option } +pub(crate) struct Key { pub(crate) purpose: Purpose, pub(crate) left: Option, pub(crate) right: Descs, pub(crate) numeric: Numeric, sizes: crate::settings::Sizes, pub(crate) environment: Option } impl Key { /// Whether a call with signature `other` can run a program built for this one: the same settings and environment, and arguments that one program serves (`alike`). fn admits(&self, other: &Key, exact: bool) -> bool { - self.numeric == other.numeric && self.environment == other.environment && alike(&self.right, &other.right, exact) && match (&self.left, &other.left) { + self.purpose == other.purpose && self.numeric == other.numeric && self.sizes == other.sizes && self.environment == other.environment && alike(&self.right, &other.right, exact) && match (&self.left, &other.left) { (Some(a), Some(b)) => alike(a, b, exact), (None, None) => true, _ => false } } @@ -696,25 +671,27 @@ impl Recording { #[derive(Debug, Default)] pub(crate) struct Programs(Mutex>); -/// A trace's outcome, with what the trace assumed: the argument signature that it ran on, and the free values that it read. It also counts the traces that its signature's changed free values made in a row before it, with no call of the signature between them that a kept outcome served (`CHANGES`), and notes whether one has served such a call since. +/// An outcome and its replay guards. `unreused_builds` counts recording attempts since any cached outcome last served this signature: a build carries its count, and a lookup hit resets the newest matching entry. #[derive(Debug)] -struct Entry { key: Key, captured: Vec, outcome: Outcome, changes: usize, served: AtomicBool } +struct Entry { key: Key, captured: Vec, outcome: Outcome, unreused_builds: AtomicUsize } /// What a trace made: a program, or the reason that it stopped, with whether the stop applies to calls with any signature (`Kept::Always`). #[derive(Clone, Debug)] enum Outcome { Built(Arc), Stopped { reason: &'static str, any_key: bool } } -/// What `Programs::find` gives a call: the newest outcome that applies to it, with the values that a program's inputs read from free values (`Entry::reread`); where none applies, a trace to make, with the number of traces that the signature's changed free values made in a row before it (`CHANGES`); or, after `CHANGES` such traces, a stop for every call with the signature. +/// An applicable cached outcome with current free inputs, a new trace carrying its attempt count, or a stop after `MAX_UNREUSED_BUILDS` attempts without a cache hit. enum Lookup { Applies(Outcome, Vec>), Trace(usize), Changing } impl Entry { /// The free values, read again for a call: `None` at an index that isn't an input. A name is read from where it's bound, and a datum from `data`, the data of the function that the call runs. The outer `None` means a free value no longer holds what the trace assumed, so the outcome doesn't apply to the call. fn reread(&self, data: &[Value]) -> Option>> { + let kind = |v: &Value| Desc::of(v).map(|d| d.kind); + let same = |v: &Value, w: &Value| v.same(w) && kind(v) == kind(w); self.captured.iter().map(|c| match (c.free.found(data)?, &c.expect) { - (Found::Value(v), Expect::Value(desc)) if desc.matches(&v) && Desc::of(&v).is_some_and(|d| d.range.within(desc.range.lo, desc.range.hi)) + (Found::Value(v), Expect::Value(desc)) if desc.matches(&v) && Desc::of(&v).is_some_and(|d| d.kind == desc.kind && d.range.within(desc.range.lo, desc.range.hi)) && laid_alike(&desc.layout, v.layout(), self.exact()) => { Some(Some(v)) } (Found::Function(f), Expect::Function(g)) if f.identity() == g.identity() => Some(None), - (Found::Value(v), Expect::Same(w)) if v.same(w) => Some(None), (Found::Value(v), Expect::Held(w)) if v.same(w) => Some(Some(v)), _ => None + (Found::Value(v), Expect::Same(w)) if same(&v, w) => Some(None), (Found::Value(v), Expect::Held(w)) if same(&v, w) => Some(Some(v)), _ => None }).collect() } /// Built programs retain concrete layouts and counts; stopped outcomes key length classes. @@ -724,17 +701,17 @@ impl Entry { /// The most outcomes that one definition, or one structure of function, keeps. The oldest goes first. const KEPT: usize = 8; -/// The traces that a signature's changed free values can make in a row, with no call of the signature between them that a kept outcome served, after which a definition stops tracing calls with that signature, and the interpreter runs them. -const CHANGES: usize = 3; +/// The most recording attempts for one signature without a cache hit before ordinary execution replaces further recording. +const MAX_UNREUSED_BUILDS: usize = 3; impl Programs { fn entries(&self) -> std::sync::MutexGuard<'_, Vec> { self.0.lock().unwrap_or_else(PoisonError::into_inner) } - /// What a call with argument signature `key` finds, where `data` and the names that the traces read give its free values (`Lookup`). An outcome that applies marks the signature's newest entry as served. + /// Finds a guarded outcome for this call. A valid lookup resets the newest matching signature's build count, even when an older entry supplies the outcome. fn find(&self, key: &Key, data: &[Value]) -> Lookup { let entries = self.entries(); // The signature's newest entry, and the newest outcome that applies, with the free values that its trace read. let (mut newest, mut applies) = (None, None); - for entry in entries.iter().rev() { + for entry in entries.iter().rev().filter(|entry| entry.key.purpose == key.purpose) { let own = entry.key.admits(key, entry.exact()); if own && newest.is_none() { newest = Some(entry); } if applies.is_none() && (own || matches!(entry.outcome, Outcome::Stopped { any_key: true, .. })) { @@ -743,18 +720,18 @@ impl Programs { if applies.is_some() && newest.is_some() { break; } } if let Some((outcome, names)) = applies { - if let Some(newest) = newest { newest.served.store(true, Ordering::Relaxed); } + if let Some(newest) = newest { newest.unreused_builds.store(0, Ordering::Relaxed); } return Lookup::Applies(outcome.clone(), names); } - match newest.filter(|e| !e.served.load(Ordering::Relaxed)).map_or(0, |e| e.changes + 1) { - CHANGES => Lookup::Changing, changes => Lookup::Trace(changes) + match newest.map_or(0, |e| e.unreused_builds.load(Ordering::Relaxed)) { + MAX_UNREUSED_BUILDS => Lookup::Changing, builds => Lookup::Trace(builds + 1) } } /// Keeps `entry` as the newest. An entry whose trace read no free values applies to every call with its signature, or with any signature for a stop that applies to them all, so it replaces the entries that it hides. The oldest goes first past `KEPT`. fn add(&self, entry: Entry) { let mut entries = self.entries(); if entry.captured.is_empty() { - match entry.outcome { Outcome::Stopped { any_key: true, .. } => entries.clear(), _ => entries.retain(|e| e.key != entry.key) } + match entry.outcome { Outcome::Stopped { any_key: true, .. } => entries.retain(|e| e.key.purpose != entry.key.purpose), _ => entries.retain(|e| e.key != entry.key) } } if entries.len() == KEPT { entries.remove(0); } entries.push(entry); @@ -769,7 +746,7 @@ pub(crate) struct Tacit { kept: std::cell::RefCell, span: &Span) -> Option> { let mut kept = self.kept.borrow_mut(); if let Some((_, programs)) = kept.find(|(s, _)| f.has_structure(s, data)) { return Some(programs.clone()); } @@ -785,46 +762,61 @@ impl Tacit { /// Runs `∂` of `f`, a function other than a dfn, from a program, as `call` does for a dfn. Programs are kept for each structure of function (`Tacit`). Each datum that `f`'s structure leaves out, an array that `f` binds as an argument or returns as a constant, becomes an input, as a free name that a dfn reads does. pub(crate) fn tacit( - f: &Function, left: Option<&Value>, right: &Value, span: &Span, traced: &dyn Fn(&Function, Option<&Value>, &Value) -> Result + f: &Function, purpose: Purpose, previous: Option<&mut Option>, left: Option<&Value>, right: &Value, span: &Span, + traced: &dyn Fn(&Function, Option<&Value>, &Value) -> Result ) -> Option> { let tacit = &session().tacit; // A call that this one's trace or replay makes finds the buffer taken, and makes its own. let mut data = std::mem::take(&mut *tacit.data.borrow_mut()); - let result = tacit.programs(f, &mut data, span).and_then(|programs| { - programmed(&programs, f.environment(), left, right, span, &data, &|values, x, y| traced(&f.with_data(&mut values.iter().cloned(), span)?, x, y)) - }); + let result = match tacit.programs(f, &mut data, span) { + Some(programs) => programmed(&programs, purpose, previous, f.environment(), left, right, span, &data, + &|values, x, y| traced(&f.with_data(&mut values.iter().cloned(), span)?, x, y)), + None => { if let Some(previous) = previous { *previous = None; } None } + }; data.clear(); *tacit.data.borrow_mut() = data; result } -/// Runs `∂` of a function from one of its `programs`, building one first where none fits these arguments, settings and free values. `data` are the function's data that its programs take as inputs (`Function::with_data`), none for a dfn. `traced` runs the call on traced inputs in place of `data` and on the arguments it's given. `None` leaves the call to the interpreter. +/// Runs a function or its `∂` from one of its `programs`. `previous`, when present for an operator body, defers a cache miss until its key repeats on consecutive invocations; existing hits always apply immediately. Other callers build on a miss. `data` are the function's data that its programs take as inputs (`Function::with_data`), none for a dfn. `traced` runs the call on traced inputs in place of `data` and on the arguments it's given. `None` leaves the call to the interpreter. #[inline(never)] pub(crate) fn programmed( - programs: &Programs, environment: Option, left: Option<&Value>, right: &Value, span: &Span, data: &[Value], + programs: &Programs, purpose: Purpose, previous: Option<&mut Option>, environment: Option, left: Option<&Value>, right: &Value, span: &Span, data: &[Value], traced: &dyn Fn(&[Value], Option<&Value>, &Value) -> Result) -> Option> { let numeric = Numeric::current(); + let previous = previous.filter(|_| purpose == Purpose::Call); let declined = |reason| { - rustymath::events::declined(rustymath::events::Route::Program, reason, right.len()); + crate::events::declined(crate::events::routes::Program, reason, right.len()); None }; - if session().transforming() { return declined("a call under another level"); } + if session().transforming() { + if let Some(previous) = previous { *previous = None; } + return declined("a call under another level"); + } let described = |v: &Value| Descs::of(v, &mut Desc::of).ok_or("an argument that programs can't hold"); let key = match (left.map(described).transpose(), described(right)) { - (Ok(left), Ok(right)) => Key { left, right, numeric, environment }, (Err(reason), _) | (_, Err(reason)) => return declined(reason) + (Ok(left), Ok(right)) => Key { purpose, left, right, numeric, sizes: crate::settings::sizes(), environment }, + (Err(reason), _) | (_, Err(reason)) => { if let Some(previous) = previous { *previous = None; } return declined(reason); } }; - let (outcome, names, event) = match programs.find(&key, data) { + let lookup = programs.find(&key, data); + let defer_trace = matches!(&lookup, Lookup::Trace(_)) && previous.as_deref().is_some_and(|previous| previous.as_ref() != Some(&key)); + let key: &Key = match previous { Some(previous) => previous.insert(key), None => &key }; + let (outcome, names, event) = match lookup { Lookup::Applies(outcome, names) => (outcome, Some(names), "replayed"), Lookup::Changing => { let reason = "a free value that changes on every call"; - programs.add(Entry { key, captured: Vec::new(), outcome: Outcome::Stopped { reason, any_key: false }, changes: CHANGES, - served: AtomicBool::new(false) }); - rustymath::events::stopped(reason, right.len(), || None); + programs.add(Entry { key: key.clone(), captured: Vec::new(), outcome: Outcome::Stopped { reason, any_key: false }, unreused_builds: AtomicUsize::new(0) }); + if purpose == Purpose::Call { return declined(reason); } + crate::events::stopped(reason, right.len(), || None); return None; } - Lookup::Trace(changes) => { + Lookup::Trace(unreused_builds) => { + if defer_trace { return None; } let (built, captured) = build(left, right, data, traced); let outcome = match built { + Ok(recording) if purpose == Purpose::Call && (!matches!(&recording.result, Descs::Leaf(_)) || !crate::fusion::records_dense(&recording.program)) => { + Outcome::Stopped { reason: "a body outside dense elementwise reuse", any_key: false } + } Ok(recording) => Outcome::Built(Arc::new(recording)), Err(Ended::Stopped(stop, noted)) => { Outcome::Stopped { reason: noted.unwrap_or(stop.cause().reason()), any_key: stop.cause().kept() == Kept::Always } @@ -833,14 +825,17 @@ pub(crate) fn programmed( Err(Ended::Failed(error)) => { crate::explain::fallback(error, right.len()); Outcome::Stopped { reason: "an error", any_key: false } } Err(Ended::Passed(error)) => return Some(Err(error)), }; - let entry = Entry { key: key.clone(), captured, outcome: outcome.clone(), changes, served: AtomicBool::new(false) }; + let entry = Entry { key: key.clone(), captured, outcome: outcome.clone(), unreused_builds: AtomicUsize::new(unreused_builds) }; let names = entry.reread(data); programs.add(entry); (outcome, names, "built") } }; let recording = match outcome { - Outcome::Built(recording) => recording, Outcome::Stopped { reason, .. } => { rustymath::events::stopped(reason, right.len(), || None); return None; } + Outcome::Built(recording) => recording, Outcome::Stopped { reason, .. } => { + if purpose == Purpose::Call { return declined(reason); } + crate::events::stopped(reason, right.len(), || None); return None; + } }; let names = names?; let program = &recording.program; @@ -856,16 +851,17 @@ pub(crate) fn programmed( let inputs: Vec<&Value> = recording.inputs.iter().map(|input| match *input { Input::Left(k) => &x[k], Input::Right(k) => &y[k], Input::Name(i) => names[i].as_ref().expect("a free name that a program reads has a value") }).collect(); - let result = run::values(program, &crate::program::planned(program), &inputs, span).map(|values| match &recording.result { - Descs::Leaf(_) => run::read(program, &values, &program.result), - // A nested result's leaves are the program's result and its outputs, each laid out as the result's description says. - nested => { - let nodes = std::iter::once(program.result.of).chain(program.outputs.iter().copied()); - nested.built(&mut nodes.zip(nested.leaves()).map(|(j, desc)| run::read(program, &values, &Read::new(j, desc.layout.clone())))) - } + let deferred = if purpose == Purpose::Call && matches!(&recording.result, Descs::Leaf(_)) { crate::fusion::recorded(program, &inputs) } else { None }; + let result = deferred.map(Ok).unwrap_or_else(|| match &recording.result { + Descs::Leaf(_) => run::replay(program, &recording.plan, &inputs, span), + // A nested result's leaves are the program's result and its outputs, each laid out as the result's description says. + nested => run::values(program, &recording.plan, &inputs, span).map(|values| { + let nodes = std::iter::once(program.result.of).chain(program.outputs.iter().copied()); + nested.built(&mut nodes.zip(nested.leaves()).map(|(j, desc)| run::read(program, &values, &Read::new(j, desc.layout.clone())))) }) + }) .and_then(|value| recording.renamed(value)); - if result.is_ok() { rustymath::events::taken(rustymath::events::Route::Trace, right.len()); } - crate::explain::program(event, program.nodes.iter().filter(|n| matches!(n.origin, Origin::Step(_))).count(), || recording.listing(&key)); + if result.is_ok() { crate::events::taken(crate::events::routes::Trace, right.len()); } + crate::explain::program(event, program.nodes.iter().filter(|n| matches!(n.origin, Origin::Step(_))).count(), || recording.listing(key)); Some(result) } diff --git a/src/program/verify.rs b/src/program/verify.rs index 2467ef13..eeca36fe 100644 --- a/src/program/verify.rs +++ b/src/program/verify.rs @@ -1,7 +1,7 @@ //! A check of each program's contracts before it runs, as milestone 2 in section 7 of `meta/graphreview.md` asks: every step reads values before its own, every read lays out as many items as its node holds, and every node's description is the one that planning its step on its operands gives (`plan`), as the trace and fusion plan them. The reference harness turns the check on for every case and page (`settings::verifies`). use super::{Node, Origin, Program, Read, Step}; -use crate::{number::Numeric, plan::{self, Desc, Operand, Pervasive}}; +use crate::{number::Numeric, plan::{self, Desc, Operand}}; /// Panics with the program's listing where `program` breaks a contract (`broken`). pub(crate) fn check(program: &Program) { @@ -40,11 +40,7 @@ fn planned(nodes: &[Node], node: &Node, step: &Step) -> Result plan::convert(&desc(of), node.desc.dtype()), Step::Copy { of, .. } if node.desc.atom => plan::item(&nodes[*of].desc), @@ -69,9 +65,3 @@ fn planned(nodes: &[Node], node: &Node, step: &Step) -> Result bool { - let constants = |p: &Pervasive| [0, 1].map(|k| p.constants[k].filter(|_| held[k]).map(f64::to_bits)); - Pervasive { constants: [None; 2], at: None, ..a.clone() } == Pervasive { constants: [None; 2], at: None, ..b.clone() } && constants(a) == constants(b) -} diff --git a/src/search.rs b/src/search.rs index 1e5f2d0c..d4058d2e 100644 --- a/src/search.rs +++ b/src/search.rs @@ -88,10 +88,6 @@ pub(crate) fn classify(cells: &Cells, span: &Span) -> Result, Error> { if let Some(classes) = shared_classes(cells) { return Ok(classes); } let (width, t) = compared_at(cells.float_width(), Numeric::current()); if let Some(classes) = compact_view(cells).and_then(|x| search::view_classes(x, t)) { return classes.map_err(Error::from); } - let compact = with_float_width!(width, F => match cells { - Cells::Items(x) => read_as::(x).map(|x| tolerant::classes(t, &x)), Cells::Arrays(_) => None, - }); - if let Some(classes) = compact { return classes.map_err(Error::from); } if let Some(x) = exact_cells(cells) { return Ok(search::first_classes(&x, |k| k)); } if let Some(x) = hashes(cells, &Hashing::default()) { return hashed_classes(cells, &x, span); } if let Some(x) = reals(cells, width) { return tolerant::classes(t, &*x).map_err(Error::from); } diff --git a/src/selection.rs b/src/selection.rs index b5b3857c..16ec23b8 100644 --- a/src/selection.rs +++ b/src/selection.rs @@ -58,7 +58,7 @@ impl Labels { } fn len(&self) -> usize { match &self.places { Places::Offsets(n) => *n, Places::Paths(paths) => paths.len() } } fn build(&mut self, a: &Value, path: &mut Vec) -> Result { - crate::explain::itemwise(rustymath::events::Route::NestedLabels, a.len()); + crate::explain::itemwise(crate::events::routes::NestedLabels, a.len()); let mut items = Vec::new(); for (i, e) in a.elements().enumerate() { generated_len(&[self.len() + self.nested.len() + 1])?; @@ -129,7 +129,7 @@ impl Labels { Ok((Selection { frame: Frame::Array(shape.into()), targets }, values)) } fn collect(&self, selected: &Value, right: &Value, targets: &mut Targets, values: &mut Gather) -> Result<(), Error> { - crate::explain::itemwise(rustymath::events::Route::LabelIds, selected.len()); + crate::explain::itemwise(crate::events::routes::LabelIds, selected.len()); let aligned; let right = if right.has_keys() && selected.has_keys() { aligned = crate::keyed::reorder(right, &selected.layout().all_keys(), true).described("replacement does not supply selected keys")?; @@ -264,8 +264,8 @@ impl Labelled { let (layout, plan) = crate::primitive::structural::plan(form, ¶ms, &[input.layout()])?; match plan.route() { ReadRoute::Input(0) => return Ok(l.view(layout)), - ReadRoute::Map { input: source, map, .. } if matches!(source.route(), ReadRoute::Input(0)) => { - return l.copied(layout, map, fill.map(|f| strip(f, level)).as_ref()).described("a structural copy"); + ReadRoute::Map(read) if matches!(read.input.route(), ReadRoute::Input(0)) => { + return l.copied(layout, &read.map, fill.map(|f| strip(f, level)).as_ref()).described("a structural copy"); } _ => {} } diff --git a/src/settings.rs b/src/settings.rs index 38c4f52f..9a580e78 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -6,7 +6,7 @@ use rustymath::buffer::{FloatWidth, Width}; use std::sync::{LazyLock, PoisonError, RwLock, atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering::Relaxed}}; /// The sizes that decide which calls wait and how block stages and batched maps split their items. `set_sizes` sets them for the whole process, and a block stage reads them when it runs. -#[derive(Clone, Copy, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub struct Sizes { /// The fewest result items of a call that dispatch defers. A call with a smaller result runs at once. pub min_items: usize, diff --git a/src/transform.rs b/src/transform.rs index fc041a0e..32cabe2c 100644 --- a/src/transform.rs +++ b/src/transform.rs @@ -319,7 +319,7 @@ pub(crate) fn call(op: Op<'_>, left: Option<&Value>, right: &Value, span: &Span) /// A call with a selected running level. #[inline(never)] fn routed(op: Op<'_>, left: Option<&Value>, right: &Value, span: &Span, newest: Mark) -> Option> { - if let Op::Operation(operation, Pairing::Agreed(agreement)) = op { + if !matches!(newest, Mark::Trace(_) | Mark::Tape(_)) && let Op::Operation(operation, Pairing::Agreed(agreement)) = op { let (x, y) = agreement.laid(left, right); return Some(operation.call(Pairing::Axis(None), x.as_ref(), &y, span)); } @@ -407,7 +407,7 @@ impl Carrier { } /// The items of a dual or a batch for `storage`, built once into `cell` as `n` items from `at`. Building them records an `itemwise` row in `•explain` under `route`. pub(crate) fn built<'a>( - cell: &'a std::sync::OnceLock, route: rustymath::events::Route, n: usize, at: impl Fn(usize) -> Value + cell: &'a std::sync::OnceLock, route: crate::events::Route, n: usize, at: impl Fn(usize) -> Value ) -> &'a crate::array::Storage { cell.get_or_init(|| { crate::explain::itemwise(route, n); diff --git a/testkit/src/lib.rs b/testkit/src/lib.rs index a9075677..b444aed9 100644 --- a/testkit/src/lib.rs +++ b/testkit/src/lib.rs @@ -2,7 +2,10 @@ pub mod reference; pub mod timing; +pub(crate) const DEFAULT_PREFS: &str = r#"•prefs ["mathish":$f]"#; + /// basedpl's root directory, which holds `tests/`: the one above this crate's. +#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] pub(crate) fn root() -> &'static std::path::Path { std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().expect("testkit sits in basedpl's root") } /// Takes the machine's timing lock exclusively, as `rustymath_timing::exclusive` describes, then writes `started` to stderr. A worker's caller counts a timing's deadline from that note (`basedpl.worker`). diff --git a/testkit/src/reference.rs b/testkit/src/reference.rs index dd6699e3..a53e14a8 100644 --- a/testkit/src/reference.rs +++ b/testkit/src/reference.rs @@ -1,4 +1,5 @@ //! Development-only checks, shared by the tests and the worker: reference cases, `.bpl` reference files and the examples in `.qmd` pages. +use crate::DEFAULT_PREFS; use basedpl::{EvalOptions, InterruptHandle, Session, Value, hooks::Numeric, settings::Compiles}; use serde_json::{Value as JsonValue, json}; use std::{collections::VecDeque, sync::{Arc, Mutex}}; @@ -132,10 +133,10 @@ fn runs(case: &JsonValue) -> Vec { fn enter(prefs: Option<&str>, run: Option) -> Result<(), JsonValue> { basedpl::settings::reset(); basedpl::settings::set_verifies(true); - if let Some(prefs) = prefs - && let Some(error) = Session::current().eval_apart(prefs, EvalOptions { timeout: Some(TIMEOUT), echo: false, ..EvalOptions::default() }).error { - return Err(json!({"status":"invalid", "message":format!("file header: {error}")})); - } + let prefs = format!("{DEFAULT_PREFS}\n{}", prefs.unwrap_or_default()); + if let Some(error) = Session::current().eval_apart(&prefs, EvalOptions { timeout: Some(TIMEOUT), echo: false, ..EvalOptions::default() }).error { + return Err(json!({"status":"invalid", "message":format!("file header: {error}")})); + } if let Some(Run { batch, fused, compiles, .. }) = run { basedpl::settings::set_batch(batch); basedpl::settings::set_small_sizes(fused); @@ -292,9 +293,9 @@ pub fn route(case: &JsonValue, setup: &str, interrupt: &InterruptHandle) -> Resu } pub fn routed(case: &JsonValue) -> bool { ["route", "allows", "gap"].iter().any(|key| case.get(key).is_some()) } -/// The routes and kinds of event that rustymath's inventory requires some case to exercise (`events::Route`): each route with each kind, "taken", "declined" or "itemwise", that it reports. +/// The routes and kinds of event that BPL and rustymath require some case to exercise (`events::Route`): each route with each kind, "taken", "declined" or "itemwise", that it reports. pub fn inventory() -> impl Iterator { - rustymath::events::Route::ALL.iter().flat_map(|&route| { + basedpl::hooks::event_routes().flat_map(|route| { [("taken", route.reports_taken()), ("declined", route.reports_decline()), ("itemwise", route.reports_itemwise())].into_iter() .filter(|&(_, required)| required).map(move |(kind, _)| (kind, route)) }) @@ -461,6 +462,7 @@ pub fn file(name: &str, text: &str) -> Result { offset += line.len() + 1; } let prefs = lines.first().filter(|line| line.starts_with("•prefs")).copied().unwrap_or_default(); + let mathish = if name.ends_with("_mathish.bpl") { r#"•prefs ["mathish":$t]"# } else { "" }; let (mut i, mut title, mut local) = (usize::from(!prefs.is_empty()), "", ""); let mut file = File { name: name.into(), setup: String::new(), cases: Vec::new() }; let mut ids = rustymath::hashing::HashSet::default(); @@ -490,7 +492,7 @@ pub fn file(name: &str, text: &str) -> Result { let options = suffix.and_then(|(text, s)| Some((text, s.strip_suffix(']')?))) .filter(|(_, s)| s.starts_with("rtol=") || s.starts_with("atol=") || FLAGS.iter().any(|f| s.starts_with(f))); let mut case = json!({"id":id, "line":start + 1, "section":title, "comment":options.map_or(comment, |(text, _)| text)}); - let prefs = [prefs, local].into_iter().filter(|s| !s.is_empty()).collect::>().join("\n"); + let prefs = [mathish, prefs, local].into_iter().filter(|s| !s.is_empty()).collect::>().join("\n"); if !prefs.is_empty() { case["prefs"] = json!(prefs); } if let Some((_, options)) = options { for option in options.split_whitespace() { @@ -703,9 +705,9 @@ pub fn accounting(results: &[JsonValue]) -> JsonValue { /// The modes that `case` plans: the names of its runs (`runs`). pub fn modes(case: &JsonValue) -> Vec<&'static str> { runs(case).into_iter().map(|run| run.words).collect() } -/// The result of `case` when the process checking it stopped: each mode it plans (`modes`) with a `"crash"` status, and `message` saying why. +/// The case's worker abort. Mode results were not returned and remain "unchecked". pub fn crashed(case: &JsonValue, message: &str) -> JsonValue { - let modes: Vec<_> = modes(case).into_iter().map(|mode| json!({"mode":mode, "status":"crash"})).collect(); + let modes: Vec<_> = modes(case).into_iter().map(|mode| json!({"mode":mode, "status":"unchecked"})).collect(); recorded(case, json!({"status":"crash", "message":message, "modes":modes})) } diff --git a/testkit/src/timing.rs b/testkit/src/timing.rs index 1e763aa8..d5db1cd0 100644 --- a/testkit/src/timing.rs +++ b/testkit/src/timing.rs @@ -1,5 +1,5 @@ //! BPL measurements share reference records and setups. Numerical timing, copy units, footprint copies and retries live in Rustymath. -use crate::{reference::{self, File}, timing_lock}; +use crate::{DEFAULT_PREFS, reference::{self, File}, timing_lock}; use basedpl::{EvalOptions, Evaluation, InterruptHandle, Session, Source, Turn, protocol::response}; use rustymath::hashing::HashMap; use rustymath_timing::{Budget, COUNT_MARGIN, Cause, CopyTime, Count, Footprint, Held, MEASURES, NATIVE_COLUMN, copy_time, footprint}; @@ -29,7 +29,7 @@ pub fn session(prefs: &str, setup: &str) -> Result { configure fn configured(prefs: &str, setup: &str) -> Result { single_threaded_defaults(); let session = Session::new(); - let result = session.eval(&format!("•prefs [\"seed\":0]\n{prefs}\n{setup}")); + let result = session.eval(&format!("{DEFAULT_PREFS}\n•prefs [\"seed\":0]\n{prefs}\n{setup}")); if result.error.is_some() { Err(result) } else { Ok(session) } } @@ -46,23 +46,27 @@ fn runner(session: &Session, code: &str, interrupt: &InterruptHandle) -> Result< }) } -/// Parse once, warm once, then take the fastest of the evaluations that `rustymath_timing::fastest` makes, forcing the result each time. Gives that time, the most bytes that those evaluations held at once, as `rustymath::memory::peak_held` counts them, and the bytes that they left held in large allocations beyond what the first of them left. -pub fn time(session: &Session, code: &str, interrupt: &InterruptHandle) -> Result<(f64, usize, usize), Evaluation> { +/// Parse once, time the first fully forced evaluation, then take the fastest warm evaluation. Gives the warm time, peak bytes held during warm runs, bytes left held, and first-evaluation time. Parsing and setup are outside both timings. +pub fn time(session: &Session, code: &str, interrupt: &InterruptHandle) -> Result<(f64, usize, usize, f64), Evaluation> { timed_runs(session, code, interrupt, true) } /// The least time that a case's timed evaluations take together: 5 ms. A case that runs in microseconds then gives the fastest of hundreds of evaluations, which varies less between test runs than the fastest of `rustymath_timing::RUNS`. const LEAST: std::time::Duration = std::time::Duration::from_millis(5); -fn timed_runs(session: &Session, code: &str, interrupt: &InterruptHandle, warmup: bool) -> Result<(f64, usize, usize), Evaluation> { +fn timed_runs(session: &Session, code: &str, interrupt: &InterruptHandle, warmup: bool) -> Result<(f64, usize, usize, f64), Evaluation> { let (mut run, mut failed) = (runner(session, code, interrupt)?, None); - if warmup { run(&mut failed); } + let cold_seconds = if warmup { + let first = rustymath::parallel::Instant::now(); + run(&mut failed); + first.elapsed().as_secs_f64() + } else { 0.0 }; let mut start = 0; let (seconds, held) = rustymath::memory::peak_held(|| { rustymath_timing::fastest(LEAST, |k| { run(&mut failed); if k == 0 { start = rustymath::memory::held(); } }) }); let left = rustymath::memory::held().saturating_sub(start); - failed.map_or(Ok((seconds, held, left)), Err) + failed.map_or(Ok((seconds, held, left, cold_seconds)), Err) } /// The browser build's budget column: a case's third budget, after its macOS and Linux budgets. @@ -121,7 +125,7 @@ impl<'a> Group<'a> { self.current = name; } single_threaded_defaults(); - self.session.eval(header); + self.session.eval(&format!("{DEFAULT_PREFS}\n{header}")); if self.size == Size::Small { basedpl::settings::set_small_sizes(true); } Ok(&self.session) } @@ -177,7 +181,7 @@ pub fn costs(files: &[File], only: Option>, prefs: &str, footprint let copy = (section != "widths").then(|| copy_time::(n)); let session = group.session(section)?; let obligations = route(session, case, interrupt)?.violations(); - let (total, held, left) = timed_runs(session, code, interrupt, false).map_err(|failed| failure(code, failed))?; + let (total, held, left, _) = timed_runs(session, code, interrupt, false).map_err(|failed| failure(code, failed))?; if rustymath::memory::checks() && left > 0 { return Err(format!("{}: the timed runs left {left} more bytes held in large allocations than the first run", label(file, case))); } @@ -408,24 +412,45 @@ pub fn request(spec: &JsonValue, interrupt: &InterruptHandle) -> Result costs.push(json!({"code":code, "seconds":seconds, "copies":seconds / n as f64 / copy.seconds})), + Ok((seconds, _, _, cold_seconds)) => costs.push(json!({"code":code, "seconds":seconds, "cold_seconds":cold_seconds, "copies":seconds / n as f64 / copy.seconds})), Err(failed) => return Ok(response(failed)), } } Ok(json!({"n":n, "costs":costs, "output":[], "error":null})) } -/// Runs each case `steps` times as `time` runs it, for an instruction count or a profile, and gives no timing. Unlike `request`, it takes no timing lock, so counts can run side by side under one shared hold. +/// Runs each case `steps` times as `time` runs it, for an instruction count, and gives no timing. Unlike `request`, it takes no timing lock, so counts can run side by side under one shared hold. pub fn steps_request(spec: &JsonValue, interrupt: &InterruptHandle) -> Result { + repeated_request(spec, interrupt, None:: Result<(), String>>) +} + +/// Prepares one case, forces two warm calls, signals readiness, then repeats in that same Session. +pub fn profile_request( + spec: &JsonValue, interrupt: &InterruptHandle, ready: impl FnOnce() -> Result<(), String> +) -> Result { + repeated_request(spec, interrupt, Some(ready)) +} + +fn repeated_request( + spec: &JsonValue, interrupt: &InterruptHandle, mut ready: Option Result<(), String>> +) -> Result { let text = |key| spec[key].as_str().ok_or_else(|| format!("steps needs {key} as a string")); let steps = spec["steps"].as_u64().ok_or("steps needs steps as a count")?; let cases = spec["cases"].as_array().ok_or("steps needs cases as an array")?; + if ready.is_some() && cases.len() != 1 { return Err("profiling needs exactly one case".into()); } let session = match session(text("prefs")?, text("setup")?) { Ok(session) => session, Err(failed) => return Ok(response(failed)) }; for code in cases { let code = code.as_str().ok_or("each case must be a string")?; let mut failed = None; match runner(&session, code, interrupt) { - Ok(mut run) => { for _ in 0..steps { run(&mut failed); if failed.is_some() { break; } } } Err(error) => failed = Some(error) + Ok(mut run) => { + if let Some(ready) = ready.take() { + for _ in 0..2 { run(&mut failed); if let Some(failed) = failed { return Ok(response(failed)); } } + ready()?; + } + for _ in 0..steps { run(&mut failed); if failed.is_some() { break; } } + } + Err(error) => failed = Some(error) } if let Some(failed) = failed { return Ok(response(failed)); } } diff --git a/tests/derivatives.rs b/tests/derivatives.rs index 511d9435..91019b9c 100644 --- a/tests/derivatives.rs +++ b/tests/derivatives.rs @@ -129,6 +129,7 @@ fn derivatives_match_numerical_ones() { for _ in 0..3 { let n: usize = c.shape.iter().product(); let x: Vec = (0..n).map(|_| points.next(c.low, c.high)).collect(); + eprintln!("derivative {} at {x:?}, shape {:?}", c.f, c.shape); let s = Session::new(); let result = s.eval(&format!("f←{} ⋄ x←{}", c.f, literal(&x, c.shape))); assert!(result.error.is_none(), "{}: {:?}", c.f, result.error); diff --git a/tests/jit.rs b/tests/jit.rs index fbe5430f..4491513c 100644 --- a/tests/jit.rs +++ b/tests/jit.rs @@ -372,6 +372,7 @@ fn compiled_stages_match_interpreted(more: usize) { let (compiled, runs) = run(Compiles::Always); assert!(runs > 0, "{code} on {n} items ran no compiled part"); let interpreted = run(Compiles::Never).0; + if matches!(code.as_str(), "+/b-1" | "0ₓ₄+/xf -[0 0 0 1 1 2⋄1 2 3 2 3 3] +[0 0 0 1 1 2⋄1 2 3 2 3 3]ₓ ⍝ aplcart/table.tsv:1573 — Value of saddle point; First-true masks use cumulative counts under basedpl left scan Nm←[3 4⋄1 2] ⋄ ,#⍨(⊢=⍴⍴⌈⌿){⍵∧1=+\⍵}∘,∘∧⊢=↢⍉⌽∘⍴⍴⌊/ Nm ⍝ [3] @@ -5234,11 +5234,6 @@ Y←2 3 4⍴⍳24ₓ ⋄ ,⍠ ¯1↓⍳≢⍴Y 1#Y ⍝ aplcart/table.tsv:1902 — Valid siteswap pattern? Jv←5 3 1 ⋄ {n=≢∪n|⍵+⍳n←≢⍵}Jv ⍝ $t -⍝ aplcart/table.tsv:1903 — Rounding N to I decimal places (Fast ⍎Is⍕N) -I←2 -N←1.234 ¯2.345 0.005 -I{p÷⍨⌊0.5+⍵×p←10*⍺}N ⍝ 1.23 ¯2.34 0.01 - ⍝ aplcart/table.tsv:1904 — Rotate first major cells (1⊖) of each subvector of Y indicated by Av (fast ∊1⌽¨Av⊂Yv) Av←1 0 1 0 0 Y←3 1 5 2 4 @@ -6195,8 +6190,8 @@ Nv←1e16 1 ¯1e16 1 ⍝ aplcart/table.tsv:2141 — Numeric matrix of all permutations of length Js in unspecified order (faster than generating in lexicographical order) -Js←3 ⋄ {⍉⊃{1=⍵?,⊂,0;,/¨(⍳⍵)⌽¨⊂(∇,↢⊂!⍴⊢)⍵-1}⍵}Js -[0 1 2⋄1 0 2⋄1 2 0⋄0 2 1⋄2 0 1⋄2 1 0] +Js←3ₓ ⋄ {⍉⊃{1ₓ=⍵?,⊂,0ₓ;,/¨(⍳⍵)⌽¨⊂(∇,↢⊂!⍴⊢)⍵-1ₓ}⍵}Js +[0 1 2⋄1 0 2⋄1 2 0⋄0 2 1⋄2 0 1⋄2 1 0]ₓ ⍝ aplcart/table.tsv:2142 — Editing Dv with Cv ('/' to delete and ',' to insert) Cv← " /,xy" @@ -8455,11 +8450,11 @@ J←1 9 10 999 ⋄ ⌊1+10↣⍟ J ⍝ [1 1 2 3]ₓ Js←4 ⋄ ≢∘∪⊢∨⍳ Js ⍝ 3ₓ ⍝ aplcart/tt.tsv:1323 — Numeric matrix of all unordered combinations of Is out of Js without replacement; Reuse concrete inputs from aplcart/table.tsv:1571; execute this alternate recipe independently -Is←2 -Js←4 -Is {⍵#₋₁⍨∧⌿ -[0 0 0 1 1 2⋄1 2 3 2 3 3] +[0 0 0 1 1 2⋄1 2 3 2 3 3]ₓ ⍝ aplcart/tt.tsv:1328 — Odd integers from 1 to 2×Js Js←4 ⋄ 1+2×⍳ Js ⍝ 1 3 5 7 @@ -9031,10 +9026,6 @@ mac "A=++ AA" ⍝ "++++" m←⌽(32⍴2)⊤32↑0 16389 24582 57344 0 13 0 2⊥⌽6⌷baby(m) ⍝ 4294967283 -⍝ aplcart/table.tsv:1706 — ⍺-ary representation of rational ⍵; Ported in lib/dyalog.bpl; checked against the independent Dyalog expectation; Concrete arguments; shared 0-origin based-array port loaded from lib/dyalog.bpl; The source reads the comparison tolerance; this port uses the fixed 1e-14 -[ary:]←•load "lib/dyalog.bpl" -2 ary ÷3 ⍝ "0.0101..." - ⍝ aplcart/table.tsv:1740 — Box the simple text array ⍵; Ported in lib/dyalog.bpl; checked against the independent Dyalog expectation; Concrete arguments; shared 0-origin based-array port loaded from lib/dyalog.bpl [box:]←•load "lib/dyalog.bpl" box ["abc"⋄"def"] diff --git a/tests/reference/aplcart_mathish.bpl b/tests/reference/aplcart_mathish.bpl new file mode 100644 index 00000000..fd4e1028 --- /dev/null +++ b/tests/reference/aplcart_mathish.bpl @@ -0,0 +1,11 @@ +⍝⍝ Mathish semantics + +⍝ aplcart/table.tsv:1903 — Rounding N to I decimal places (Fast ⍎Is⍕N) +I←2 +N←1.234 ¯2.345 0.005 +I{p÷⍨⌊0.5+⍵×p←10*⍺}N ⍝ 1.23 ¯2.34 0.01 + +⍝ aplcart/table.tsv:1706 — ⍺-ary representation of rational ⍵; Ported in lib/dyalog.bpl; checked against the independent Dyalog expectation; Concrete arguments; shared 0-origin based-array port loaded from lib/dyalog.bpl; The source reads the comparison tolerance; this port uses the fixed 1e-14 +[ary:]←•load "lib/dyalog.bpl" +2 ary ÷3 ⍝ "0.0101..." + diff --git a/tests/reference/april.bpl b/tests/reference/april.bpl index db018606..ef85babc 100644 --- a/tests/reference/april.bpl +++ b/tests/reference/april.bpl @@ -183,17 +183,11 @@ ⍝ april:586 — 0 1 0 1∧0 0 1 1 ⍝ 0 0 0 1 -⍝ april:587 — -3.2∧1.9 ⍝ 60.8 +⍝ april:587 — Exact rational LCM of commensurable fractions +16ᵣ5∧19ᵣ10 ⍝ 304ᵣ5 -⍝ april:588 — -6.3∧5.1 ⍝ 107.1 - -⍝ april:589 — -⌈1000×1.3j2.6∧4.5j8.9 ⍝ ¯172900j232700 - -⍝ april:590 — -⌈1000×3.8j7.6∧5.2j6.8 ⍝ ¯159600j326800 +⍝ april:588 — Exact rational LCM of commensurable fractions +63ᵣ10∧51ᵣ10 ⍝ 1071ᵣ10 ⍝ april:594 — 0 1 0 1⊼0 0 1 1 ⍝ $t $t $t $f @@ -201,17 +195,11 @@ ⍝ april:599 — 0 1 0 1∨0 0 1 1 ⍝ 0 1 1 1 -⍝ april:600 — -3.2∨1.9 ⍝ 0.1 - -⍝ april:601 — -6.3∨5.1 ⍝ 0.3 - -⍝ april:602 — -1.3j2.6∨4.5j8.9 ⍝ 0.1 +⍝ april:600 — Exact rational GCD of commensurable fractions +16ᵣ5∨19ᵣ10 ⍝ 1ᵣ10 -⍝ april:603 — -3.8j7.6∨5.2j6.8 ⍝ 0.2 +⍝ april:601 — Exact rational GCD of commensurable fractions +63ᵣ10∨51ᵣ10 ⍝ 3ᵣ10 ⍝ april:607 — 0 1 0 1⊽0 0 1 1 ⍝ $t $f $f $f @@ -2226,9 +2214,6 @@ fn←2× ⋄ fn⍣3⁝4 ⍝ 32 ⍝ april:1753 — ↓⍣2 [2 2]⍴⍳4ₓ ⍝ (⊂[[0 1] [2 3]])ₓ -⍝ april:1754 — remove April numeric separators -⌊1000000000×2○⍣=1 ⍝ 739085133ₓ - ⍝ april:1755 — remove April numeric separators ⌊100000×{⍵{2.0÷⍨⍵+⍺÷⍵}⍣≡⍵}123456789 ⍝ 1111111106ₓ @@ -2844,8 +2829,8 @@ ef←{⍵+2} ⍝ april:2098 — Three-element averaging fork function train with operator-derived function +/÷1×≢ ⍳12ₓ ⍝ 5.5 -⍝ april:2100 — Five-element monadic fork function train -!⍴-,÷ 3 +⍝ april:2100 — Five-element monadic fork function train; integral factorial count with a float reciprocal +(!∘⌊)⍴-,÷ 3 ¯3 0.3333333333333333 ¯3 0.3333333333333333 ¯3 0.3333333333333333 ⍝ april:2101 — Five-element monadic fork function train with second argument value at end @@ -4806,10 +4791,10 @@ fibonacci¨⍳10ₓ ⍝ [0 1 1 2 3 5 8 13 21 34]ₓ ⍝ => [0 1 1 2 3 5 8 13 21 34]ₓ -⍝ april/libraries/dfns/numeric/demo.lisp:44 — -{1∧+↢÷/0,⍵#1}¨⍳10ₓ +⍝ april/libraries/dfns/numeric/demo.lisp:44 — Fibonacci numbers through exact continued fractions +{1ᵣ∧+↢÷/0ᵣ,⍵#1ᵣ}¨⍳10ₓ ⍝ => -0 1 1 2 3 5 8 13 21 34 +[0 1 1 2 3 5 8 13 21 34]ᵣ ⍝ april/libraries/dfns/numeric/demo.lisp:45 — [factors:]←•load "lib/numeric.bpl" @@ -5247,9 +5232,9 @@ stamps¨15 16 17 18 [cfract:]←•load "lib/numeric.bpl" cfract 5÷8 ⍝ [0 1 1 1 2]ₓ -⍝ april/libraries/dfns/numeric/demo.lisp:204 — Fixed CT=1E¯14 port; original April expectation retained +⍝ april/libraries/dfns/numeric/demo.lisp:204 — Exact rational input; original April expectation retained [cfract:]←•load "lib/numeric.bpl" -cfract 67÷29 ⍝ [2 3 4 2]ₓ +cfract 67ᵣ29 ⍝ [2 3 4 2]ᵣ ⍝ april/libraries/dfns/numeric/demo.lisp:205 — Fixed CT=1E¯14 port; original April expectation retained [cfract: to:]←•load "lib/numeric.bpl" @@ -5271,9 +5256,9 @@ cfract¨ ¯4 to 4 ⍝ => [1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1]ₓ -⍝ april/libraries/dfns/numeric/demo.lisp:209 — Fixed CT=1E¯14 port; original April expectation retained +⍝ april/libraries/dfns/numeric/demo.lisp:209 — Exact rational input; original April expectation retained [cfract:]←•load "lib/numeric.bpl" -cfract¯2.3 ⍝ [¯3 1 2 3]ₓ +cfract¯23ᵣ10 ⍝ [¯3 1 2 3]ᵣ ⍝ april/libraries/dfns/numeric/demo.lisp:210 — Euclidean continued fractions terminate at denominator zero, including non-coprime integer approximations; no change to approximate GCD or recursion limits [cfract:]←•load "lib/numeric.bpl" @@ -5631,14 +5616,6 @@ embrace ← {'(',⍵,')'} for {'[',⍵,']'} for {'{',⍵,'}'} for {'<',⍵,'>'} [invr:]←•load "lib/power.bpl" ⌊{32+⍵×1.8}invr 212 ⍝ 100ₓ -⍝ april/libraries/dfns/power/demo.lisp:21 — Upstream numerical-inverse library setup; its CT=1E-14 is the fixed language tolerance -[invr:]←•load "lib/power.bpl" -⌊10000×(1↣○)invr(1↣○)10 ⍝ 685398ₓ - -⍝ april/libraries/dfns/power/demo.lisp:22 — Upstream numerical-inverse library setup; its CT=1E-14 is the fixed language tolerance -[invr:]←•load "lib/power.bpl" -⌊1000× **invr 1.23 ⍝ 1230ₓ - ⍝ april/libraries/dfns/power/demo.lisp:23 — Upstream numerical-inverse library setup; its CT=1E-14 is the fixed language tolerance [invr:]←•load "lib/power.bpl" ⌊1000× ⍟⍟invr 1.23 ⍝ 1230ₓ @@ -5717,12 +5694,6 @@ enlist ,↢⊂/⍳5ₓ ⍝ [0 1 2 3 4]ₓ ⍝ => 7 22 11 34 17 52 26 13 40 20 10 5 16 8 4 2 1 -⍝ april/libraries/dfns/power/demo.lisp:42 — Dyalog independently confirms five Newton iterates at fixed CT, rather than April fixture six -[nr: traj:]←•load "lib/power.bpl" -⌊1000000000×1e¯12↣(2↣○ nr)traj 1.5 -⍝ => -[1500000000 1570909012 1570796336 1570796326 1570796326]ₓ - ⍝ april/libraries/dfns/power/demo.lisp:44 — Upstream Newton trajectory; fixed tolerance substituted for read-only ⎕CT [nr: traj:]←•load "lib/power.bpl" ⌊1000000000×1e¯3↣(2↣○ nr)traj 1.5 @@ -6936,40 +6907,18 @@ tt←tree 32¿32 revt←{↑⌽ ⍵ get ⍺} 32 check tt ⍝ $t -⍝ april/libraries/dfns/numeric/demo.lisp:271 — Phinary; original independent upstream expectation -[phinary:]←•load "lib/numeric.bpl" -phinary42 ⍝ "10100010.00100001" - ⍝ april/libraries/dfns/numeric/demo.lisp:272 — Phinary; original independent upstream expectation [align: phinary: to:]←•load "lib/numeric.bpl" ↓align phinary ¯4 to 16 ⍝ => " ¯101.01 " " ¯100.01 " " ¯10.01 " " ¯1 " " 0 " " 1 " " 10.01 " " 100.01 " " 101.01 " " 1000.1001 " " 1010.0001 " " 10000.0001 " " 10001.0001 " " 10010.0101 " " 10100.0101 " " 10101.0101 " "100000.101001" "100010.001001" "100100.001001" "100101.001001" "101000.100001" -⍝ april/libraries/dfns/numeric/demo.lisp:279 — Phinary; original independent upstream expectation -[align: phinary: to:]←•load "lib/numeric.bpl" -↓align phinary (1+√5 ÷ 2)* 0 to 10 -⍝ => -" 1" " 10" " 100" " 1000" " 10000" " 100000" " 1000000" " 10000000" " 100000000" " 1000000000" "10000000000" - -⍝ april/libraries/dfns/numeric/demo.lisp:283 — Phinary; original independent upstream expectation -[align: phinary: to:]←•load "lib/numeric.bpl" -↓align phinary (1+√5 ÷ 2)* ¯1 to ¯10 -⍝ => -"0.1 " "0.01 " "0.001 " "0.0001 " "0.00001 " "0.000001 " "0.0000001 " "0.00000001 " "0.000000001 " "0.0000000001" - ⍝ april/libraries/dfns/numeric/demo.lisp:286 — Phinary; original independent upstream expectation [phinary: to:]←•load "lib/numeric.bpl" ↓⊃ {(⍕⍵),": ",⍕ 0 phinary ⍵}¨ 0 to 9 ⍝ => "0: " "1: 0ₓ " "2: [1 ¯2]ₓ " "3: [2 ¯2]ₓ " "4: [2 0 ¯2]ₓ " "5: [3 ¯1 ¯4]ₓ " "6: [3 1 ¯4]ₓ " "7: [4 ¯4]ₓ " "8: [4 0 ¯4]ₓ " "9: [4 1 ¯2 ¯4]ₓ" -⍝ april/libraries/dfns/numeric/demo.lisp:289 — Phinary; original independent upstream expectation -[align: phinary: to:]←•load "lib/numeric.bpl" -↓align phinary (√5)* 0 to 6 -⍝ => -" 1 " " 10.1 " " 1000.1001 " " 100000.00001 " " 1010000.000101 " " 100010010.010001001 " "10000000010.0100000001" - ⍝ april/libraries/dfns/string/demo.lisp:304 — Wrap and justify a paragraph; reviewed independent Dyalog expectation; Supply concrete text in place of unavailable noteSample; compare the resulting lines [wrapnote:]←•load "lib/string.bpl" note←"Title",(•ucs 13 13),"one two three",(•ucs 13),"four five six" @@ -6998,11 +6947,11 @@ a←1 2 3 1 2 3 ⍝ ⎕: 1 2 3 -⍝ april/libraries/dfns/numeric/demo.lisp:136 — Recover fractions from approximate convergents +⍝ april/libraries/dfns/numeric/demo.lisp:136 — Recover fractions from exact convergents [rational:]←•load "lib/numeric.bpl" -rational (÷≥)⍣(⍳10ₓ)0.0 +rational (÷≥)⍣(⍳10ₓ)0ᵣ ⍝ => -[0 1 1 2 3 5 8 13 21 34⋄1 1 2 3 5 8 13 21 34 55] +[0 1 1 2 3 5 8 13 21 34⋄1 1 2 3 5 8 13 21 34 55]ᵣ ⍝ — Splay tree validity after searches (32 nodes; April tree demo:899) [foldl:]←•load "lib/array.bpl" @@ -7088,9 +7037,6 @@ hex ¯1+2*53 ⍝ ⊂"001fffffffffffff" ⍝ april:2362 — Imaginary number; Test Format text; use BasedPL's subscript ⱼ separator instead of April's uppercase J ⍕3j9 ⍝ "3ⱼ9" -⍝ april:410 — Commuted division follows the Mathish zero rule -÷⍨0 ⍝ 0 - ⍝ april:1933 — Strand assignment of nested scalar variable; Pure example translated to completed BasedPL semantics; independent upstream expectation retained; Remove redundant dialect settings; based folds return the result directly [a b c]←⊂3 3⍴1 ⋄ +/[a b c] ⍝ [3 3 3⋄3 3 3⋄3 3 3] @@ -7116,12 +7062,6 @@ scg_3 ← [3;4;3 4;0 2 4;1 2 3]ₓ g←scg_3 ⊃∪scc¨(⊂g)gperm¨↓pmat≢g ⍝ 1 5⍴[0 0 0 0 0]ₓ -⍝ april/libraries/dfns/numeric/demo.lisp:211 — Real rational recovery uses the first continued-fraction convergent within relative error 1e-14. Pi recovers 5419351/1725033, whose canonical continued fraction ends in 4. April's 14-column float-round-trip expectation is retained separately; 0+ makes coefficients approximate -[cfract: rational:]←•load "lib/numeric.bpl" -rational (+↢÷)/¨,\ 0+(0 cfract π1) -⍝ => -[3 22 333 355 103993 104348 208341 312689 833719 1146408 5419351⋄1 7 106 113 33102 33215 66317 99532 265381 364913 1725033] - ⍝ april:2311 — Namespace-as-data: empty keyed vector, dotted insertion and lookup myns←⍬:⍬ myns.aa←5 diff --git a/tests/reference/april_mathish.bpl b/tests/reference/april_mathish.bpl new file mode 100644 index 00000000..94b170e0 --- /dev/null +++ b/tests/reference/april_mathish.bpl @@ -0,0 +1,61 @@ +⍝⍝ Mathish complex arithmetic and numerical convergence +⍝ april:589 — +⌈1000×1.3j2.6∧4.5j8.9 ⍝ ¯172900j232700 + +⍝ april:590 — +⌈1000×3.8j7.6∧5.2j6.8 ⍝ ¯159600j326800 + +⍝ april:602 — +1.3j2.6∨4.5j8.9 ⍝ 0.1 + +⍝ april:603 — +3.8j7.6∨5.2j6.8 ⍝ 0.2 + +⍝ april:1754 — remove April numeric separators +⌊1000000000×2○⍣=1 ⍝ 739085133ₓ + +⍝ april/libraries/dfns/power/demo.lisp:21 — Upstream numerical-inverse library setup; its CT=1E-14 is the fixed language tolerance +[invr:]←•load "lib/power.bpl" +⌊10000×(1↣○)invr(1↣○)10 ⍝ 685398ₓ + +⍝ april/libraries/dfns/power/demo.lisp:22 — Upstream numerical-inverse library setup; its CT=1E-14 is the fixed language tolerance +[invr:]←•load "lib/power.bpl" +⌊1000× **invr 1.23 ⍝ 1230ₓ + +⍝ april/libraries/dfns/power/demo.lisp:42 — Dyalog independently confirms five Newton iterates at fixed CT, rather than April fixture six +[nr: traj:]←•load "lib/power.bpl" +⌊1000000000×1e¯12↣(2↣○ nr)traj 1.5 +⍝ => +[1500000000 1570909012 1570796336 1570796326 1570796326]ₓ + +⍝ april/libraries/dfns/numeric/demo.lisp:271 — Phinary; original independent upstream expectation +[phinary:]←•load "lib/numeric.bpl" +phinary42 ⍝ "10100010.00100001" + +⍝ april/libraries/dfns/numeric/demo.lisp:279 — Phinary; original independent upstream expectation +[align: phinary: to:]←•load "lib/numeric.bpl" +↓align phinary (1+√5 ÷ 2)* 0 to 10 +⍝ => +" 1" " 10" " 100" " 1000" " 10000" " 100000" " 1000000" " 10000000" " 100000000" " 1000000000" "10000000000" + +⍝ april/libraries/dfns/numeric/demo.lisp:283 — Phinary; original independent upstream expectation +[align: phinary: to:]←•load "lib/numeric.bpl" +↓align phinary (1+√5 ÷ 2)* ¯1 to ¯10 +⍝ => +"0.1 " "0.01 " "0.001 " "0.0001 " "0.00001 " "0.000001 " "0.0000001 " "0.00000001 " "0.000000001 " "0.0000000001" + +⍝ april/libraries/dfns/numeric/demo.lisp:289 — Phinary; original independent upstream expectation +[align: phinary: to:]←•load "lib/numeric.bpl" +↓align phinary (√5)* 0 to 6 +⍝ => +" 1 " " 10.1 " " 1000.1001 " " 100000.00001 " " 1010000.000101 " " 100010010.010001001 " "10000000010.0100000001" + +⍝ april:410 — Commuted division follows the Mathish zero rule +÷⍨0 ⍝ 0 + +⍝ april/libraries/dfns/numeric/demo.lisp:211 — Real rational recovery uses the first continued-fraction convergent within relative error 1e-14. Pi recovers 5419351/1725033, whose canonical continued fraction ends in 4. April's 14-column float-round-trip expectation is retained separately; 0+ makes coefficients approximate +[cfract: rational:]←•load "lib/numeric.bpl" +rational (+↢÷)/¨,\ 0+(0 cfract π1) +⍝ => +[3 22 333 355 103993 104348 208341 312689 833719 1146408 5419351⋄1 7 106 113 33102 33215 66317 99532 265381 364913 1725033] + diff --git a/tests/reference/core.bpl b/tests/reference/core.bpl index 8dc8af91..a6b48f91 100644 --- a/tests/reference/core.bpl +++ b/tests/reference/core.bpl @@ -447,6 +447,16 @@ x←2 3 1 ⋄ n←{(⍵+2÷⍵)÷2} ⋄ a←{z←⍕⍵ ⋄ ⌈/⍵}∂ x ⋄ b ⍝ program-item — `∂` runs from a program where a dfn reads one item of a traced array, as `↑`, `⊃`, subscripts and selection do. No trace stops v←1 2 3 4 ⋄ f←{(↑⍵)×+/⍵} ⋄ g←{(1⊃⍵)+⍵₃} ⋄ h←{+/⍵×⍵.[2]} ⋄ e←•explain "[f∂v ; g∂v ; h∂v]" ⋄ e.value,(≢e.programs.event),≢e.stopped.reason ⍝ [[11 1 1 1] [0 1 0 1] [3 3 13 3] 3ₓ 0ₓ] +⍝ pending-fill-scalar-read — A small read of deferred filled arithmetic preserves atom, vector and scalar-box selection shapes +a←128⍴0.5 ⋄ r←a×a ⋄ [r₁ ; ⍴r.[[1]] ; ⍴(⊂1)⌷r ; ↑r] ⍝ [0.25 [1]ₓ ⍬ₓ 0.25] + +⍝ pending-fill-static-floor — A scalar-box Copy keeps its planned float storage before a static floor reads it +•prefs ["kinds":"static" "float":32] ⋄ a←128⍴0.5 ⋄ r←⌊a ⋄ [r₁ ; (•storage r).kind ; ⍴3↑r] ⍝ [0 "float32" [3]ₓ] + +⍝ pending-fill-typed-items — Partial reads retain Boolean results and the fixed width of an integer fill's product +a←128⍴$t ⋄ b←128⍴2ₓ₂ ⋄ r←b×b ⋄ x←r₁ ⋄ [(~a)₁ ; x ; (•storage x).kind] ⍝ $f 4ₓ₂ "int16" + + ⍝ program-per-item — A dfn equivalent to multiplication uses the fold's program; Each through a guard on traced data runs without one v←1 2 3 4 ⋄ f←{{⍺×⍵}/⍵} ⋄ g←{+/{⍵>2?⍵×2;⍵}¨⍵} ⋄ e←•explain "[f∂v ; g∂v]" ⋄ e.value,(≢e.programs.event),≢e.stopped.reason ⍝ [[24 12 8 6] [1 1 2 2] 1ₓ 1ₓ] @@ -488,6 +498,22 @@ g←{m←2 3⍴⍵×2 ⋄ ⍵+0.001×(+/⍵)×+/0⊃↓m} ⋄ x←1 2 3 ⋄ q← ⍝ program-weak-atom — A weak atom keeps its weak flag in a recorded body: beside float32 data it gives float32 in a loop's traced step and in a map, as the untraced call does, and a strong float64 left argument gives float64 v←[1 2 3]ₛ ⋄ s←+/[2.5] ⋄ [(•storage 2.5({⍺+⍵}⍣2)v).kind ; (•storage s({⍺+⍵}⍣2)v).kind ; (•storage 2.5 {⍺+⍵}¨ v).kind] ⍝ "float32" "float64" "float32" +⍝ program-weak-spelling — A weak bound tine and a dfn literal use the same typed preparation beside fixed data +•prefs ["kinds":"static" "mathish":$f] ⋄ v←[0 1 2]ₓ₁ ⋄ x←[1 2 3]ₛ ⋄ d←{+/(1ₓ+v)×⍵} ⋄ t←+/((1ₓ+)∘(v⍨))×⊢ ⋄ e←•explain "[d∂x;t∂x]" ⋄ [e.value≡2⍴⊂[1 2 3]ₛ;2=≢e.programs.event;≢e.stopped.reason] ⍝ $t $t 0ₓ + +⍝ program-weak-classes — Fresh bound integers replay within a minimum-width class and use the proper width across the 255/256 boundary +•prefs ["kinds":"static" "mathish":$f] ⋄ v←[0 1 2]ₓ₁ ⋄ x←[1 2 3]ₛ ⋄ g←{(+/((⍺+)∘(v⍨))×⊢)∂⍵} ⋄ e←•explain "[1ₓ g x;255ₓ g x;256ₓ g x;257ₓ g x]" ⋄ [e.value≡[1 2 3;255 0 1;256 257 258;257 258 259];(4=≢e.programs.event)∧e.programs.event.[[1 3]ₓ]≡2⍴⊂"replayed"] ⍝ $t $t + +⍝ program-weak-computed — An input keeps its class while adding one crosses 255/256; computed promotion and a reduction seed decline rather than reuse the first width +•prefs ["kinds":"static" "mathish":$f] ⋄ v←[0 1 2]ₓ₁ ⋄ x←[1 2 3]ₛ ⋄ f←{+/((⍺+1ₓ)+v)×⍵} ⋄ g←{((⍺+1ₓ)+/v)×+/⍵} ⋄ e←•explain "[254ₓ(f∂)x;255ₓ(f∂)x;254ₓ(g∂)x;255ₓ(g∂)x]" ⋄ [e.value≡[255 0 1;256 257 258;2 2 2;259 259 259];≢e.programs.event] ⍝ $t 0ₓ + +⍝ program-weak-observed — Observing a capture as a count also guards its weakness before reusing arithmetic on it +•prefs ["kinds":"dynamic" "mathish":$f] ⋄ p←[254 255]ₓ₁ ⋄ x←[1 2 3]ₛ ⋄ a←2ₓ ⋄ f←{q←a↑⍵ ⋄ +/(a+p)×q} ⋄ e←•explain "f∂x" ⋄ a←↑,2ₓ ⋄ b←•explain "f∂x" ⋄ [e.value≡[0 1 0]ₛ;b.value≡[256 257 0]ₛ;1=≢e.programs.event] ⍝ $t $t $t + +⍝ program-weak-polymorphic — Changing weak floating captures and fresh bound tines keep value-polymorphic replay +•prefs ["kinds":"dynamic" "mathish":$f] ⋄ v←[1 2 3]ₛ ⋄ a←1 ⋄ f←{+/a×⍵} ⋄ e←•explain "r←f∂v ⋄ a←1.5 ⋄ s←f∂v ⋄ a←256 ⋄ t←f∂v ⋄ a←257 ⋄ [r;s;t;f∂v]" ⋄ •prefs ["kinds":"static"] ⋄ g←{(+/⍺×⊢)∂⍵} ⋄ t←•explain "[1 g v;1.5 g v;256 g v;257 g v]" ⋄ wanted←[1 1 1;1.5 1.5 1.5;256 256 256;257 257 257] ⋄ events←3⍴⊂"replayed" ⋄ [e.value≡wanted;t.value≡wanted;(1↓e.programs.event)≡events;(1↓t.programs.event)≡events] ⍝ $t $t $t $t + + ⍝ — Power computes the same result when its body reads the clock before and after arithmetic h←{t←•time 0 ⋄ y←1+2×3×⍵ ⋄ z←0≤•time⁝t ⋄ y×z} ⋄ +/h⍣3⁝1 2 3 ⍝ 1425 @@ -1085,6 +1111,12 @@ m←¿[1024 1024]⍴0 ⋄ g←{+/⌽⍵} ⍝ — Square root extends into complex numbers √0 9 ¯4 3j4 ⍝ 0 3 0j2 2j1 +⍝ planned-scalar-domain — A saved plan retains a domain proved by its observed scalar-array operand +√⊂4.0 ⍝ ⊂2 + +⍝ planned-scalar-domain-broadcast — Broadcasting the scalar result retains its declared domain +(√⊂4.0)+1 2 ⍝ 3 4 + ⍝ — Roots of exact rationals and big integers are approximate too √1r9 18446744073709551616ᵣ ⍝ 0.3333333333333333 4294967296 @@ -4267,9 +4299,6 @@ $t $f $t ⍭¯1 ⍝ error: DOMAIN ERROR -⍝ — A prime index accepts a float within comparison tolerance of an integer -⍭1.000000000000001 ⍝ 3ₓ - ⍝ — 1⍭2.5 ⍝ error: DOMAIN ERROR @@ -4327,7 +4356,11 @@ $t $f $t ([¯3 ¯2⋄2 1]↕4 5⍴⍳20ₓ)≡⊢⌺[3 2⋄2 1] 4 5⍴⍳20ₓ ⍝ $t ⍝ window-fold-slides — A fold along a vector's or matrix's windows slides over the source, directly for narrow windows and by van Herk-Gil-Werman for wide ones, and matches folding each window alone, NaN included -v←¿3000⍴0 ⋄ i←17 1000 ⋄ v.[i]←$n ⋄ w←¿45000⍴0 ⋄ m←¿[130 130]⍴0 ⋄ j←¿3000⍴100ₓ ⋄ b←0=¿3000⍴3ₓ ⋄ ∧/[(+/50↕v)≡({+/⍵}⍤1)50↕v;(⌈/50↕w)≡({⌈/⍵}⍤1)50↕w;(⌊/3↕w)≡({⌊/⍵}⍤1)3↕w;(+/+/3 3↕m)≡({+/,⍵}⍤2)3 3↕m;(+/50↕j)≡({+/⍵}⍤1)50↕j;(∨/50↕b)≡({∨/⍵}⍤1)50↕b;(⌈⌿3↕w)≡({⌈/⍵}⍤1)⍉3↕w;↑["sliding fold"]∊(•explain "+/50↕v").routes.route] ⍝ $t +•prefs ["seed":0] ⋄ v←¿3000⍴0 ⋄ i←17 1000 ⋄ v.[i]←$n ⋄ w←¿45000⍴0 ⋄ m←¿[130 130]⍴0 ⋄ j←¿3000⍴100ₓ ⋄ b←0=¿3000⍴3ₓ +[+/50↕v ; ⌈/50↕w ; ⌊/3↕w ; +/+/3 3↕m ; +/50↕j ; ∨/50↕b ; ⌈⌿3↕w ; ↑["sliding fold"]∊(•explain "+/50↕v").routes.route] +⍝ => +•prefs ["seed":0] ⋄ v←¿3000⍴0 ⋄ i←17 1000 ⋄ v.[i]←$n ⋄ w←¿45000⍴0 ⋄ m←¿[130 130]⍴0 ⋄ j←¿3000⍴100ₓ ⋄ b←0=¿3000⍴3ₓ +[({+/⍵}⍤1)50↕v ; ({⌈/⍵}⍤1)50↕w ; ({⌊/⍵}⍤1)3↕w ; ({+/,⍵}⍤2)3 3↕m ; ({+/⍵}⍤1)50↕j ; ({∨/⍵}⍤1)50↕b ; ({⌈/⍵}⍤1)⍉3↕w ; $t] ⍝ window-axis-fold-slides — A fold along two axes of windows whose positions each hold several items, as pooling over channels has them, folds both axes in one pass over the source, for windows that tile or slide, under Rank too, and matches folding one axis at a time a←¿[256 256 4]⍴0 ⋄ j←(¿[130 130 2]⍴100)ₓ ⋄ r←¿[16 64 64 4]⍴0 ⋄ ∧/[((⌈/₂ ₃)[[2 2]⋄[2 2]]↕a)≡(⌈/₂)(⌈/₂)[[2 2]⋄[2 2]]↕a;((+/₂ ₃)3 3↕j)≡(+/₂)(+/₂)3 3↕j;({⌈/₂ ₃ [[2 2]⋄[2 2]]↕⍵}⍤3 r)≡{⌈/₂⌈/₂ [[2 2]⋄[2 2]]↕⍵}⍤3 r;↑["sliding fold"]∊(•explain "(⌈/⍠2 3)[[2 2]⋄[2 2]]↕a").routes.route] ⍝ $t @@ -4348,7 +4381,11 @@ x←⍳5ₓ ⋄ [["size":3 "edge":"clamp"]↕x;["size":3 "edge":"reflect" "step" x←⍳5ₓ ⋄ ∧/[(["size":[3]]↕x)≡3↕x;(["size":¯3]↕x)≡¯3↕x;(["size":3 "edge":"fill"]↕x)≡¯3↕x;(["size":[3 2] "step":[1 2]]↕4 5⍴⍳20ₓ)≡[3 2⋄1 2]↕4 5⍴⍳20ₓ] ⍝ $t ⍝ window-padded-fold-slides — Folds of wrapped, clamped and filled windows slide over the padded source, a chosen fill included, and a product of them reads the windows in blocks -v←¿3000⍴0 ⋄ k←¿3 4⍴0 ⋄ wr←["size":50 "edge":"wrap"] ⋄ cl←["size":3 "edge":"clamp"] ⋄ fl←["size":¯51 "fill":2] ⋄ ∧/[(+/wr↕v)≡({+/⍵}⍤1)wr↕v;(⌈/cl↕v)≡({⌈/⍵}⍤1)cl↕v;(+/fl↕v)≡({+/⍵}⍤1)fl↕v;↑["sliding fold"]∊(•explain "+/wr↕v").routes.route;↑["sliding fold"]∊(•explain "+/fl↕v").routes.route;(([3000 3]⍴cl↕v)+.×k)≡(1×[3000 3]⍴cl↕v)+.×k] ⍝ $t +•prefs ["seed":0] ⋄ v←¿3000⍴0 ⋄ k←¿3 4⍴0 ⋄ wr←["size":50 "edge":"wrap"] ⋄ cl←["size":3 "edge":"clamp"] ⋄ fl←["size":¯51 "fill":2] +[+/wr↕v ; ⌈/cl↕v ; +/fl↕v ; ↑["sliding fold"]∊(•explain "+/wr↕v").routes.route ; ↑["sliding fold"]∊(•explain "+/fl↕v").routes.route ; ([3000 3]⍴cl↕v)+.×k] +⍝ => +•prefs ["seed":0] ⋄ v←¿3000⍴0 ⋄ k←¿3 4⍴0 ⋄ wr←["size":50 "edge":"wrap"] ⋄ cl←["size":3 "edge":"clamp"] ⋄ fl←["size":¯51 "fill":2] +[({+/⍵}⍤1)wr↕v ; ({⌈/⍵}⍤1)cl↕v ; ({+/⍵}⍤1)fl↕v ; $t ; $t ; (1×[3000 3]⍴cl↕v)+.×k] ⍝ — ["size":3 "pad":1]↕⍳3ₓ @@ -4644,7 +4681,7 @@ x←¯1j0.1 1j¯1 +⍨⁻3ᵣ ⍝ 3r2 ⍝ — Inverse sine selects a branch which maps back to the input -0.5=1↣○1↣○⁻0.5 ⍝ $t +1↣○1↣○⁻0.5 ⍝ 0.5 ⍝ — Inverse sine within the real domain has no imaginary component ⊢/∨1↣○⁻0.5 ⍝ 0 @@ -4993,12 +5030,6 @@ x←[[1 2 6]ₓ÷[1 2 3]ₓ;⍳3ₓ;1ₓ+[2 3]ₓ;10ₓ-[2 3]ₓ;[2 3]ₓ×4ₓ; ⍝ — Truth values are Booleans, whatever the arguments x←["ab"∊"b";~0 1;0 1⊼1 1;0 1⊽0 0] ⋄ [x {(•storage ⍵).kind}¨x] ⍝ [[[$f $t] [$t $f] [$t $f] [$t $f]];4⍴⊂"bool"] -⍝ logical-numeric-truth — a float stands for a Boolean within tolerance only under Mathish -n←1-1e¯15 ⋄ on←[~[0 n];(1+1e¯15)⊼n;0⊽n] -•prefs ["mathish":$f] ⋄ off←{"DOMAIN"::1 ⋄ ~n ⋄ 0}0 ⋄ •prefs ["mathish":$t] ⋄ [on;off] -⍝ => -[[[$t $f] $f $f] 1] - ⍝ — Structural functions keep machine storage. A rational prototype stays unbounded x←[⌽⍳3ₓ;1⌽⍳3ₓ;5↑⍳3ₓ;1↓⍳3ₓ;[[2 0]]⌷⍳3ₓ;1 0 1#⍳3ₓ;1 0 1#⁻[1 2]ₓ;+\⍳3ₓ;-¨⍳3ₓ;[1 2]ₓ∪[2 3]ₓ;[1 2]ₓ∩[2]ₓ; 2ₓ~[1]ₓ;,⊃[[1 2]ₓ 3ₓ];,⍉2 2⍴⍳4ₓ;,(1ₓ+⍳2ₓ)×⊗1ₓ+⍳2ₓ;0ₓ@1⍳3ₓ;{+/,⍵}⌺3⍳3ₓ;[2 2]ₓ⊤3ₓ;⍴2 3⍴1ₓ;⍳0ₓ;⍴1ₓ;↑0⍴⊂1r2 1r3] @@ -5551,10 +5582,12 @@ x ⍝ 10 20 9 ⍋[1 (1+8E¯15) 1] ⍝ [0 2 1]ₓ ⍝ — Fast combinations give what their parts give in turn: partial grades and sorts, ranks, and counts and folds under Key +•prefs ["seed":0] v←¿1000⍴0 ⋄ k←¿1000⍴7 ⋄ b←0=¿1000⍴3 ⋄ d←⍳¯1000 ⋄ z←v ⋄ z.[[3 500 999]]←$n ⋄ z.[[7 9]]←-0 -[((10↑)∘⍋ k)≡({10↑⍋⍵} k); ((¯10↑)∘⍒ k)≡({¯10↑⍒⍵} k); ((10↑)∘< d)≡({10↑<⍵} d); (¯10↑> d)≡({¯10↑>⍵} d); ((5↑)∘⍋ z)≡({5↑⍋⍵} z); ((¯5↑)∘⍋ z)≡({¯5↑⍋⍵} z); ((10↑)∘⍋ v)≡({10↑⍋⍵} v); (¯10↑< v)≡({¯10↑<⍵} v); (↑> v)≡({↑>⍵} v); ((3⌷)∘⍒ v)≡({3⌷⍒⍵} v); (⍋⍋ v)≡({⍋⍵}⍋v); (⍋⍒ v)≡({⍋⍵}⍒v); ((≢⊢)⌸ k)≡({≢⍵}⌸ k); (k (+/⊢)⌸ v)≡(k {+/⍵}⌸ v); (k (⌈⌿⊢)⌸ v)≡(k {⌈⌿⍵}⌸ v); (k (⌊/⊢)⌸ b)≡(k {⌊/⍵}⌸ b); (k (+/⊢)⌸ k)≡(k {+/⍵}⌸ k)] +[((10↑)∘⍋ k)≡({10↑⍋⍵} k); ((¯10↑)∘⍒ k)≡({¯10↑⍒⍵} k); ((10↑)∘< d)≡({10↑<⍵} d); (¯10↑> d)≡({¯10↑>⍵} d); ((5↑)∘⍋ z)≡({5↑⍋⍵} z); ((¯5↑)∘⍋ z)≡({¯5↑⍋⍵} z); ((10↑)∘⍋ v)≡({10↑⍋⍵} v); (¯10↑< v)≡({¯10↑<⍵} v); (↑> v)≡({↑>⍵} v); ((3⌷)∘⍒ v)≡({3⌷⍒⍵} v); (⍋⍋ v)≡({⍋⍵}⍋v); (⍋⍒ v)≡({⍋⍵}⍒v); ((≢⊢)⌸ k)≡({≢⍵}⌸ k); k (+/⊢)⌸ v; (k (⌈⌿⊢)⌸ v)≡(k {⌈⌿⍵}⌸ v); (k (⌊/⊢)⌸ b)≡(k {⌊/⍵}⌸ b); (k (+/⊢)⌸ k)≡(k {+/⍵}⌸ k)] ⍝ => -$t $t $t $t $t $t $t $t $t $t $t $t $t $t $t $t $t +•prefs ["seed":0] ⋄ v←¿1000⍴0 ⋄ k←¿1000⍴7 +[$t;$t;$t;$t;$t;$t;$t;$t;$t;$t;$t;$t;$t;k {+/⍵}⌸ v;$t;$t;$t] ⍝ selected-prefix-route — A prefix of a grade runs as a partial grade, which selects its items without grading the rest ⍝⍝⍝ route partial grade @@ -5700,9 +5733,6 @@ v←⍳1000ₓ ⋄ +/v+v ⍝ 999000ₓ ⍝ — +/[1 2 3]ₓ ⍝ 6ₓ -⍝ — Mathish division gives zero for zero divided by zero in arrays -0 1÷0 2 ⍝ 0 0.5 - ⍝ — Structure keeps float storage, and an empty array takes its prototype's storage {(•storage ⍵).kind}¨[⌽⍳3;2 3⍴⍳6;0 3⍴0;0⍴1ₓ] ⍝ "float64" "float64" "float64" "integer" @@ -5851,8 +5881,13 @@ y←¿64 8 4⍴0 ⋄ z←¿8 4 64⍴0 ⋄ [h g]←•index 8 4 ⋄ f←•explai [["+.×"] 0⍴⊂"" $t $t $t ["+.×"] $t] ⍝ fusion:contraction-gradient — `∂` through a contraction in index notation, along one axis or several, gives the gradient of the same product written tacitly. A cotangent that holds one value, as `+/,` gives, factors out of the gradient's fold, and no contraction declines +•prefs ["seed":0] x←¿32 16⍴0 ⋄ w←¿16 24⍴0 ⋄ [r k c]←•index 32 16 24 ⋄ y←¿32 4 4⍴0 ⋄ z←4 4 24⍴w ⋄ [h g]←•index 4 4 ⋄ e←•explain "{+/,+/⍠k⍵.[r k]×w.[k c]}∂x" -[e.value≡{+/,⍵+.×w}∂x ; ({+/,+/⍠[h g]⍵.[r h g]×z.[h g c]}∂y)≡{+/,(32 16⍴⍵)+.×w}∂y ; ∨/e.declined.route≡¨⊂"contraction"] ⍝ $t $t $f +[e.value ; {+/,+/⍠[h g]⍵.[r h g]×z.[h g c]}∂y ; ∨/e.declined.route≡¨⊂"contraction"] +⍝ => +•prefs ["seed":0] +x←¿32 16⍴0 ⋄ w←¿16 24⍴0 ⋄ y←¿32 4 4⍴0 +[{+/,⍵+.×w}∂x ; {+/,(32 16⍴⍵)+.×w}∂y ; $f] ⍝ program-storage-order — A program stores a map in the order of a value that it reads in another order. In `b.[c]++/⍠k⍵.[r k]×w.[k c]`, named agreement puts `c` first, while the product holds `[r c]`. The sum and each map after it then store as `[r c]`, and a fold along `c` folds that order. Both gradients match the same models written tacitly, within the last bits that a fold's order can change. x←0.1×8 4⍴⍳32 ⋄ w←0.01×4 6⍴⍳24 ⋄ b←0.1×⍳6 ⋄ v←0.1×⍳6 ⋄ [r k c]←•index 8 4 6 ⋄ close←{(⌈/,|⍺-⍵)≤1e¯12×⌈/,|⍵} @@ -5927,14 +5962,17 @@ v←⍳262144ₓ ⋄ s←1+v ⋄ [5↑1+v;3⊃2×v+1;5↑s] ⍝ [1 2 3 4 5;8;1 x←[4 256 256]⍴0.5 ⋄ e←•explain "(-⍤2) x+1" ⋄ [0ₓ=≢e.forced.function;e.value≡-x+1] ⍝ $t $t ⍝ — `+.×` on a pending copy of windows, and its gradients, give what they give on a stored copy, and the copy stays pending +•prefs ["seed":0] conv←{ img←¿64 64 16⍴0 ⋄ k←¿144 8⍴0 - w←[3844 144]⍴3 3↕img ⋄ wf←[3844 144]⍴(3 3↕img)+0 - same←((w+.×k)≡wf+.×k)∧({+/,w+.×⍵}∂k)≡{+/,wf+.×⍵}∂k - image←({+/,([3844 144]⍴3 3↕⍵)+.×k}∂img)≡0.5×{+/,(2×[3844 144]⍴3 3↕⍵)+.×k}∂img - [same image (•storage w).pending] + w←[3844 144]⍴3 3↕img + [w+.×k ; {+/,w+.×⍵}∂k ; {+/,([3844 144]⍴3 3↕⍵)+.×k}∂img ; (•storage w).pending] } -conv 0 ⍝ 3⍴$t +conv 0 +⍝ => +•prefs ["seed":0] +img←¿64 64 16⍴0 ⋄ k←¿144 8⍴0 ⋄ wf←[3844 144]⍴(3 3↕img)+0 +[wf+.×k ; {+/,wf+.×⍵}∂k ; 0.5×{+/,(2×[3844 144]⍴3 3↕⍵)+.×k}∂img ; $t] ⍝ fusion:reshaped-transpose-product — A product reads a reshaped pending transpose in its new matrix dimensions x←[2 65536]⍴⍳131072ₓ @@ -6466,9 +6504,6 @@ u←•UcS ⋄ u ["A⍳"⋄"λ😀"] ⍝ [65 9075⋄955 128512]ₓ 'a'×2 ⍝ error: DOMAIN ERROR -⍝ — A character offset accepts a float within comparison tolerance of an integer -'a'+1.000000000000001 ⍝ 'b' - ⍝ — 'a'+∞ ⍝ error: DOMAIN ERROR @@ -7044,12 +7079,6 @@ x←["b":2 "h":3 "w":4]⍴⍳24ₓ ⍝ — Complex floor uses Gaussian-integer cells, not independent component floors ⌊1.5j0.5 ⍝ 1j1 -⍝ — Complex gcd uses Gaussian-integer arithmetic and tolerant comparison -0.2=3.8j7.6∨5.2j6.8 ⍝ $t - -⍝ — Scale and ceil to expose the complex lcm despite floating-point roundoff -⌈1000×3.8j7.6∧5.2j6.8 ⍝ ¯159600j326800 - ⍝ — |3 ¯3 3j4 ⍝ 3 3 5 @@ -7065,9 +7094,6 @@ x←[¯7 7 0 ¯9223372036854775808 9223372036854775807]ₓ ⍝ => [[2 1 0 1 1]ₓ [¯1 ¯2 0 ¯2 ¯2]ₓ [1 3 0 0 3]ₓ [0 0 0 0 0]ₓ] -⍝ — Floor applies comparison tolerance near integers, including across a storage-width boundary -[⌊1.000000000000001 ¯1.000000000000001;⌊255.9999999999999 0] ⍝ [[1 ¯1]ₓ [256 0]ₓ] - ⍝ — Ceiling of a float is an exact integer ⌈¯2.3 0.1 3 ⍝ [¯2 1 3]ₓ @@ -7080,12 +7106,6 @@ x←[¯7 7 0 ¯9223372036854775808 9223372036854775807]ₓ ⍝ — Signum of a float is an exact integer ׯ2.5 0 ∞ ⍝ [¯1 0 1]ₓ -⍝ — An integer argument accepts a float within comparison tolerance of an integer -(0.1×3 × 10)⌷10 20 30 40 ⍝ 40 - -⍝ — A count accepts a float within comparison tolerance of an integer -⍳(0.1×3)×10 ⍝ 0 1 2 - ⍝ — A count still rejects a fraction outside tolerance ⍳2.5 ⍝ error: DOMAIN ERROR @@ -7134,33 +7154,11 @@ x←[¯7 7 0 ¯9223372036854775808 9223372036854775807]ₓ ⍝ — Euler's identity holds within tolerance *π0j1 ⍝ ¯1 -⍝ — The gcd of two reals recovers their rational ratio, in either order [rtol=1e-14 atol=0] -x←[1 1ₓ ¯1 0 0 1 1E¯200 1E200 1E¯200] -y←[103993÷33102 π1 ¯103993÷33102 ¯0.75 0 1.000000000000005 3E¯200 3E200 1E200] -[x∨y y∨x] -⍝ => -2⍴⊂[÷33102 ÷1725033 ÷33102 0.75 0 1 1E¯200 1E200 1E¯200] - -⍝ — Reals outside comparison tolerance of a rational ratio have a tiny gcd -1E¯12>1∨1.00000000000005 ⍝ $t - ⍝ — The gcd of NaN and any real is NaN [$n∨3.5 3.5∨$n $n∨$n 0∨$n $n∧2.5] ⍝ $n $n $n $n $n ⍝⍝ Search depth and random -⍝ — Index-of compares each candidate directly; tolerance is not transitive -x←1 -y←1+8E¯15 -z←1+16E¯15 -[x y]⍳z ⍝ 1ₓ - -⍝ — Unique-mask puts a chain of tolerant matches in one class -≠[1 1+8E¯15 1+16E¯15] ⍝ $t $f $f - -⍝ — A row is a copy of the first earlier distinct row that it matches, so rows don't chain -≠[1 2⋄1+8E¯15 2⋄1+16E¯15 2] ⍝ $t $f $t - ⍝ — Monadic `∩` keeps each distinct major cell that occurs more than once, in order of first appearance ∩"mississippi" ⍝ "isp" @@ -7206,24 +7204,12 @@ z←1+16E¯15 ⍝ — Positions count along the whole axis, also in a group much smaller than the axis 1⊃⍷(300⍴0),1 1 ⍝ [300 301]ₓ -⍝ — A large index-of takes the first position within tolerance, not the nearest value -(300⍴5 , [1+16E¯15 1])⍳1+8E¯15 ⍝ 300ₓ - -⍝ — A large index-of finds nothing outside tolerance -(300⍴5 , 1)⍳1+2E¯14 ⍝ 301ₓ - ⍝ — A large search never matches a character with its code point (97ₓ+⍳8ₓ)⍳36⍴"abcdefgh" ⍝ 36⍴8ₓ -⍝ — Large membership compares exact integers with floats within tolerance -[1+8E¯15 1+2E¯14]∊⍳300ₓ ⍝ $t $f - ⍝ — A large index-of of exact values applies no tolerance (⍳300ₓ)⍳1000000000000001r1000000000000000 ⍝ 300ₓ -⍝ — Without uses tolerance when approximate values participate -1 2~[1+8E¯15] ⍝ [2] - ⍝ — Without does not apply tolerance to all-exact values [1 2]ₓ~[1000000000000001r1000000000000000] ⍝ [1 2]ₓ @@ -7257,30 +7243,9 @@ z←1+16E¯15 ⍝ — Union retains duplicates within either argument 1 1∪2 2 ⍝ 1 1 2 2 -⍝ — Membership uses tolerance when approximate values participate -1 2∊[1+8E¯15] ⍝ $t $f - ⍝ — A vector pattern does not fit a unit search target (,1)⍷1 ⍝ $f -⍝ native-search-shapes — Compact and mixed patterns share shape traversal; fractional needles keep tolerant matching -p←[1 [2 3]] ⋄ v←[1 [2 3] 0 1 [2 3]] -[p⍷v [1 2]⍷[1 2 1⋄1 2 3] [1 2 3]ₓ⍳[2 2.000000000000001]] -⍝ => -[$t $f $f $t $f;[$t $f $f⋄$t $f $f];[1 1]ₓ] - -⍝ native-find-widths — Compact float patterns use the wider operand's tolerance and match NaNs -[(32•float[1])⍷[1+8E¯15] (32•float[1])⍷32•float[1+8E¯7] [$n]⍷[$n 0]] -⍝ => -[[$t] [$f] [$t $f]] - -⍝ exact-key-search — A search gives the same positions whether every cell is an exact key or one float cell sends them all through `Value::matches`, and for strings held as items or as a matrix's rows -h←["ab";"cd";"";7ₓ;'x';[3 4]ₓ;"abc";1ₓ] ⋄ ns←32⍴["cd";"zz";"";7ₓ;'x';[3 4]ₓ;"ab";'y';"abc";$t] -s←["ab";"cd";"ef";"gh";"ij";"kl";"mn";"op"] ⋄ t←32⍴s,⊂"zz" -[(ns∊h)≡ns∊h,⊂0.5 ; (≠h,ns)≡¯1↓≠h,ns,⊂0.5 ; (h⍳ns)≡8⌊(h,⊂0.5)⍳ns ; (s⍳t)≡(⊃s)⍳⊃t ; 10↑h⍳ns ; h⍳1 1+1E¯15] -⍝ => -[$t;$t;$t;$t;[1 8 2 3 4 5 0 8 6 7]ₓ;[7 7]ₓ] - ⍝ shared-classes — Items that share a few strings, as a selection makes them, class as the same strings held as arrays of their own do, for `=`, `∪`, `≠` and Key, beyond the items read before a search of arrays of their own gives up s←{"S",⍕⍵}¨⍳7ₓ ⋄ k←[2000⍴[3 1 4 1 5 6 2 0]ₓ]⌷s ⋄ d←{⍵,""}¨k ⋄ ∧/[(=k)≡=d ; (∪k)≡∪d ; (≠k)≡≠d ; (k{≢⍵}⌸⍳≢k)≡d{≢⍵}⌸⍳≢d ; (∪k)≡[[3 1 4 5 6 2 0]]⌷s] ⍝ $t @@ -7548,9 +7513,6 @@ m←[4 3]⍴2ₓ 5ₓ 2ₓ 7ₓ 1ₓ 5ₓ 3ₓ 3ₓ 9ₓ 5ₓ 5ₓ 0ₓ ⋄ {⍵ ⍝ — Membership of the same needles in each cell's own haystack m←[4 3]⍴2ₓ 5ₓ 2ₓ 7ₓ 1ₓ 5ₓ 3ₓ 3ₓ 9ₓ 5ₓ 5ₓ 0ₓ ⋄ {(5ₓ 3ₓ 8ₓ)∊⍵}⍤1 m ⍝ [$t $f $f⋄$t $f $f⋄$f $t $f⋄$t $f $f] -⍝ — Interval index in each cell's own boundaries, and index of within the tolerance -[({⍵⍸2.5}⍤1)[2 3]⍴1 2 3 0 5 10 ; ({⍵⍳0.3}⍤1)[2 3]⍴0.1 0.2 0.30000000000000004 0.3 0.1 0.2] ⍝ [[2 1]ₓ ; [2 0]ₓ] - ⍝ — A search against each cell's own haystack makes no fallback [batched] m←[4 3]⍴2ₓ 5ₓ 2ₓ 7ₓ 1ₓ 5ₓ 3ₓ 3ₓ 9ₓ 5ₓ 5ₓ 0ₓ ⋄ (•explain "{⍵⍳5ₓ}⍤1 m").fallbacks.reason ⍝ 0⍴⊂"" @@ -7595,10 +7557,6 @@ f←{"DOMAIN"::¯1 ⋄ ⍵○0.5} ⋄ r←f¨1 2 13 m←4 6⍴⍳24 ⋄ t←2 3 4⍴⍳24 [({["size":3;"edge":"wrap"]↕⍵}⍤1 m)≡4 6 3⍴["size":1 3;"edge":"wrap"]↕m ; ({(×/⍴⍵)⍴⍵}⍤2 t)≡2 12⍴t ; (•explain "{3↕⍵}⍤1 m").fallbacks.reason,(•explain "{(×/⍴⍵)⍴⍵}⍤2 t").fallbacks.reason] ⍝ [$t $t 0⍴⊂""] -⍝ — `≡` compares a batch's exact-integer or Boolean cells with a float constant on the batch's whole, within the tolerance [batched] -m←3 2⍴⍳6ₓ ⋄ b←2 2⍴$t $f $f $t ⋄ c←2 3.0000000000000004 -[{⍵≡c}⍤1 m ; {⍵≡1 0.0}⍤1 b ; (•explain "{⍵≡c}⍤1 m").fallbacks.reason] ⍝ [[$f $t $f] [$t $f] 0⍴⊂""] - ⍝ — A record built in a batched run holds a batch for each entry, so Rank and Each whose results are records batch [batched] v←⍳4 [{["a":⍵ "b":2×⍵]}⍤0 v ; (•explain "{[""a"":⍵ ""b"":2×⍵]}⍤0 v").fallbacks.reason,(•explain "{[""a"":⍵ ""b"":2×⍵]}¨v").fallbacks.reason] ⍝ [([0 1 2 3;"a" "b"]:[0 0⋄1 2⋄2 4⋄3 6]) 0⍴⊂""] @@ -8001,8 +7959,9 @@ w←[1 2⋄3 4] ⋄ x←1 2⍴10 20 ⋄ [w {a←⍺ ⋄ {+/a×⍵}∂⍵}¹ ¹ x ⍝ — A loss mapped over the rows of `X` has the gradient of the same loss written with `+.×` X←[3 2]⍴0.1×⍳6ₓ loss←{th←⍵ ⋄ +/{⊼1○+/th×⍵}⍤1X} -lossw←{+/⊼1○X+.×⍵} -(loss∂ 0.5 ¯0.25)≡lossw∂ 0.5 ¯0.25 ⍝ $t +loss∂ 0.5 ¯0.25 +⍝ => +X←[3 2]⍴0.1×⍳6ₓ ⋄ {+/⊼1○X+.×⍵}∂ 0.5 ¯0.25 ⍝ — Per-example gradients under Rank match the closed form X←[3 2]⍴0.1×⍳6ₓ @@ -8173,9 +8132,6 @@ f←{⎕←⍵ ⋄ ⍵} ⋄ 2 +⍥f 3 ⍝ — 2↣×⍣0⁝3 ⍝ 3 -⍝ — Tolerant Key puts a chain of matches in one group -{≢⍵}⌸[1 1+8E¯15 1+16E¯15] ⍝ [3]ₓ - ⍝ — A vector's keys reach the operand as its items, as Each passes them "ab" "cd" "ab"{⍺≡"ab"}⌸1 2 3 ⍝ $t $f @@ -8313,7 +8269,7 @@ f←{⍵×+⍵} 1e200×[(∨4○0.4j1e¯200)₁ (∨0○1e¯200j0.4)₁ (∨(4○⁻)1e¯200j0.4)₀] ⍝ 0.3713906763541037 ¯0.3713906763541037 0.3713906763541037 ⍝ — Reals whose results are complex for some items give what each item gives alone -[⍟¯1 2.5;2√¯4 9.5;1○⁻2 0.5]≡[(⍟¯1),⍟2.5 (2√¯4),2√9.5 (1○⁻2),1○⁻0.5] ⍝ $t +[⍟¯1 2.5;2√¯4 9.5;1○⁻2 0.5] ⍝ [(⍟¯1),⍟2.5 (2√¯4),2√9.5 (1○⁻2),1○⁻0.5] ⍝ — Complex storage holds infinities as reals, and an infinity equals only itself [1j2 2 ∞ ∞=5 2 5 ∞;1j2 ∞=∞ 1j2] ⍝ [[$f $t $f $t];[$f $f]] @@ -8378,13 +8334,6 @@ f←{⍵×+⍵} 2≥1j2 ⍝ error: DOMAIN ERROR -⍝ — Complex equality compares the magnitude of the difference within tolerance, in either order -x←1j1 1j1 1ₓ 1 0 1.7E308j1.7E308 1.7E308j1.7E308 -y←1j1.000000000000012 1j1.00000000000002 1j5E¯15 1j5E¯14 0j1E¯100 1.7E308j1.7E308 ¯1.7E308j¯1.7E308 -[x=y y=x x≠y y≠x] -⍝ => -[[$t $f $t $f $f $t $f] [$t $f $t $f $f $t $f] [$f $t $f $t $t $f $t] [$f $t $f $t $t $f $t]] - ⍝ — Output before a complex error still shows ⎕←1j2 ⋄ 1j2<0 ⍝ error: DOMAIN ERROR @@ -8555,9 +8504,6 @@ add←{⍺+⍵} ⍝ — Steps of 1 stop before passing an end that isn't whole 0.5↦3 ⍝ 0.5 1.5 2.5 -⍝ — The number of steps is whole within tolerance -0↦0.3÷0.1 ⍝ 0 1 2 3 - ⍝ — Each item is the start plus an exact count, so an exact start gives exact items (•storage 1ₓ↦5).kind ⍝ "integer" @@ -8585,27 +8531,9 @@ add←{⍺+⍵} ⍝⍝ Exact and tolerant comparisons -⍝ — Approximate equality uses relative tolerance 1E¯14. https://docs.dyalog.com/20.0/language-reference-guide/system-functions/ct/ -0.3=[0.3 0.1+0.2 0.4] ⍝ $t $t $f - ⍝ — 1 2≤2 1 ⍝ $t $f -⍝ — Tolerant equality is not transitive: x=y and y=z need not imply x=z -[1=1+8E¯15 (1+8E¯15)=1+16E¯15 1=1+16E¯15] ⍝ $t $t $f - -⍝ — Comparisons apply tolerance only when an approximate number takes part -x←[0.3 0.1+0.2 ¯0.3 1 1 1E8 1E8 0 1E¯100 1E308 1r3 1r3 9007199254740993ₓ 1ₓ 1 1r3] -y←[0.1+0.2 0.3 ¯0.1-0.2 1+5E¯15 1+5E¯14 1E8+5E¯7 1E8+5E¯5 1E¯100 0 ¯1E308 2r6 1r2 9007199254740992ₓ 1000000000000001r1000000000000000 1000000000000001r1000000000000000 1÷3] -[x=y⋄x≠y⋄xy⋄x≥y] -⍝ => -[$t $t $t $t $f $t $f $f $f $f $t $f $f $f $t $t - $f $f $f $f $t $f $t $t $t $t $f $t $t $t $f $f - $f $f $f $f $t $f $t $t $f $f $f $t $f $t $f $f - $t $t $t $t $t $t $t $t $f $f $t $t $f $t $t $t - $f $f $f $f $f $f $f $f $t $t $f $f $t $f $f $f - $t $t $t $t $f $t $f $f $t $t $t $f $t $f $t $t] - ⍝ — An empty exact comparison has an exact fill ↑1ₓ=0#1ₓ ⍝ $f @@ -8623,9 +8551,6 @@ j←[¯3 ¯1 0 1 3 1000 ¯1000]ₓ ⍝ — Booleans compare with a whole float as the integers 0 and 1 [$t $f]=1 ⍝ $t $f -⍝ — Tolerance makes the whole float 2E14 equal the integers beside it, as a float comparison does -(200000000000000+⍳3ₓ)=200000000000000 ⍝ $t $t $t - ⍝ — With Mathish mode off, a float reads 9007199254740993 as 9007199254740992, so the comparison still finds them equal •prefs ["mathish":$f] ⋄ (9007199254740992+⍳3ₓ)=9007199254740992 ⍝ $t $t $f @@ -8643,9 +8568,6 @@ j←[¯3 ¯1 0 1 3 1000 ¯1000]ₓ ⍝ => [1 1 1 ; ¯6 ¯1 0] -⍝ — Under Mathish mode, a residue is 0 where the divisor times the floor of the quotient equals the item within tolerance, as 0.1 times 3 and 10 do here -0.1|0.3 1 ⍝ 0 0 - ⍝ — A residue scan of integers from a whole float seed gives floats, the scan from the integer it stands for 7|\[3 10 4 ¯5]ₓ ⍝ 3 1 0 ¯5 @@ -8979,9 +8901,6 @@ widens¨"{{+/⍵×⍵}⍤1 ⍵}⍤2 s" "{k←+/⍵ ⋄ {⍵<5?k;⍵}¨⍵}⍤1 s ⍝ — Principal Lambert W has limit infinity ×↢*⍨⁻∞ ⍝ ∞ -⍝ — Mathish division leaves other indeterminate arithmetic unchanged -[0÷0 ∞-∞ 0×∞ ∞×0ₓ ∞÷∞] ⍝ [0 $n $n $n $n] - ⍝ — Complex numbers can have infinite parts. A number with no imaginary part is real [∞+1j2 1j∞ ∞j0] ⍝ ∞j2 1j∞ ∞ @@ -9389,19 +9308,6 @@ f←↑-ᵘ ⋄ 2 f 3 ⍝ ¯1.0 (2×3) + 4 ⍝ comment ) 10.0 -⍝ — -0÷0 ⍝ 0 - -⍝ — Exact numbers that `÷ₐ` divides as 64-bit floats take Mathish's zero for 0÷0. Integer floor division gives 0 whether Mathish is on or off -[0ₓ÷ₐ0ₓ ; 0ₓ÷0ₓ] -[0 0ₓ] - -⍝ — Mathish division follows the array and fused routes -(0÷⍳8)+0 ⍝ 8⍴0 - -⍝ — -0÷¯0 ⍝ 0 - ⍝ — ¯0 ⍝ 0.0 @@ -9633,10 +9539,6 @@ c←"[¯7 7 ¯1 0]ₓ₂÷ₓ[2 ¯2 3 ¯5]ₓ₂" "[¯7 7 ¯1 0]ₓ₄÷ₓ[2 ¯ x←1.5 ⋄ {(•storage ⍎⍵).kind}¨"(-)ₛ 3" "xₛ" "(x+1)ₕ" "(2ₓ)ₚ" "float32" "float32" "float16" "float64" -⍝ subscript-fixed — After a function, ₓ₁, ₓ₂, ₓ₄ or ₓ₈ converts its results to fixed integers of that width. A float takes its floor and then clamps to the type's range: NaN gives 0, and an infinity gives the nearer end of the range. Under Mathish mode the floor is tolerant: 2.9999999999999996 gives 3, where it gives 2 with Mathish mode off. An integer wraps to the width, and a Boolean gives 0 or 1 -v←0.5 1.5 ¯0.1 2.9999999999999996 ¯3.5 300.5 $n ∞ ¯∞ ⋄ a←[⊢ₓ₁ v ; ⊢ₓ₂ v ; ⊢ₓ₁ [255 256 ¯1 ¯257]ₓ ; ⊢ₓ₂ 0≠[1 0 1]ₓ] ⋄ •prefs ["mathish":$f] ⋄ a,⊂(⊢ₓ₂ v) -[[0 1 0 3 0 255 0 255 0]ₓ₁ ; [0 1 ¯1 3 ¯4 300 0 32767 ¯32768]ₓ₂ ; [255 0 255 255]ₓ₁ ; [1 0 1]ₓ₂ ; [0 1 ¯1 2 ¯4 300 0 32767 ¯32768]ₓ₂] - ⍝ subscript-join-names — Between two items, ⱼ, ₑ and ᵣ join them as one term: xⱼy is x+y×0ⱼ1, xₑy is x×10*y and xᵣy is the exact quotient of xₓ by yₓ, also under static kinds x←3 ⋄ y←2 ⋄ •prefs ["kinds":"static"] ⋄ s←xᵣy ⋄ •prefs ["kinds":"dynamic"] ⋄ [xⱼy xₑy xᵣy s] ⍝ [3ⱼ2 300 3ᵣ2 3ᵣ2] diff --git a/tests/reference/core_mathish.bpl b/tests/reference/core_mathish.bpl new file mode 100644 index 00000000..b0e715df --- /dev/null +++ b/tests/reference/core_mathish.bpl @@ -0,0 +1,151 @@ +⍝⍝ Mathish semantics + +⍝ — A prime index accepts a float within comparison tolerance of an integer +⍭1.000000000000001 ⍝ 3ₓ + +⍝ logical-numeric-truth — a float stands for a Boolean within tolerance only under Mathish +n←1-1e¯15 ⋄ on←[~[0 n];(1+1e¯15)⊼n;0⊽n] +•prefs ["mathish":$f] ⋄ off←{"DOMAIN"::1 ⋄ ~n ⋄ 0}0 ⋄ •prefs ["mathish":$t] ⋄ [on;off] +⍝ => +[[[$t $f] $f $f] 1] + +⍝ — Mathish division gives zero for zero divided by zero in arrays +0 1÷0 2 ⍝ 0 0.5 + +⍝ — A character offset accepts a float within comparison tolerance of an integer +'a'+1.000000000000001 ⍝ 'b' + +⍝ — Complex gcd uses Gaussian-integer arithmetic and tolerant comparison +0.2=3.8j7.6∨5.2j6.8 ⍝ $t + +⍝ — Scale and ceil to expose the complex lcm despite floating-point roundoff +⌈1000×3.8j7.6∧5.2j6.8 ⍝ ¯159600j326800 + +⍝ — Floor applies comparison tolerance near integers, including across a storage-width boundary +[⌊1.000000000000001 ¯1.000000000000001;⌊255.9999999999999 0] ⍝ [[1 ¯1]ₓ [256 0]ₓ] + +⍝ — An integer argument accepts a float within comparison tolerance of an integer +(0.1×3 × 10)⌷10 20 30 40 ⍝ 40 + +⍝ — A count accepts a float within comparison tolerance of an integer +⍳(0.1×3)×10 ⍝ 0 1 2 + +⍝ — The gcd of two reals recovers their rational ratio, in either order [rtol=1e-14 atol=0] +x←[1 1ₓ ¯1 0 0 1 1E¯200 1E200 1E¯200] +y←[103993÷33102 π1 ¯103993÷33102 ¯0.75 0 1.000000000000005 3E¯200 3E200 1E200] +[x∨y y∨x] +⍝ => +2⍴⊂[÷33102 ÷1725033 ÷33102 0.75 0 1 1E¯200 1E200 1E¯200] + +⍝ — Reals outside comparison tolerance of a rational ratio have a tiny gcd +1E¯12>1∨1.00000000000005 ⍝ $t + +⍝ — Index-of compares each candidate directly; tolerance is not transitive +x←1 +y←1+8E¯15 +z←1+16E¯15 +[x y]⍳z ⍝ 1ₓ + +⍝ — Unique-mask puts a chain of tolerant matches in one class +≠[1 1+8E¯15 1+16E¯15] ⍝ $t $f $f + +⍝ — A row is a copy of the first earlier distinct row that it matches, so rows don't chain +≠[1 2⋄1+8E¯15 2⋄1+16E¯15 2] ⍝ $t $f $t + +⍝ — A large index-of takes the first position within tolerance, not the nearest value +(300⍴5 , [1+16E¯15 1])⍳1+8E¯15 ⍝ 300ₓ + +⍝ — A large index-of finds nothing outside tolerance +(300⍴5 , 1)⍳1+2E¯14 ⍝ 301ₓ + +⍝ — Large membership compares exact integers with floats within tolerance +[1+8E¯15 1+2E¯14]∊⍳300ₓ ⍝ $t $f + +⍝ — Without uses tolerance when approximate values participate +1 2~[1+8E¯15] ⍝ [2] + +⍝ — Membership uses tolerance when approximate values participate +1 2∊[1+8E¯15] ⍝ $t $f + +⍝ native-search-shapes — Compact and mixed patterns share shape traversal; fractional needles keep tolerant matching +p←[1 [2 3]] ⋄ v←[1 [2 3] 0 1 [2 3]] +[p⍷v [1 2]⍷[1 2 1⋄1 2 3] [1 2 3]ₓ⍳[2 2.000000000000001]] +⍝ => +[$t $f $f $t $f;[$t $f $f⋄$t $f $f];[1 1]ₓ] + +⍝ native-find-widths — Compact float patterns use the wider operand's tolerance and match NaNs +[(32•float[1])⍷[1+8E¯15] (32•float[1])⍷32•float[1+8E¯7] [$n]⍷[$n 0]] +⍝ => +[[$t] [$f] [$t $f]] + +⍝ exact-key-search — A search gives the same positions whether every cell is an exact key or one float cell sends them all through `Value::matches`, and for strings held as items or as a matrix's rows +h←["ab";"cd";"";7ₓ;'x';[3 4]ₓ;"abc";1ₓ] ⋄ ns←32⍴["cd";"zz";"";7ₓ;'x';[3 4]ₓ;"ab";'y';"abc";$t] +s←["ab";"cd";"ef";"gh";"ij";"kl";"mn";"op"] ⋄ t←32⍴s,⊂"zz" +[(ns∊h)≡ns∊h,⊂0.5 ; (≠h,ns)≡¯1↓≠h,ns,⊂0.5 ; (h⍳ns)≡8⌊(h,⊂0.5)⍳ns ; (s⍳t)≡(⊃s)⍳⊃t ; 10↑h⍳ns ; h⍳1 1+1E¯15] +⍝ => +[$t;$t;$t;$t;[1 8 2 3 4 5 0 8 6 7]ₓ;[7 7]ₓ] + +⍝ — Interval index in each cell's own boundaries, and index of within the tolerance +[({⍵⍸2.5}⍤1)[2 3]⍴1 2 3 0 5 10 ; ({⍵⍳0.3}⍤1)[2 3]⍴0.1 0.2 0.30000000000000004 0.3 0.1 0.2] ⍝ [[2 1]ₓ ; [2 0]ₓ] + +⍝ — `≡` compares a batch's exact-integer or Boolean cells with a float constant on the batch's whole, within the tolerance [batched] +m←3 2⍴⍳6ₓ ⋄ b←2 2⍴$t $f $f $t ⋄ c←2 3.0000000000000004 +[{⍵≡c}⍤1 m ; {⍵≡1 0.0}⍤1 b ; (•explain "{⍵≡c}⍤1 m").fallbacks.reason] ⍝ [[$f $t $f] [$t $f] 0⍴⊂""] + +⍝ — Tolerant Key puts a chain of matches in one group +{≢⍵}⌸[1 1+8E¯15 1+16E¯15] ⍝ [3]ₓ + +⍝ — Complex equality compares the magnitude of the difference within tolerance, in either order +x←1j1 1j1 1ₓ 1 0 1.7E308j1.7E308 1.7E308j1.7E308 +y←1j1.000000000000012 1j1.00000000000002 1j5E¯15 1j5E¯14 0j1E¯100 1.7E308j1.7E308 ¯1.7E308j¯1.7E308 +[x=y y=x x≠y y≠x] +⍝ => +[[$t $f $t $f $f $t $f] [$t $f $t $f $f $t $f] [$f $t $f $t $t $f $t] [$f $t $f $t $t $f $t]] + +⍝ — Approximate equality uses relative tolerance 1E¯14. https://docs.dyalog.com/20.0/language-reference-guide/system-functions/ct/ +0.3=[0.3 0.1+0.2 0.4] ⍝ $t $t $f + +⍝ — Tolerant equality is not transitive: x=y and y=z need not imply x=z +[1=1+8E¯15 (1+8E¯15)=1+16E¯15 1=1+16E¯15] ⍝ $t $t $f + +⍝ — Comparisons apply tolerance only when an approximate number takes part +x←[0.3 0.1+0.2 ¯0.3 1 1 1E8 1E8 0 1E¯100 1E308 1r3 1r3 9007199254740993ₓ 1ₓ 1 1r3] +y←[0.1+0.2 0.3 ¯0.1-0.2 1+5E¯15 1+5E¯14 1E8+5E¯7 1E8+5E¯5 1E¯100 0 ¯1E308 2r6 1r2 9007199254740992ₓ 1000000000000001r1000000000000000 1000000000000001r1000000000000000 1÷3] +[x=y⋄x≠y⋄xy⋄x≥y] +⍝ => +[$t $t $t $t $f $t $f $f $f $f $t $f $f $f $t $t + $f $f $f $f $t $f $t $t $t $t $f $t $t $t $f $f + $f $f $f $f $t $f $t $t $f $f $f $t $f $t $f $f + $t $t $t $t $t $t $t $t $f $f $t $t $f $t $t $t + $f $f $f $f $f $f $f $f $t $t $f $f $t $f $f $f + $t $t $t $t $f $t $f $f $t $t $t $f $t $f $t $t] + +⍝ — Tolerance makes the whole float 2E14 equal the integers beside it, as a float comparison does +(200000000000000+⍳3ₓ)=200000000000000 ⍝ $t $t $t + +⍝ — Under Mathish mode, a residue is 0 where the divisor times the floor of the quotient equals the item within tolerance, as 0.1 times 3 and 10 do here +0.1|0.3 1 ⍝ 0 0 + +⍝ — Mathish division leaves other indeterminate arithmetic unchanged +[0÷0 ∞-∞ 0×∞ ∞×0ₓ ∞÷∞] ⍝ [0 $n $n $n $n] + +⍝ — +0÷0 ⍝ 0 + +⍝ — Exact numbers that `÷ₐ` divides as 64-bit floats take Mathish's zero for 0÷0. Integer floor division gives 0 whether Mathish is on or off +[0ₓ÷ₐ0ₓ ; 0ₓ÷0ₓ] +[0 0ₓ] + +⍝ — Mathish division follows the array and fused routes +(0÷⍳8)+0 ⍝ 8⍴0 + +⍝ — +0÷¯0 ⍝ 0 + +⍝ subscript-fixed — After a function, ₓ₁, ₓ₂, ₓ₄ or ₓ₈ converts its results to fixed integers of that width. A float takes its floor and then clamps to the type's range: NaN gives 0, and an infinity gives the nearer end of the range. Under Mathish mode the floor is tolerant: 2.9999999999999996 gives 3, where it gives 2 with Mathish mode off. An integer wraps to the width, and a Boolean gives 0 or 1 +v←0.5 1.5 ¯0.1 2.9999999999999996 ¯3.5 300.5 $n ∞ ¯∞ ⋄ a←[⊢ₓ₁ v ; ⊢ₓ₂ v ; ⊢ₓ₁ [255 256 ¯1 ¯257]ₓ ; ⊢ₓ₂ 0≠[1 0 1]ₓ] ⋄ •prefs ["mathish":$f] ⋄ a,⊂(⊢ₓ₂ v) +[[0 1 0 3 0 255 0 255 0]ₓ₁ ; [0 1 ¯1 3 ¯4 300 0 32767 ¯32768]ₓ₂ ; [255 0 255 255]ₓ₁ ; [1 0 1]ₓ₂ ; [0 1 ¯1 2 ¯4 300 0 32767 ¯32768]ₓ₂] + +⍝ — The number of steps is whole within tolerance +0↦0.3÷0.1 ⍝ 0 1 2 3 + diff --git a/tests/reference/dyalog.bpl b/tests/reference/dyalog.bpl index e57c1245..05b454af 100644 --- a/tests/reference/dyalog.bpl +++ b/tests/reference/dyalog.bpl @@ -88,9 +88,6 @@ a ⍝ 2 2 1 2 10⍟0j1 1j2 0j2.266180070913597 0.3494850021680094j0.480828578784234 -⍝ dyalog:logarithm:3 — -1⍟1 ⍝ 0 - ⍝ dyalog:pi-times:1 — π0.5 1 2 ⍝ 1.570796326794897 3.141592653589793 6.283185307179586 diff --git a/tests/reference/dyalog_mathish.bpl b/tests/reference/dyalog_mathish.bpl new file mode 100644 index 00000000..863a13ac --- /dev/null +++ b/tests/reference/dyalog_mathish.bpl @@ -0,0 +1,5 @@ +⍝⍝ Mathish semantics + +⍝ dyalog:logarithm:3 — +1⍟1 ⍝ 0 + diff --git a/tests/reference/graphics.bpl b/tests/reference/graphics.bpl index eb95f708..3cb7582f 100644 --- a/tests/reference/graphics.bpl +++ b/tests/reference/graphics.bpl @@ -72,11 +72,15 @@ apng←{∨/"acTL"⍷•ucs "image/png"⊃•mime ⍵} f←0.2×2 2 3 3⍴5ₓ|⍳36ₓ g←0.2×3 2 2 1⍴5ₓ|⍳12ₓ s←⍴•image •image⁻ 1 2 3 3⍴0.2 -(f≡•image •image⁻ f) (g≡•image •image⁻ g) (s≡[2 3 3]) ⍝ $t $t $t +[•image •image⁻ f;•image •image⁻ g;s] +⍝ => +[0.2×2 2 3 3⍴5ₓ|⍳36ₓ;0.2×3 2 2 1⍴5ₓ|⍳12ₓ;[2 3 3]ₓ] ⍝ image-inverse-fps — With fps, •image⁻ encodes grey frames that have no channel axis h←0.2×3 2 2⍴5ₓ|⍳12ₓ -(3 2 2 1⍴h)≡•image ["fps":12]•image⁻ h ⍝ $t +•image ["fps":12]•image⁻ h +⍝ => +0.2×3 2 2 1⍴5ₓ|⍳12ₓ ⍝ image-delay — delay sets each frame's seconds, stored exactly as 1/n for a whole rate n, otherwise in milliseconds or seconds delays←{t←⍸"fcTL"⍷•ucs ⍵ ⋄ {256⊥2 2⍴⍵}⍤1 (⊂t+⊗24ₓ+⍳4ₓ)⌷⍵} @@ -86,7 +90,9 @@ delays ["delay":[1÷24 0.04 1.6 100]]•image⁻ 4 2 2⍴0.2 ⍝ image-delay-time — delay makes the first axis time, and one delay applies to every frame h←0.2×3 2 2⍴5ₓ|⍳12ₓ -(3 2 2 1⍴h)≡•image ["delay":0.1]•image⁻ h ⍝ $t +•image ["delay":0.1]•image⁻ h +⍝ => +0.2×3 2 2 1⍴5ₓ|⍳12ₓ ⍝ image-delay-length — delay needs one number, or one for each frame ["delay":[0.1 0.2 0.3]]•image⁻ 2 2 2⍴0.2 diff --git a/tests/reference/lib.bpl b/tests/reference/lib.bpl index 596bb7bd..876b8bba 100644 --- a/tests/reference/lib.bpl +++ b/tests/reference/lib.bpl @@ -127,12 +127,6 @@ t←0↣(put foldl) [["a" 1] ["b" 2] ["c" 3]] ⍝⍝ Dyalog library -⍝ — ary writes zero, terminating fractions and exact powers of the base, and repeats a recurring unit twice -[ary:]←•load "lib/dyalog.bpl" -[2 ary 0;2 ary 0.75;2 ary ¯0.75;2 ary 8;16 ary 255;10 ary ÷7] -⍝ => -"0" "0.11" "¯0.11" "1000" "FF" "0.142857142857..." - ⍝ — base_64 encodes and decodes the RFC 4648 test vectors [base_64:]←•load "lib/dyalog.bpl" v←"f" "fo" "foo" "foob" "fooba" "foobar" @@ -345,12 +339,6 @@ m←⌽(32⍴2)⊤32↑0 16404 49152 57344 32789 24598 8215 57344 57344 16406 49 ⍝ => 0 16404 49152 57344 32789 24598 8215 57344 57344 16406 49152 40981 24600 25 57344 57344 0 0 0 0 3 1 4294967292 2 3 14 0 0 0 0 0 0 -⍝ — ary rounds a fraction whose digits run past its limit, and marks the result with ? -[ary:]←•load "lib/dyalog.bpl" -[10 ary π 1;10 ary 2*÷2;16 ary π 1] -⍝ => -"3.1415926535898?" "1.4142135623731?" "3.243F6A8885A?" - ⍝ — Cut 3 tiles a matrix by the given movements and sizes, keeping partial tiles; ¯3 keeps only whole tiles, and a negative size reverses each tile [Cut:]←•load "lib/dyalog.bpl" x←5 7⍴↦35 diff --git a/tests/reference/lib_mathish.bpl b/tests/reference/lib_mathish.bpl new file mode 100644 index 00000000..1faa79f7 --- /dev/null +++ b/tests/reference/lib_mathish.bpl @@ -0,0 +1,14 @@ +⍝⍝ Mathish semantics + +⍝ — ary writes zero, terminating fractions and exact powers of the base, and repeats a recurring unit twice +[ary:]←•load "lib/dyalog.bpl" +[2 ary 0;2 ary 0.75;2 ary ¯0.75;2 ary 8;16 ary 255;10 ary ÷7] +⍝ => +"0" "0.11" "¯0.11" "1000" "FF" "0.142857142857..." + +⍝ — ary rounds a fraction whose digits run past its limit, and marks the result with ? +[ary:]←•load "lib/dyalog.bpl" +[10 ary π 1;10 ary 2*÷2;16 ary π 1] +⍝ => +"3.1415926535898?" "1.4142135623731?" "3.243F6A8885A?" + diff --git a/tests/reference/ngn.bpl b/tests/reference/ngn.bpl index 682aa9b5..441228cc 100644 --- a/tests/reference/ngn.bpl +++ b/tests/reference/ngn.bpl @@ -317,9 +317,6 @@ a←" this is a test "⋄a≠' ' ⊂ a ⍝ ngn:105 — ÷2j3 ⍝ 0.1538461538461539j¯0.2307692307692308 -⍝ ngn:106 — -0÷0 ⍝ 0 - ⍝ ngn:107 — 27÷9 ⍝ 3 diff --git a/tests/reference/ngn_mathish.bpl b/tests/reference/ngn_mathish.bpl new file mode 100644 index 00000000..27e709d5 --- /dev/null +++ b/tests/reference/ngn_mathish.bpl @@ -0,0 +1,5 @@ +⍝⍝ Mathish semantics + +⍝ ngn:106 — +0÷0 ⍝ 0 + diff --git a/tests/reference/static.bpl b/tests/reference/static.bpl index df1a4a9e..0ffc9fdf 100644 --- a/tests/reference/static.bpl +++ b/tests/reference/static.bpl @@ -172,7 +172,7 @@ g←{⍵+1} ⋄ f←{+/⍵×(g ⍵×m)} ⋄ m←2 ⋄ a←f∂1.5 2.5 ⋄ m←3 x←0.5×[3 2]⍴⍳6 ⋄ c←{+/+/⍵.[r]} ⋄ r←[2 2 2]ₓ ⋄ g←c∂x ⋄ r←[1 1 1]ₓ ⋄ h←c∂x ⋄ r←[0 1 2]ₓ ⋄ e←•explain "c∂x" ⋄ [g ; h ; e.value ; e.programs.event] [[0 0⋄0 0⋄3 3] [0 0⋄3 3⋄0 0] [1 1⋄1 1⋄1 1] ["replayed"]] -⍝ — A free name that a dfn reads as a count holds its value in the program: every call after the first, while the name holds the same value, replays, and a new value makes a new trace. After nine traces in a row that never replayed, the definition traces no more: a call that no program fits runs without one, with the stopped-trace reason "a free value that changes on every call". Its kept programs go on serving, so a second run of this code replays the first nine counts and needs nine new ones after them, which the eighteen counts give +⍝ — A free count observation guards a recording by value. Repeated changes without reuse can stop further recording for that argument signature; ordinary differentiation still gives the right value, and •explain reports why it fell back. x←0.5×⍳20 ⋄ n←0 ⋄ f←{+/n↑⍵} ⋄ t←{n⊢←⍵ ⋄ f∂x} ⋄ j←t¨1↦18 ⋄ n←19 ⋄ k←•explain "f∂x" ⋄ [k.value ; k.stopped.reason] [1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 ; ["a free value that changes on every call"]] @@ -318,3 +318,7 @@ e←0⍴0.5 ⍝ structural-operator-captures — Axis names and rank vectors held in names remain call-time inputs, even after a body has been recorded d←["r":2 "c":3]⍴⍳6 ⋄ axis←"r" ⋄ f←{+/⍠axis ⍵} ⋄ a←f d ⋄ axis←"c" ⋄ b←f d ⋄ m←2 3⍴⍳6 ⋄ ranks←0 1 ⋄ g←{≢⍤ranks ⍵} ⋄ c←g m ⋄ ranks←1 0 ⋄ e←g m ⋄ [a≡+⌿d ; b≡+/d ; c≡(≢⍤[0 1])m ; e≡(≢⍤[1 0])m] 4⍴$t + +⍝ capture-reuse-keeps-recording — Reusing an older program between changes to an observed count keeps recording useful; the final count's derivative can still replay +x←0.5×⍳5 ⋄ n←1 ⋄ f←{+/n↑⍵} ⋄ t←{n⊢←⍵ ⋄ f∂x} ⋄ j←t¨1 2 1 3 1 4 ⋄ e←•explain "f∂x" ⋄ [e.value ; e.programs.event ; ≢e.stopped.reason] +[1 1 1 1 0 ; ["replayed"] ; 0ₓ] diff --git a/tests/settings.rs b/tests/settings.rs index b0a65775..32deec2c 100644 --- a/tests/settings.rs +++ b/tests/settings.rs @@ -6,9 +6,10 @@ use common::*; #[test] fn checks_that_change_settings() { - let checks: [fn(); 8] = [ + let checks: [fn(); 11] = [ mathish_arithmetic, boxed_display_and_function_trees, expression_dissection, threads_give_the_same_results, hanzi_spellings_and_language, leading_prefs_set_how_their_code_reads, highlight_after_a_prefs_header, retained_selection_parameters_survive_data_reads, + retained_programs_rebind_block_geometry, recorded_agreements_keep_source_maps, operator_bodies_reuse_consecutive_signatures, ]; for check in checks { basedpl::settings::reset(); @@ -224,3 +225,49 @@ fn retained_selection_parameters_survive_data_reads() { check(r#""q"⌷["b":4 "a":5]"#, "0"); } } + +fn recorded_agreements_keep_source_maps() { + let s = Session::new(); + assert!(s.eval(r#"•prefs ["mathish":$f "kinds":"static" "float":32]"#).error.is_none()); + equiv_in(&s, "f←{(-⊗)⍨⍵} ⋄ (f⍣2)2.0 4.0", "2 2 2 2⍴0 2 ¯2 0 ¯2 0 ¯4 ¯2 2 4 0 2 0 2 ¯2 0"); + for (body, expected) in [("{1.0 3.0+⊗⍵}", "2 2 2⍴4 6 6 8 6 8 8 10"), ("{1.0 3.0⊣⊗⍵}", "2 2 2⍴1 1 1 1 3 3 3 3")] { + equiv_in(&s, &format!("f←{body} ⋄ (f⍣2)2.0 4.0"), expected); + } + equiv_in(&s, "g←{+/,1.0 3.0+⊗⍵} ⋄ g∂2.0 4.0", "2.0 2.0"); +} + +fn operator_bodies_reuse_consecutive_signatures() { + let s = Session::new(); + assert!(s.eval(r#"•prefs ["kinds":"static" "mathish":$f "float":32] ⋄ f←{⍵+1.0}"#).error.is_none()); + basedpl::settings::set_sizes(basedpl::settings::Sizes { min_items: 1, ..basedpl::settings::Sizes::DEFAULT }); + equiv_in(&s, r#"e←•explain "f⍣2⁝1.0 2.0" ⋄ [e.value;≢e.programs.event]"#, "[3 4;0ₓ]"); + equiv_in(&s, r#"e←•explain "f⍣4⁝1.0 2.0" ⋄ [e.value;e.programs.event]"#, r#"[5 6;["built";"replayed"]]"#); + equiv_in(&s, r#"e←•explain "f⍣2⁝1.0 2.0" ⋄ [e.value;e.programs.event]"#, r#"[3 4;["replayed"]]"#); + equiv_in(&s, r#"g←{0.0 1.0+⊗⍵} ⋄ e←•explain "g⍣6⁝,0.0" ⋄ [e.value≡g g g g g g ,0.0;≢e.programs.event]"#, "[$t;0ₓ]"); + equiv_in(&s, "blend←{(0.5×⍺)+(0.25×⍵)} ⋄ cells←[2.0 4.0;6.0 8.0;10.0 12.0] ⋄ seed←1.0 3.0 ⋄ seed(blend/)cells", "2.078125 3.421875"); + equiv_in(&s, "seed(blend\\)cells", "[1 2.5;2 3.25;3.5 4.625]"); +} + +fn retained_programs_rebind_block_geometry() { + let s = Session::new(); + assert!(s.eval(r#"•prefs ["kinds":"static" "mathish":$f] ⋄ v←1 2 3 ⋄ x←0.25×⍳17 ⋄ f←{+/,⊼⍵+⊗v}"#).error.is_none()); + for block in [7, 64, 5] { + basedpl::settings::set_sizes(basedpl::settings::Sizes { block, min_items: 1, ..basedpl::settings::Sizes::DEFAULT }); + // Block geometry is construction context; the next call at that geometry reuses the recording. + equiv_in(&s, r#"e←•explain "f∂x" ⋄ e.value"#, "12+6×0.25×⍳17"); + equiv_in(&s, "e.programs.event", r#"["built"]"#); + equiv_in(&s, r#"e←•explain "f∂x" ⋄ e.value"#, "12+6×0.25×⍳17"); + equiv_in(&s, "e.programs.event", r#"["replayed"]"#); + } + // Power warms ordinary calls first at length 2; derivatives warm first at length 4. + equiv_in(&s, "a←2 2 ⋄ f←{a×⍵+1} ⋄ f⍣2⁝1 2", "10 14"); + equiv_in(&s, "f∂1 2", "[2 0⋄0 2]"); + equiv_in(&s, "f⍣2⁝1 2", "10 14"); + equiv_in(&s, "a←3 3 ⋄ f⍣2⁝1 2", "21 30"); + equiv_in(&s, "f∂1 2", "[3 0⋄0 3]"); + equiv_in(&s, "a←3 3 3 3 ⋄ f∂1 2 3 4", "[3 0 0 0⋄0 3 0 0⋄0 0 3 0⋄0 0 0 3]"); + equiv_in(&s, "f⍣2⁝1 2 3 4", "21 30 39 48"); + equiv_in(&s, "a←4 4 4 4 ⋄ [f∂1 2 3 4 ; f⍣2⁝1 2 3 4]", "[[4 0 0 0⋄0 4 0 0⋄0 0 4 0⋄0 0 0 4];36 52 68 84]"); + equiv_in(&s, "h←{⊂2×⍵} ⋄ h⍣2⁝1 2", "⊂⊂4 8"); + equiv_in(&s, "h⍣2⁝3 4", "⊂⊂12 16"); +} diff --git a/tests/speed.rs b/tests/speed.rs index 30668953..beb1cee7 100644 --- a/tests/speed.rs +++ b/tests/speed.rs @@ -65,12 +65,21 @@ macro_rules! speed_tests { } speed_tests!( + timed_bodies counts_bodies "bodies.bpl", timed_arrays counts_arrays "arrays.bpl", timed_calls counts_calls "calls.bpl", timed_growth counts_growth "growth.bpl", timed_images counts_images "images.bpl", timed_maps counts_maps "maps.bpl", - timed_models counts_models "models.bpl", timed_steps counts_steps "steps.bpl", timed_structure counts_structure "structure.bpl", timed_tables counts_tables "tables.bpl", + timed_models counts_models "models.bpl", timed_moving counts_moving "moving.bpl", timed_steps counts_steps "steps.bpl", timed_structure counts_structure "structure.bpl", timed_tables counts_tables "tables.bpl", ); +const MATHISH_COMPARISONS: &str = "comparisons_mathish.bpl"; +#[test] +#[cfg_attr(debug_assertions, ignore = "debug assertions change the counts")] +fn counts_comparisons_mathish() { counts(MATHISH_COMPARISONS) } + #[test] fn every_speed_file_has_tests() { let names: Vec<_> = reference::load(&Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/speed")).unwrap().into_iter().map(|f| f.name).collect(); - assert_eq!(names, FILES); + let mut registered = FILES.to_vec(); + registered.push(MATHISH_COMPARISONS); + registered.sort_unstable(); + assert_eq!(names, registered); } diff --git a/tests/speed/arrays.bpl b/tests/speed/arrays.bpl index b31b931e..d8828559 100644 --- a/tests/speed/arrays.bpl +++ b/tests/speed/arrays.bpl @@ -46,276 +46,276 @@ rb←rkb:w ⍝ Sharing storage copies nothing. ⍝ Expected: 0 copies each: shares storage. ,m -⍝⍝: 0.5 0.5 0.5 instructions linux 0.397 allocated linux 0.032 +⍝⍝: 0.5 0.5 0.5 instructions macos 0.938 linux 0.397 allocated macos 0.032 linux 0.032 ⍴m ⍴ v -⍝⍝: 0.5 0.5 0.5 instructions linux 0.968 allocated linux 0.074 +⍝⍝: 0.5 0.5 0.5 instructions macos 1.654 linux 0.968 allocated macos 0.074 linux 0.074 ⍝ Reading each item once, with almost nothing to write. ⍝ Expected: `+/v`, `0+/v` (the kernel's sum, then the seed), `-/v`, `⌈/v`, `+/m` and `+⌿m` 0.5 copies: reads 8 bytes an item and writes nothing; `+/jv` and `-/jv` 0.13 copies: reads 2 bytes an item; `+/z` 1 copy: reads 16 bytes an item; `∧/b` 0.06 copies: reads 1 byte an item. ⍝ Wasm on CI's x86 runner: under Node 26's V8, `+/v` costs what it costs natively, 0.48 to 0.51 copies in the runs of 28 and 29 October 2026, against 1.0 to 1.3 under Node 22. `-/v` folds its items in pairs and costs about 0.7 there. On this Mac's V8 the order reverses: `+/v` costs 0.6 and `-/v` 0.8. On this Mac's V8, the only one that can be timed here, a pair fold raised `+/v` from 0.60 to 0.82 copies. `0+/v` runs the same kernel as `+/v` and has the same budgets. +/v -⍝⍝: 1 0.68 0.76 instructions linux 2.183 allocated linux 0.171 +⍝⍝: 1 0.68 0.76 instructions macos 3.762 linux 2.183 allocated macos 0.171 linux 0.171 0+/v -⍝⍝: 1 0.72 0.84 instructions linux 2.752 allocated linux 0.189 +⍝⍝: 1 0.72 0.84 instructions macos 4.212 linux 2.752 allocated macos 0.189 linux 0.189 +/jv -⍝⍝: 0.5 0.5 0.53 instructions linux 2.567 allocated linux 0.175 +⍝⍝: 0.5 0.5 0.53 instructions macos 3.631 linux 2.567 allocated macos 0.175 linux 0.175 +/z -⍝⍝: 2 1.8 2.4 instructions linux 2.401 allocated linux 0.172 +⍝⍝: 2 1.8 2.4 instructions macos 4.566 linux 2.401 allocated macos 0.172 linux 0.172 -/v -⍝⍝: 1 1.7 0.95 instructions linux 1.749 allocated linux 0.123 +⍝⍝: 1 1.7 0.95 instructions macos 3.144 linux 1.749 allocated macos 0.123 linux 0.123 -/jv -⍝⍝: 0.5 0.5 0.53 instructions linux 2.086 allocated linux 0.128 +⍝⍝: 0.5 0.5 0.53 instructions macos 3.024 linux 2.086 allocated macos 0.128 linux 0.128 ⌈/v -⍝⍝: 1 0.69 0.7 instructions linux 2.587 allocated linux 0.171 +⍝⍝: 1 0.69 0.7 instructions macos 3.797 linux 2.587 allocated macos 0.171 linux 0.171 ∧/b -⍝⍝: 0.5 0.5 0.5 instructions linux 2.124 allocated linux 0.171 +⍝⍝: 0.5 0.5 0.5 instructions macos 3.047 linux 2.124 allocated macos 0.171 linux 0.171 +/m -⍝⍝: 1 1.1 0.98 instructions linux 3.064 allocated linux 0.304 +⍝⍝: 1 1.1 0.98 instructions macos 4.929 linux 3.064 allocated macos 0.304 linux 0.304 +⌿m -⍝⍝: 1 0.93 0.95 instructions linux 2.984 allocated linux 0.304 +⍝⍝: 1 0.93 0.95 instructions macos 4.584 linux 2.984 allocated macos 0.304 linux 0.304 ⍝ A fold over several axes of a simple array needs no call per cell. `+ × ⌈ ⌊` fold each axis in turn. Other primitives fold each cell's ravel once. ⍝ Expected: 0.5 copies each: reads 8 bytes an item and writes nothing. +/₁ ₂ms -⍝⍝: 1 1.1 1.3 instructions linux 3.93 allocated linux 0.401 +⍝⍝: 1 1.1 1.3 instructions macos 6.138 linux 3.93 allocated macos 0.401 linux 0.401 -/₁ ₂ms -⍝⍝: 1 2.5 1.8 instructions linux 4.347 allocated linux 0.474 +⍝⍝: 1 2.5 1.8 instructions macos 5.786 linux 4.347 allocated macos 0.474 linux 0.474 ⍝ A cheap ordered right fold keeps each consecutive lane's running value in a local. ⍝ Expected: 6 copies: about 3 cycles an item, because the running value is usually below the next item and most steps skip the division. |/jv -⍝⍝: 12 19 40 instructions linux 17.508 allocated linux 0.574 +⍝⍝: 12 19 40 instructions macos 16.1 linux 17.508 allocated macos 0.574 linux 0.574 ⍝ Reading one argument and writing each item once. ⍝ Expected: `-v` and `⌽v` 1 copy: one read and one write per item; `-jv` 0.25 copies: 2-byte reads and writes; `~b`, `∧\b` and `∨\b` 0.13 copies: 1-byte reads and writes; `1⌽v` 1 copy: two contiguous copies; `⊖m` 1 copy: contiguous row copies; `1↓v` 1 copy: one copy; `(n÷2)↑v` 0.5 copies: copies half the items; `n⍴0` 0.5 copies: writes 8 bytes an item. -v -⍝⍝: 2 1.9 2.1 instructions linux 1.746 allocated linux 8.06 +⍝⍝: 2 1.9 2.1 instructions macos 2.995 linux 1.746 allocated macos 8.06 linux 8.06 -jv -⍝⍝: 0.5 0.5 0.7 instructions linux 1.747 allocated linux 2.063 +⍝⍝: 0.5 0.5 0.7 instructions macos 2.609 linux 1.747 allocated macos 2.063 linux 2.063 ~b -⍝⍝: 0.5 0.5 0.5 instructions linux 1.236 allocated linux 1.062 +⍝⍝: 0.5 0.5 0.5 instructions macos 2.102 linux 1.236 allocated macos 1.062 linux 1.062 ∧\b -⍝⍝: 0.5 0.5 0.5 instructions linux 1.311 allocated linux 1.09 +⍝⍝: 0.5 0.5 0.5 instructions macos 2.091 linux 1.311 allocated macos 1.09 linux 1.09 ∨\b -⍝⍝: 0.5 0.5 0.5 instructions linux 1.42 allocated linux 1.09 +⍝⍝: 0.5 0.5 0.5 instructions macos 2.088 linux 1.42 allocated macos 1.09 linux 1.09 ⌽v -⍝⍝: 2 1.9 2.7 instructions linux 1.475 allocated linux 8.057 +⍝⍝: 2 1.9 2.7 instructions macos 2.814 linux 1.475 allocated macos 8.057 linux 8.057 ⍝ In wasm on CI's x86 runner, `1⌽v` and `1↓v` cost about 0.8 copies in some runs and 1.6 to 1.9 in others. The cause is upstream. V8 runs wasm's `memory.copy` with the host's `memmove`. On CI's Ubuntu 24.04, glibc 2.39's `memmove` copies runs of 4 KB to 1 MB with `rep movsb`. Zen 3 and Zen 4 run `rep movsb` many times slower when the destination sits a little past the source modulo 4 KB (glibc bug 30994, fixed in glibc 2.40). The browser build's allocator puts each equal-sized block 16 bytes further along modulo 4 KB than the one before. A copy into the next block then lands 8 to 16 bytes past its source, in the slow case. The build keeps `memory.copy`: a loop of its own made these copies 1.6 times slower on this Mac's Node. 1⌽v -⍝⍝: 2 1.8 1.4 instructions linux 0.852 allocated linux 8.058 +⍝⍝: 2 1.8 1.4 instructions macos 3.052 linux 0.852 allocated macos 8.058 linux 8.058 ⊖m -⍝⍝: 2 1.9 1.9 instructions linux 1.995 allocated linux 8.067 +⍝⍝: 2 1.9 1.9 instructions macos 3.306 linux 1.995 allocated macos 8.067 linux 8.067 ⍝ In wasm on CI, `1↓v` varies as `1⌽v` does, for the reason given above `1⌽v`. 1↓v -⍝⍝: 2 1.8 1.3 instructions linux 0.995 allocated linux 8.063 +⍝⍝: 2 1.8 1.3 instructions macos 3.286 linux 0.995 allocated macos 8.063 linux 8.063 n÷2 ↑ v -⍝⍝: 1 0.66 0.79 instructions linux 1.449 allocated linux 4.082 +⍝⍝: 1 0.66 0.79 instructions macos 3.037 linux 1.449 allocated macos 4.082 linux 4.082 ⍝ Expected: 0.5 copies: writes 8 bytes an item. n⍴0 -⍝⍝: 1 1.2 1.3 instructions linux 1.22 allocated linux 8.057 +⍝⍝: 1 1.2 1.3 instructions macos 1.989 linux 1.22 allocated macos 8.057 linux 8.057 ⍝ Reading two arguments and writing each item once. ⍝ Expected: `v+w`, `kv+kv` and `v⌊w` 1.5 copies: two 8-byte reads and one write per item; `jv+jv` and `jv∧ᵇjv` 0.38 copies: 2-byte reads and writes; `v×2` 1 copy: one read and one write per item; `jv×2.5` 0.63 copies: its products are floats, with 2-byte reads and 8-byte writes; `jv=jv` 0.31 copies: two 2-byte reads and a 1-byte write; `b×jv` 0.31 copies: 1- and 2-byte reads, 2-byte writes; `b×kv` 1.1 copies: 1- and 8-byte reads, 8-byte writes; `b∧b` 0.19 copies: 1-byte reads and writes; `c=c` 0.56 copies: two 4-byte reads and a 1-byte write; `⍳√n ×⊗ ⍳√n` 0.5 copies: writes 8 bytes an item. ⍝⍝⍝ route vector kernel v+w -⍝⍝: 3 4.5 3.7 instructions linux 2.137 allocated linux 8.079 +⍝⍝: 3 4.5 3.7 instructions macos 3.482 linux 2.137 allocated macos 8.079 linux 8.079 jv+jv -⍝⍝: 0.76 0.62 0.75 instructions linux 2.243 allocated linux 2.085 +⍝⍝: 0.76 0.62 0.75 instructions macos 3.373 linux 2.243 allocated macos 2.085 linux 2.085 kv+kv -⍝⍝: 3 2.4 3.2 instructions linux 3.712 allocated linux 8.085 +⍝⍝: 3 2.4 3.2 instructions macos 6.175 linux 3.712 allocated macos 8.085 linux 8.085 v×2 -⍝⍝: 2 2.1 1.9 instructions linux 1.939 allocated linux 8.085 +⍝⍝: 2 2.1 1.9 instructions macos 3.341 linux 1.939 allocated macos 8.085 linux 8.085 jv×2.5 -⍝⍝: 1.3 1.5 2.2 instructions linux 3.206 allocated linux 10.089 +⍝⍝: 1.3 1.5 2.2 instructions macos 5.528 linux 3.206 allocated macos 10.089 linux 10.089 jv=jv -⍝⍝: 0.62 0.5 1.1 instructions linux 1.583 allocated linux 1.081 +⍝⍝: 0.62 0.5 1.1 instructions macos 2.405 linux 1.583 allocated macos 1.081 linux 1.081 jv∧ᵇjv -⍝⍝: 0.76 0.5 0.73 instructions linux 2.387 allocated linux 2.112 +⍝⍝: 0.76 0.5 0.73 instructions macos 4.215 linux 2.387 allocated macos 2.112 linux 2.112 b×jv -⍝⍝: 0.62 0.54 0.69 instructions linux 1.968 allocated linux 2.585 +⍝⍝: 0.62 0.54 0.69 instructions macos 2.998 linux 1.968 allocated macos 2.585 linux 2.585 ⍝⍝⍝ held linux 2.81: Syd1's Boolean-by-integer product costs 2.2 times the 1.25 its Linux budget allows; cause not traced b×kv -⍝⍝: 2.2 2.5 3.8 instructions linux 6.623 allocated linux 10.085 +⍝⍝: 2.2 2.5 3.8 instructions macos 9.212 linux 6.623 allocated macos 10.085 linux 10.085 v⌊w -⍝⍝: 3 4.4 3.8 instructions linux 2.105 allocated linux 8.079 +⍝⍝: 3 4.4 3.8 instructions macos 3.983 linux 2.105 allocated macos 8.079 linux 8.079 b∧b -⍝⍝: 0.5 0.5 0.5 instructions linux 1.542 allocated linux 1.083 +⍝⍝: 0.5 0.5 0.5 instructions macos 2.522 linux 1.542 allocated macos 1.083 linux 1.083 c=c -⍝⍝: 1.2 0.77 2 instructions linux 1.905 allocated linux 1.1 +⍝⍝: 1.2 0.77 2 instructions macos 2.943 linux 1.905 allocated macos 1.1 linux 1.1 ⍝ Expected: 0.73 copies: 0.5 for writing 8 bytes an item, and one node, about 0.23 copies of 65,536 floats (`fusion::MAC_NODE_NS`, 1,800 ns), because the call waits under the cost rule and nothing fuses with it (decision 13). Jeremy accepted a lone 65,536-item call's node on 30 October 2026, as for `c=c`. Main set this expected cost on 31 October 2026, for Jeremy's review. In the browser the call runs at once, because the cost rule's limit there, from the browser's node estimate of 4 × `MAC_NODE_NS`, is about 830 KB, above the call's 512 KB. CI measured it there at 1.06 copies. ⍳√n ×⊗ ⍳√n -⍝⍝: 1.5 1.7 1.9 instructions linux 4.935 allocated linux 8.379 +⍝⍝: 1.5 1.7 1.9 instructions macos 6.694 linux 4.935 allocated macos 8.379 linux 8.379 ⍝ Arithmetic on two keyed vectors whose keys are equal but were built separately, as the same 65,536 texts made twice. ⍝ Expected: 1.5 copies: as `v+w`, once the call knows that the two key vectors are equal. ⍝ On 30 October 2026 `ra+2×ra`, whose operands share one key object, cost 1.62 on macOS. -⍝ On 31 October 2026 `ra+rb` cost about 1.7 on macOS: every call after the first finds its union of keys among the latest that rustymath keeps for each thread (`layout::recalled`). +⍝ Fresh calls compare ordered key labels. Pending execution retains the resulting maps; key unions are not cached. ra+rb -⍝⍝: 3 25 33 instructions linux 42.47 allocated linux 8.096 +⍝⍝: 3 25 33 instructions macos 58.717 linux 42.47 allocated macos 8.096 linux 8.096 ⍝ Products of 64-bit integers that leave 64 bits become exact big integers, each held in an allocation of its own. ⍝ Expected: 220 copies: about 25 ns an item for an allocation, a free and a 128-bit product. kv×kv -⍝⍝: 330 2.4 3.3 instructions linux 5.727 allocated linux 8.085 +⍝⍝: 330 2.4 3.3 instructions macos 7.695 linux 5.727 allocated macos 8.085 linux 8.085 ⍝ A tolerant gcd of floats walks the continued fraction of each pair's ratio in 64-bit integers wherever the ratio fits. ⍝ Expected: 700 copies: about 35 continued-fraction steps per pair, each waiting on a float division of about 10 cycles. v∨w -⍝⍝: 910 1300 1400 instructions linux 1964.33 allocated linux 8.079 +⍝⍝: 910 1300 1400 instructions macos 1945.9 linux 1964.33 allocated macos 8.079 linux 8.079 ⍝ Compressing by a mask can't be vectorised, because each kept item's place depends on the items before it. ⍝ Expected: `b#v` 2 copies: about 1 cycle an item, since each kept item's place depends on the items before it; `⍸b` 2 copies: about 1 cycle an item, as compression. b#v -⍝⍝: 4 6.3 5.8 instructions linux 13.124 allocated linux 2.736 +⍝⍝: 4 6.3 5.8 instructions macos 12.925 linux 13.124 allocated macos 2.736 linux 2.736 ⍸b -⍝⍝: 4 2.4 3.1 instructions linux 4.856 allocated linux 0.708 +⍝⍝: 4 2.4 3.1 instructions macos 5.162 linux 4.856 allocated macos 0.708 linux 0.708 ⍝ Replicating by counts of 0 to 2 writes four copies of each item where its run starts. The next item overwrites the copies past its count. A group of 4 items whose counts are all at most 4 writes after one check for the group (`gather::repeat`). With a check and a taken branch for each item, the loop cost about a third more in the speed test's binary than through `bpl`: `cnt#v` measured 7.1 copies there against 5.3, and `(0+cnt)#v` 9.7 against 8.1. ⍝ Expected: 4 copies: about 2 cycles an item for four stores, which the next item overwrites past its count. ⍝⍝⍝ held linux 8.55: Syd1's replication by counts costs 2.1 times the 4 its Linux budget allows; cause not traced cnt#v -⍝⍝: 8 8 13 instructions linux 27.182 allocated linux 16.078 +⍝⍝: 8 8 13 instructions macos 29.183 linux 27.182 allocated macos 16.078 linux 16.078 ⍝ Counts held as floats are checked for whole numbers 256 at a time before they replicate. ⍝ Expected: 6 copies: as `cnt#v`, plus about 1 copy for the add that makes the counts floats, and about 1 more for the whole-number check than `cnt#v` pays to widen its 1-byte counts, because the check rounds each float and writes it as a 64-bit integer. ⍝ It has cost 25 to 26 in wasm in every CI run from 53 to 62, where `cnt#v` costs about 6.7. 0+cnt # v -⍝⍝: 12 22 48 instructions linux 36.785 allocated linux 26.158 +⍝⍝: 12 22 48 instructions macos 41.452 linux 36.785 allocated macos 26.158 linux 26.158 ⍝ A range with an approximate length converts each position from an integer to a float. Wasm has no vector instruction for that, so there each block of 8 positions adds 0 to 7 to the block's first float. ⍝ Expected: 0.5 copies: writes 8 bytes an item. ⍳n -⍝⍝: 1 1.8 1.9 instructions linux 6.507 allocated linux 2.032 +⍝⍝: 1 1.8 1.9 instructions macos 9.054 linux 6.507 allocated macos 2.032 linux 2.032 ⍝ Comparing within tolerance takes a subtraction, the magnitudes, a scale and a comparison for each item. Tolerant floor raises each item by its scaled magnitude, capped below a half, before it floors. ⍝ Expected: `⌊v` 2.75 copies: about 11 vector operations an item, 6 for the tolerance and 5 for the conversion to a 64-bit integer, its range check, and the select and flag for an item that doesn't fit; `v]s` 2 copies: a tolerant comparison and a negation, blended; `(0>)⍚[- 2×] s` 2.5 copies: a tolerant comparison and two cases, blended. ⍝⍝⍝ held macos 4.23: open.md lists the masked selects among the speed audit's outliers -@[0>]s -⍝⍝: 4 11 17 instructions linux 8.348 allocated linux 17.254 +⍝⍝: 4 11 17 instructions macos 11.997 linux 8.348 allocated macos 17.254 linux 17.254 (0>)⍚[- 2×] s -⍝⍝: 5 13 20 instructions linux 10.681 allocated linux 25.383 +⍝⍝: 5 13 20 instructions macos 15.283 linux 10.681 allocated macos 25.383 linux 25.383 ⍝ An approximate divisor reads the integers as floats. Each item then needs a float division and a floor within tolerance. ⍝ Expected: 3.5 copies: a conversion, a float division of about 1 cycle an item, and a tolerant floor. 2.5|jv -⍝⍝: 7 7.2 13 instructions linux 8.778 allocated linux 10.089 +⍝⍝: 7 7.2 13 instructions macos 8.447 linux 8.778 allocated macos 10.089 linux 10.089 ⍝ A whole divisor that stands for an integer divides the integers as that integer, with multiplications, and the residues become 8-byte floats. ⍝ Expected: 2.5 copies: the integer residue, and the writes of 8-byte floats in place of 2-byte integers. On macOS the residue, which divides in 16-bit lanes (`integer::Short`), measures about 0.9 copies, and the second pass that converts the residues to floats about 0.6. ⍝⍝⍝ held linux 3.99: Syd1's residue and float writes cost 2.3 times the 1.75 its Linux budget allows; cause not traced 3|jv -⍝⍝: 5 3.5 5.4 instructions linux 8.834 allocated linux 10.089 +⍝⍝: 5 3.5 5.4 instructions macos 8.482 linux 8.834 allocated macos 10.089 linux 10.089 ⍝ rustymath's `exp` evaluates a polynomial of 10 terms for each item, and `ln` one of 7 terms and a division. ⍝ Expected: `*s` 5.75 copies: rustymath's f64 `Exp`, which takes each block of 64 items within ±708 through a form with no clamp and one power of two, as rustymath's `tests/speed.txt` counts it; `⍟v` 6.5 copies: rustymath's f64 `Ln`, which takes each block of 64 positive, normal, finite items through a form with no special values, as rustymath's `tests/speed.txt` counts it. `*s` was 6.5 copies until 23 October 2026, when every item took the clamp and two powers of two, and 4.5 before that, from a count of 18 that left out the clamp and building 2^n. `⍟v` was 9 copies until 23 October 2026, when every item took the full form with its special values, and 5.3 before that, from a count that left out the split, the selects and the check for NaN. *s -⍝⍝: 12 10 36 instructions linux 11.263 allocated linux 8.06 +⍝⍝: 12 10 36 instructions macos 23.74 linux 11.263 allocated macos 8.06 linux 8.06 ⍟v -⍝⍝: 13 9.9 36 instructions linux 17.199 allocated linux 8.06 +⍝⍝: 13 9.9 36 instructions macos 27.178 linux 17.199 allocated macos 8.06 linux 8.06 ⍝ A dynamic square root or logarithm of a real array with negatives gives complex results. The checked real kernel fails at the first negative item, and the call runs again at the complex type (`pervasive::searched`), computing each result from its real item (`Formula::real_complex`): √|x| placed by the sign, or ln|x| with 0 or π. ⍝ Expected, as timed on 6 November 2026: `√s` 6 copies: about 4.4 instructions an item for the failed real pass and 4.5 for the complex results; `⍟s` 16 copies: about 16.4 for the failed real pass and 17 for the complex results. Wasm: twice each, without fused multiply-add. They were 35 and 115 copies while the rerun promoted each item to complex. √s -⍝⍝: 12.5 36 41 instructions linux 9.888 allocated linux 24.06 +⍝⍝: 12.5 36 41 instructions macos 10.216 linux 9.888 allocated macos 24.06 linux 24.06 ⍟s -⍝⍝: 33 86 210 instructions linux 34.395 allocated linux 24.06 +⍝⍝: 33 86 210 instructions macos 51.487 linux 34.395 allocated macos 24.06 linux 24.06 ⍝ rustymath's sine and cosine reduce each item by π in three fused multiply-adds, and evaluate a polynomial of 7 terms. Tangent reduces by π/2, evaluates a polynomial of 9 terms at half the angle, and divides once. ⍝ Expected: `1○v` and `2○v` 7 copies: about 28 vector operations an item, for the range check, the reduction, the polynomial, the sign flip, the select at 0 and the test that sends rare items to the second pass; `3○v` 8 copies: about 32 vector operations an item, for the range check, the reduction, the polynomial at half the angle, the selects that pick the quotient's terms and the test, with a division that overlaps them. `1○v` and `2○v` were 4 copies, from a count of 16 that took in only the reduction and the polynomial, then 7.3 copies until sine stopped setting the sign of a zero result, which the select at 0 already gives. `3○v` was 6.3 copies, from a count of 17 that left out the range check, the selects and the test, and charged the division 2 copies. 1○v -⍝⍝: 14 12 39 instructions linux 13.882 allocated linux 8.06 +⍝⍝: 14 12 39 instructions macos 24.112 linux 13.882 allocated macos 8.06 linux 8.06 2○v -⍝⍝: 14 12 44 instructions linux 14.507 allocated linux 8.06 +⍝⍝: 14 12 44 instructions macos 25.486 linux 14.507 allocated macos 8.06 linux 8.06 3○v -⍝⍝: 16 17 57 instructions linux 15.007 allocated linux 8.06 +⍝⍝: 16 17 57 instructions macos 28.361 linux 15.007 allocated macos 8.06 linux 8.06 ⍝ The hyperbolic functions build on rustymath's `exp` and `expm1`, with a division. The inverse circle functions evaluate a polynomial of 10 or 12 terms after a reduction with a square root or a division. ⍝ Expected: `5○v` 10 copies: about 40 vector operations an item, for `expm1`'s 34 and the arithmetic around it, with a division that overlaps them; `7○v` 7.5 copies: rustymath's f64 `Tanh`, whose `expm1` needs no guards for |x| up to 20, as rustymath's `tests/speed.txt` counts it; `6○v` 7.25 copies: about 29 vector operations an item, for `exp`'s 26 and the arithmetic around it, with a division that overlaps them; `1○⁻v` 7 copies and `2○⁻v` 7.5 copies: about 28 and 30 vector operations an item, for the reduction, the polynomial of 12 terms, the selects that undo the reduction and the check for a NaN made from a number, with a square root that overlaps them; `3○⁻v` 7.25 copies: about 29 vector operations an item, for the selects that pick one of three intervals, the polynomial of 10 terms, the selects of the added multiple of π/4 and the sign, with a division that overlaps them. `5○v` and `7○v` were 10.5 copies until `expm1` stopped selecting -0 for a zero argument, which only set the sign of a zero result. `5○v`, `6○v` and `7○v` were 11, 6.5 and 6.5 copies before that, from `exp`'s old count of 18, two exponentials for `5○v` where it computes one `expm1`, and 2 copies for each division. `1○⁻v`, `2○⁻v` and `3○⁻v` were 6, 6 and 5.5 copies, from counts of 16, 16 and 14 that left out most of the selects and the check, and charged the square root or division 2 copies. 5○v -⍝⍝: 20 20 58 instructions linux 14.887 allocated linux 8.06 +⍝⍝: 20 20 58 instructions macos 29.363 linux 14.887 allocated macos 8.06 linux 8.06 6○v -⍝⍝: 15 14 49 instructions linux 11.889 allocated linux 8.06 +⍝⍝: 15 14 49 instructions macos 24.865 linux 11.889 allocated macos 8.06 linux 8.06 7○v -⍝⍝: 15 14 54 instructions linux 14.637 allocated linux 8.06 +⍝⍝: 15 14 54 instructions macos 28.364 linux 14.637 allocated macos 8.06 linux 8.06 1○⁻v -⍝⍝: 14 16 52 instructions linux 14.215 allocated linux 8.084 +⍝⍝: 14 16 52 instructions macos 25.481 linux 14.215 allocated macos 8.084 linux 8.084 2○⁻v -⍝⍝: 15 17 52 instructions linux 15.465 allocated linux 8.084 +⍝⍝: 15 17 52 instructions macos 27.481 linux 15.465 allocated macos 8.084 linux 8.084 3○⁻v -⍝⍝: 15 17 51 instructions linux 12.774 allocated linux 8.084 +⍝⍝: 15 17 51 instructions macos 23.543 linux 12.774 allocated macos 8.084 linux 8.084 ⍝ The inverse hyperbolic functions build on `ln`'s reduction, after a square root or a division. `4○` is √(1 + x²), computed as rustymath's `hypot` of x and 1. ⍝ Expected: `5○⁻v` 10.75 copies: about 43 vector operations an item, for the forms near 0 and beyond 2^52, `ln`'s split, the polynomial and the selects for NaN and ∞, with a square root and a division that overlap them; `4○v` 4 copies: about 16 vector operations an item, for scaling x and 1 by the power of two that keeps x² finite and the selects for the larger and for ∞, with a square root that overlaps them. `5○⁻v` was 7.3 copies, from a square root and a `log1p` charged the old way. `4○v` was 2.5 copies, from a square root charged 2 copies and two operations, without `hypot`'s scaling. 5○⁻v -⍝⍝: 22 26 57 instructions linux 17.771 allocated linux 8.084 +⍝⍝: 22 26 57 instructions macos 30.044 linux 17.771 allocated macos 8.084 linux 8.084 4○v -⍝⍝: 8 5.5 17 instructions linux 5.634 allocated linux 8.06 +⍝⍝: 8 5.5 17 instructions macos 12.358 linux 5.634 allocated macos 8.06 linux 8.06 ⍝ Monadic `○`, cis, computes each result from its real item through `Cis`'s real-argument body (`Formula::real_complex`): the cosine and sine formulas, with arguments past 2^23 taking the complex form. ⍝ Expected: 14 copies: sine's 26 and cosine's 28 vector operations, as rustymath's speed test counts them for `Sin` and `Cos`, and 16-byte writes. It measured 24 copies on 6 November 2026: the complex pairs it writes keep the loop at two lanes, where `1○v` runs four. ⍝ In wasm, CI's runner gave 47 under Node 22's V8 on 25 October 2026, and 55 to 58 under Node 26's from 28 October 2026. V8's newer x86-64 code caused the rise: under Node 26 on Apple silicon, the 0.1.75 release and a build of 29 October 2026 cost the same. Its wasm budget is set from Node 26's 57. ○v -⍝⍝: 28 37 80 instructions linux 98.98 allocated linux 16.06 +⍝⍝: 28 37 80 instructions macos 165.738 linux 98.98 allocated macos 16.06 linux 16.06 ⍝ A constant exponent that is whole and from 1 to 8 in magnitude squares and multiplies, and one of 0.5 or 1.5 starts from the square root (`rustymath::Exponent`), each in a loop of its own. Another exponent calls rustymath's `pow` for each item, which at f64 reads tables that NEON and wasm can't vectorize. ⍝ Expected: `v*2` 1 copy: one read and one write per item; `v*0.5` 2 copies: one square root of each item, as `√v` takes, which measured 1.88 on 30 October 2026; `v*w` 15 copies: a vectorised `pow` in extra precision, about twice `⍟` and `*` together. Before 30 October 2026, `v*0.5` took `pow`'s route, at 13 times `√v`'s cost. v*2 -⍝⍝: 2 2.1 2.2 instructions linux 2.201 allocated linux 8.085 +⍝⍝: 2 2.1 2.2 instructions macos 3.308 linux 2.201 allocated macos 8.085 linux 8.085 v*0.5 -⍝⍝: 4 33 82 instructions linux 9.518 allocated linux 8.085 +⍝⍝: 4 33 82 instructions macos 12.931 linux 9.518 allocated macos 8.085 linux 8.085 v*w -⍝⍝: 30 32 82 instructions linux 39.186 allocated linux 8.079 +⍝⍝: 30 32 82 instructions macos 90.966 linux 39.186 allocated macos 8.079 linux 8.079 ⍝ Bases 2 and 10 and degrees 2 and 3 have loops of their own. ⍝ Expected: `2⍟v` and `10⍟v` 6.5 copies: as `⍟v`, through the fast form of rustymath's `Logb` for bases 2 and 10; `3√v` 9.25 copies: about 37 vector operations an item, for the step for subnormals, the first estimate from the exponent bits, three Newton steps, the last correction, the select for 0, ∞ and NaN, and the sign. `2⍟v` and `10⍟v` were 9 copies, as `⍟v` was. `3√v` was 4 copies, from a count of 15 that took in only the Newton steps. 2⍟v -⍝⍝: 13 12 36 instructions linux 11.1 allocated linux 8.085 +⍝⍝: 13 12 36 instructions macos 20.724 linux 11.1 allocated macos 8.085 linux 8.085 10⍟v -⍝⍝: 13 11 37 instructions linux 11.104 allocated linux 8.085 +⍝⍝: 13 11 37 instructions macos 20.725 linux 11.104 allocated macos 8.085 linux 8.085 3√v -⍝⍝: 19 18 60 instructions linux 18.006 allocated linux 8.085 +⍝⍝: 19 18 60 instructions macos 26.548 linux 18.006 allocated macos 8.085 linux 8.085 ⍝ Exact integers read as floats for a function whose results are always approximate. ⍝ Expected: `⍟jv` 7 copies: as `⍟v`, with about 2 operations to convert each integer; `*jv` 7 copies: as `*s`, with the same conversion; `jv⍟2ₓ` 10.5 copies: a conversion and a logarithm of each base, a division, and a second pass that writes the items whose base is 2 or 10 from `log2` or `log10`; `√jv` 2 copies: a conversion and a square root. `⍟jv` was 9.5 copies, as `⍟v` was 9, and `*jv` and `jv⍟2ₓ` were 4.5 and 7.3 copies, from the old count for `*s`. ⍟jv -⍝⍝: 14 12 40 instructions linux 18.489 allocated linux 10.062 +⍝⍝: 14 12 40 instructions macos 29.427 linux 18.489 allocated macos 10.062 linux 10.062 *jv -⍝⍝: 14 15 49 instructions linux 12.553 allocated linux 10.062 +⍝⍝: 14 15 49 instructions macos 25.986 linux 12.553 allocated macos 10.062 linux 10.062 jv⍟2ₓ -⍝⍝: 21 21 63 instructions linux 18.987 allocated linux 10.089 +⍝⍝: 21 21 63 instructions macos 31.358 linux 18.987 allocated macos 10.089 linux 10.089 √jv -⍝⍝: 4 6.1 5.9 instructions linux 6.471 allocated linux 10.062 +⍝⍝: 4 6.1 5.9 instructions macos 8.144 linux 6.471 allocated macos 10.062 linux 10.062 ⍝ `!` calls rustymath's `gamma`, which sums its Lanczos series in double words. A distribution's density, cdf and quantile each run over the array as one slice. The quantile's `erfcinv` sorts each block of items by region, then evaluates each region's rational approximation over its items, with no iteration. `⌻` evaluates a polynomial of degree 7 by Horner's rule. ⍝ Expected: `!v` 132 copies: `Gamma`'s 530 vector operations, as rustymath's speed test counts them, for a double-word Lanczos sum and quotient, a double-word ln of the exponent, `exp_plus`, and `sinpi` for the reflection; `nd.density v` 7.8 copies: `exp`'s 26 vector operations and five more an item, with `sd`'s reciprocals computed once; `nd.cdf v` 8 copies: its inputs all lie where `erfc` takes `erfc_near`, a ratio of polynomials in x² with one division and no exponential, about 31 vector operations an item with the argument and `erfc`'s range checks; `nd.quantile v` 22.5 copies: `Erfcinv`'s 85 vector operations, 21 copies as rustymath's speed test counts them, the check that each probability lies from 0 to 1, and the scaling on each side; `coeffs⌻v` 2.75 copies: a read and a write, and seven fused multiply-adds an item. `nd.density v` was 5.5 copies, from the old count for `exp`. `!v`, `nd.quantile v` and `coeffs⌻v` were 80, 12 and 1.8 copies, from counts that left out most of `gamma`'s and `erfcinv`'s pieces, and the reads and writes. ⍝ Its wasm expected cost is 264, twice its native one, because wasm's SIMD has no fused multiply-add. !v -⍝⍝: 200 160 540 instructions linux 157.492 allocated linux 8.06 +⍝⍝: 200 160 540 instructions macos 409.237 linux 157.492 allocated macos 8.06 linux 8.06 nd.density v -⍝⍝: 16 15 53 instructions linux 14.195 allocated linux 8.162 +⍝⍝: 16 15 53 instructions macos 32.649 linux 14.195 allocated macos 8.162 linux 8.162 ⍝ `nd.cdf v` measured 15.8 and 16.8 copies on Linux in two CI runs of v0.1.62, with two divisions an item, and 16.5 in v0.1.64's run after its argument multiplied by the reciprocal of `sd`. x86-64 builds still divided for each item, because the closure read `sd` through a reference. Since 22 October 2026 `erfc_line` computes the reciprocal once, which took macOS from 11.1 copies to 9.65. nd.cdf v -⍝⍝: 16 16 53 instructions linux 16.195 allocated linux 16.158 +⍝⍝: 16 16 53 instructions macos 31.654 linux 16.195 allocated macos 16.158 linux 16.158 ⍝ `nd.quantile v` measures 31 on macOS, where `Erfcinv`'s loop takes 86% of it and retires about 91 instructions an item, nearly all in its rational approximations. Since 22 October 2026 `regions` computes each item's arguments once, which took about 5 instructions an item off. Its wasm expected cost is 45, twice its native one, because wasm's SIMD has no fused multiply-add. nd.quantile v -⍝⍝: 45 67 100 instructions linux 91.713 allocated linux 16.162 +⍝⍝: 45 67 100 instructions macos 119.019 linux 91.713 allocated macos 16.162 linux 16.162 ⍝ The log-normal density takes ln x from `Ln`'s slice form, then the normal density at it over x. The log-normal cdf and quantile run the normal slice forms at ln x, the quantile through an `exp`. ⍝ Expected: `lg.density v` 18 copies: `Ln`'s 36 vector operations, the normal density's 31, and a clamp, a division and a select, about 72 an item; `lg.cdf v` 20 copies: `ln` and the argument, about 38 operations, `erfc`'s mixed route, about 33, and 7 for its range checks and the result; `lg.quantile v` 29 copies: `nd.quantile v`'s expected 22.5, and an `exp` of 26 operations. ⍝ Wasm: twice each native cost, so `lg.density v` 36 copies. Wasm's SIMD has no fused multiply-add, so `FUSED` is false there, and each `mla` in `ln`, `exp`, `erfc` and the normal formulas is a multiply and an add. `nd.density v` costs 16.2 in wasm against 9.8 natively, and `lg.density v` 35.3 against 22.7. lg.density v -⍝⍝: 36 28 95 instructions linux 26.265 allocated linux 16.162 +⍝⍝: 36 28 95 instructions macos 51.081 linux 26.265 allocated macos 16.162 linux 16.162 ⍝ `nd.cdf ⍟v` alone costs 35 on macOS. ⍝⍝⍝ held macos 39.3: its `erfc` arguments, -ln v/√2, span `ERFC_SPLIT`, so every block takes `erfc`'s mixed route, which on AArch64 picks `erfc_near` or `erfc_far` with a branch for each item lg.cdf v -⍝⍝: 40 81 130 instructions linux 77.047 allocated linux 16.158 +⍝⍝: 40 81 130 instructions macos 105.753 linux 77.047 allocated macos 16.158 linux 16.158 ⍝ `lg.quantile v` measures 39 on macOS and 41 on Linux. lg.quantile v -⍝⍝: 58 74 140 instructions linux 101.154 allocated linux 16.162 +⍝⍝: 58 74 140 instructions macos 140.719 linux 101.154 allocated macos 16.162 linux 16.162 ⍝ The other closed-form laws compute at their arguments' width. A law's logarithm, arctangent, tangent, power or root comes from rustymath's slice form of `Log1p`, `Ln`, `Atan`, `Tan`, `Pow` or `Root`, a block at a time, between a pass that writes the block's arguments and a pass that finishes its results and checks them for NaN. The uniform and Laplace cdfs are single loops. ⍝ Expected: `ud.cdf v` 2.5 copies: a division of about 1 cycle an item, which the subtraction and the clamp's compares and selects overlap; `ex.quantile v` 13.25 copies: `Log1p`'s 40 vector operations, a pass that negates each item, and a pass of about 9 for the range check, the scaling and the NaN check; `ca.cdf v` 12.5 copies: `Atan`'s 29 operations, as `3○⁻v` counts them, a pass of about 6 with a division that picks z or -1/z, and a pass of about 13 that rebuilds the cdf and checks it; `ca.quantile v` 13.5 copies: `Tan`'s 32 operations, as `3○v` counts them, a pass of about 7 that picks the argument's region, and a pass of about 15 with a division that rebuilds the quantile and checks it; `lp.cdf v` 9.25 copies: about 37 operations in one loop, for the full `exp` of -|z|, the select between the two halves and the NaN check; `lp.quantile v` 9.25 copies: `Ln`'s 26 operations, a pass of about 4 and a pass of about 7; `lo.quantile v` 10 copies: `Ln`'s 26 operations, a pass whose division of about 1 cycle an item forms the odds, and a pass of about 4; `wb.cdf v` 24.5 copies: `Pow`'s 15 copies, as `v*w` expects them, a pass of 3 operations, and a pass of about 34 for the full `expm1` and the NaN check; `wb.quantile v` 28.5 copies: `Log1p`'s 10 copies, a pass that negates its results, `Root`'s cost taken as `Pow`'s 15, and two short passes. ⍝ macOS: at f64 `Pow` and `Root` read tables that NEON can't gather, and `v*w` measures about 28 copies on this Mac against its expected 15. With that cost, `wb.cdf v` and `wb.quantile v` expect 37.5 and 41.5 copies on macOS, and measured 36 and 37 on 30 October 2026. Wasm: twice each native cost, because wasm's SIMD has no fused multiply-add, as for `lg.density v`, and for the Weibull cases twice the macOS cost, 75 and 83, because wasm can't gather either. CI measured them at 67 to 71 and 71 to 73 in wasm in runs 56 to 62. `ud.cdf v` multiplies and adds nothing, so its wasm budget is its native one. ud.cdf v -⍝⍝: 5 4.7 6.5 instructions linux 5.176 allocated linux 8.158 +⍝⍝: 5 4.7 6.5 instructions macos 8.76 linux 5.176 allocated macos 8.158 linux 8.158 ex.quantile v -⍝⍝: 27 25 56 instructions linux 19.984 allocated linux 16.162 +⍝⍝: 27 25 56 instructions macos 35.1 linux 19.984 allocated macos 16.162 linux 16.162 ca.cdf v -⍝⍝: 25 25 64 instructions linux 21.518 allocated linux 16.158 +⍝⍝: 25 25 64 instructions macos 36.86 linux 21.518 allocated macos 16.158 linux 16.158 ca.quantile v -⍝⍝: 27 28 75 instructions linux 24.379 allocated linux 16.162 +⍝⍝: 27 28 75 instructions macos 49.577 linux 24.379 allocated macos 16.162 linux 16.162 lp.cdf v -⍝⍝: 19 17 54 instructions linux 15.569 allocated linux 8.158 +⍝⍝: 19 17 54 instructions macos 29.659 linux 15.569 allocated macos 8.158 linux 8.158 lp.quantile v -⍝⍝: 19 18 47 instructions linux 18.12 allocated linux 16.162 +⍝⍝: 19 18 47 instructions macos 31.185 linux 18.12 allocated macos 16.162 linux 16.162 lo.quantile v -⍝⍝: 20 18 46 instructions linux 17.109 allocated linux 16.162 +⍝⍝: 20 18 46 instructions macos 28.21 linux 17.109 allocated macos 16.162 linux 16.162 wb.cdf v -⍝⍝: 75 51 110 instructions linux 19.097 allocated linux 16.158 +⍝⍝: 75 51 110 instructions macos 37.135 linux 19.097 allocated macos 16.158 linux 16.158 wb.quantile v -⍝⍝: 83 59 110 instructions linux 56.576 allocated linux 16.162 +⍝⍝: 83 59 110 instructions macos 115.899 linux 56.576 allocated macos 16.162 linux 16.162 ⍝ `coeffs⌻v` measures 3.46 on macOS. Wasm: 4.5 copies, because wasm's SIMD has no fused multiply-add, so each of the seven is a multiply and an add. coeffs⌻v -⍝⍝: 5.5 2.6 5.6 instructions linux 5.444 allocated linux 8.055 +⍝⍝: 5.5 2.6 5.6 instructions macos 15.346 linux 5.444 allocated macos 8.055 linux 8.055 ⍝ Each item goes into a hash table, and each item of the other argument is looked up in it. A tolerant search looks in two buckets for each item, where an exact search looks in one. ⍝ Expected: 60 copies: about 30 cycles an item to insert and look up in a hash table. Linux and wasm, which runs on the same runner: 125 copies. Scalar work costs about 1.7 times as many copies on CI's x86 runner as on macOS, as `fib 16` and the exact `v⍳w` show. The tolerant table and its chains, about 2.9 MB, also overflow that runner's 1 MB L2, where macOS's L2 holds them, which adds about a quarter. CI measured 121 to 133. v⍳w -⍝⍝: 90 95 150 instructions linux 200.737 allocated linux 52.033 +⍝⍝: 90 95 150 instructions macos 92.847 linux 200.737 allocated macos 64.032 linux 52.033 ⍝ A radix sort moves every item once for each byte that differs between the keys. Keys that span a small range, as `jv`'s and `b`'s do, take a counting sort, which places each item once. ⍝ Expected: `⍋v` 50 copies: a pass that counts each byte of every key, about 14, then a pass for each of the 8 bytes, at about 4.5 copies each. The count pass makes 8 counts for each key, each a load, an increment and a store, at about one a cycle. A byte pass moves each key and its 4-byte position to the next place in its byte's bucket. Nearly every key of `v` shares its top byte, which holds a float's sign and the top 7 bits of its exponent. That byte's pass moves each block of 64 keys that share it with one copy, at about the cost of another pass. `⍋jv` 6 copies: a counting sort, with a histogram pass and one scattered write of 4-byte positions. The histogram costs about 2 cycles an item, because each count is a load, an increment and a store that the next item with the same key waits for, where a copy costs about half a cycle an item. Linux: `⍋jv` 10 copies, because scalar work costs about 1.7 times as many copies on CI's x86 runner, as `v⍳w`'s comment says; CI measured 10.24. `⍋b` 1 copy: two passes writing 4-byte positions. ⍝ Linux and wasm: `⍋v` keeps 1.5 times its costs on 29 October 2026, 82.6 and 63.7, until CI times the block pass. ⍋v -⍝⍝: 75 130 110 instructions linux 166.661 allocated linux 26.042 +⍝⍝: 75 130 110 instructions macos 165.552 linux 166.661 allocated macos 26.042 linux 26.042 ⍋jv -⍝⍝: 12 15 12 instructions linux 23.318 allocated linux 8.776 +⍝⍝: 12 15 12 instructions macos 25.643 linux 23.318 allocated macos 8.776 linux 8.776 ⍋b -⍝⍝: 2 4.2 5.2 instructions linux 7.279 allocated linux 8.044 +⍝⍝: 2 4.2 5.2 instructions macos 8.519 linux 7.279 allocated macos 8.044 linux 8.044 ⍝ A sort of keys that span a small range counts them, then writes each key as many times as it occurs. It needs no positions. ⍝ Expected: ``, 4,096 operations an item, whose sums pass what `f32` holds exactly. On macOS they run in Accelerate's `f64` product at about 58 flops in a copy's time, as `m+.×m` does, 70 copies, after converting both operands to `f64`, 8, and before converting the results to `i32`, 4, then wrapping them to `u8`, 3. Elsewhere they run on rustymath's `i32` kernel, which converts the bytes as it packs them, at about 16 integer operations in a copy's time on AVX2's eight 32-bit lanes and 8 on wasm's four, 256 and 512 copies, then wrap to `u8`, 3. On 25 October 2026 it measured 94 on macOS, in runs where `m+.×m` measured 10.2. With `--native` it measured 428: the `i32` kernel does about 10 operations in a copy's time on NEON's four lanes. um+.×um -⍝⍝: 130 560 660 instructions linux 320.254 allocated linux 101.488 +⍝⍝: 130 560 660 instructions macos 47.178 linux 320.254 allocated macos 77.238 linux 101.488 ⍝ Expected: `umₓ₄+.×umₓ₄` costs what `um+.×um` costs. Each `ₓ₄` leaves its conversion to `i32` pending, and the product reads the `u8` matrices through both conversions (`pervasive::widened_product`) on the same kernels, so no `i32` copy is stored. On 25 October 2026 it measured 107 on macOS, where `um+.×um` measured 102, and 409 on the build from before, which stored both copies and ran rustymath's `i32` kernel. umₓ₄+.×umₓ₄ -⍝⍝: 130 570 750 instructions linux 329.804 allocated linux 121.448 +⍝⍝: 130 570 750 instructions macos 58.668 linux 329.804 allocated macos 97.205 linux 121.448 jm+.=jm -⍝⍝: 64 59 74 instructions linux 45.498 allocated linux 35.823 +⍝⍝: 64 59 74 instructions macos 69.218 linux 45.498 allocated macos 35.885 linux 35.823 ⍝⍝⍝ allows forced "+.=": an inner product reads its arguments whole cm+.=⍉cm -⍝⍝: 64 67 79 instructions linux 50.528 allocated linux 39.906 +⍝⍝: 64 67 79 instructions macos 71.909 linux 50.528 allocated macos 39.968 linux 39.906 zm+.×zm -⍝⍝: 12 55 77 instructions linux 60.44 allocated linux 41.152 +⍝⍝: 12 55 77 instructions macos 12.023 linux 60.44 allocated macos 4.902 linux 41.152 ⍝ Matrix division, `•decompose`, `•decompose⁻` and the determinant `-.×` run on rustymath. Its own speed test times each factorisation on a 256×256 matrix, the size of `m`, and least squares on a 4096×16 matrix, the size of `t`. On macOS, Accelerate runs matrix products and triangular solves, and the SVD, the Hermitian eigendecomposition and the real nonsymmetric eigendecomposition from the sizes that rustymath's DEV.md gives. rustymath's own code runs LU, Householder QR, Cholesky, the complex nonsymmetric eigendecomposition and the smaller matrices' decompositions, with their block products and triangular solves on Accelerate. On Linux it runs all of them. These cases time basedpl's part on top of each factorisation: reading the arguments, writing the results, the calls in a `⍤2` stack, each path of `⌹`, the route from monadic `-.×` to LU, and rebuilding a matrix from factors. `tq` is tall enough for `⌹` and `"qr"` to take the CholeskyQR2 path. ⍝ Expected, taking each factorisation's cost from rustymath's speed test on the same platform: `⌹⍤2 ms` 30 copies: 1,024 inverses of 8×8, each about 1,000 cycles of small-matrix work; `ms⌹⍤2 sps` 45 copies: 1,024 solves of 8×8 with 8 right sides, about 1.5 times the inverses; `⌹m` 42 copies on macOS and 133 on Linux: `linalg::Lu::inverse`, 40 and 131, then a read and a write; `(side↑w)⌹m` 25 copies on macOS and 52 on Linux: `linalg::Lu::solve`, 24 and 51, then a read; `((n÷16)↑w)⌹t` 14 copies on macOS: `linalg::lstsq` on the same shape, 13, then a read; `⌹t` 16 copies on macOS: the same tall QR, about 13, then two products with 16 columns and a write; `((n÷4)↑w)⌹tq` 29 copies on macOS: CholeskyQR2 forms two Gram matrices of `tq`'s 24 columns, 288 flops an item each, at about 40 flops in a copy's time, 7.2 each, which Accelerate measured for this Gram product, far above its tall thin Gram rate, whose 4096×16 product spends most of its time on a fixed cost, then solves with a 24×24 triangle, 144 flops an item at the rank-k rate, 4.8, copies `tq`, which holds 6 copies of data, 6, and scans `tq` and multiplies it by `b`, about 3.5; `"qr"•decompose tq` 38 copies on macOS: the same, with a second triangular solve for Q, 4.8, in place of the product with `b`, then writing Q, 6; `"qr"•decompose⍤2 ms` 60 copies: 1,024 QRs of 8×8, each about 2,000 cycles of small-matrix work; `"qr"•decompose m` 56 copies on macOS and 194 on Linux: `linalg::qr`, 54 and 192, then writing Q and R; `"cholesky"•decompose⍤2 sps` 30 copies: 1,024 Cholesky factors of 8×8, each about 1,000 cycles; `"svd"•decompose⍤2 ms` 400 copies: 1,024 iterative SVDs of 8×8, each about 10,000 operations; `"svd"•decompose⁻ f` 13 copies on macOS and 68 on Linux: a 256×256 product, which costs what `m+.×m` costs, 8.8 and 64, and four passes that scale U by `s`, transpose V, convert the product and write it; `["kind":"svd" "rank":8]•decompose m` 22 copies on macOS and 66 on Linux: Halko's range finder, with 18 columns and two power iterations, makes six products of 256×256 by 18 columns, 36 flops an item each, five thin QRs and the SVD of an 18×256 matrix, which on Accelerate cost 5.4 for the three products of `m` by 256×18, at about 20 flops in a copy's time, measured for that shape, between the matrix-vector and rank-k rates, 10.3 for the two of `m`'s transpose at the matrix-vector rate, 1.2 for Qᴴ times `m` at the rank-k rate, and 4.6 for the QRs and the SVD, about 32 flops an item at the matrix-vector rate, while products this thin run at about 3.5 flops in a copy's time on rustymath's own kernels, which cost 66 copies on macOS with `--native`; `"eigen"•decompose⁻ h` 54 copies on macOS and 200 on Linux: `linalg::Lu::inverse`, 40 and 131, three passes for the vectors' condition number, scaling the vectors by the values, then a 256×256 product, 8.8 and 64, and a write; `"eigen"•decompose⍤2 ss` 300 copies: 1,024 symmetric eigendecompositions of 8×8, each about 5,000 operations; `"eigen"•decompose⍤2 ms` 700 copies: 1,024 nonsymmetric eigendecompositions of 8×8, each about 13,000 operations; `⌹zm` 21 copies on macOS and 67 on Linux: a complex 128×128 inverse, 8n³/3 complex multiply-adds, about 341 real ones an item, at the rate `linalg::Lu::inverse` reaches on a 256×256 matrix, about 17 and 5.2 in a copy's time, then a read and a write; `-.×m` 24 copies on macOS and 51 on Linux: the LU in `linalg::Lu::solve`, whose solve costs under a copy; `-.×⍤2 ms` 20 copies: 1,024 LUs of 8×8. In wasm, which rustymath's speed test doesn't time, the expected costs stay the flop counts at about 4 in a copy's time: `⌹m` 128, `(side↑w)⌹m` 42, `((n÷16)↑w)⌹t` 8, `⌹t` 12, `((n÷4)↑w)⌹tq` 95, `"qr"•decompose tq` 130, `"qr"•decompose m` 170, `"svd"•decompose⁻ f` 128, `"eigen"•decompose⁻ h` 128, `⌹zm` 86 and `-.×m` 42. ⌹⍤2 ms -⍝⍝: 60 88 100 instructions linux 172.735 allocated linux 8.483 +⍝⍝: 60 88 100 instructions macos 108.664 linux 172.735 allocated macos 8.171 linux 8.483 ms⌹⍤2 sps -⍝⍝: 90 76 99 instructions linux 206.351 allocated linux 23.371 +⍝⍝: 90 76 99 instructions macos 169.092 linux 206.351 allocated macos 23.214 linux 23.371 ⌹m -⍝⍝: 84 140 210 instructions linux 195.551 allocated linux 58.489 +⍝⍝: 84 140 210 instructions macos 56.032 linux 195.551 allocated macos 19.333 linux 58.489 side↑w ⌹ m -⍝⍝: 50 73 79 instructions linux 84.593 allocated linux 25.047 +⍝⍝: 50 73 79 instructions macos 46.318 linux 84.593 allocated macos 11.672 linux 25.047 ⍝ `((n÷16)↑w)⌹t` takes `linalg::lstsq`'s CholeskyQR2 route on macOS, and Householder on rustymath's own kernels on Linux and in wasm, where `tall_for_qr` gives 16-column matrices to Householder since 23 October 2026. (n÷16 ↑ w)⌹t -⍝⍝: 28 46 53 instructions linux 44.126 allocated linux 19.348 +⍝⍝: 28 46 53 instructions macos 39.225 linux 44.126 allocated macos 11.281 linux 19.348 ⍝ `⌹t` forms U from the tall QR's factors and then multiplies it out. Taking the pseudoinverse as R's pseudoinverse times Q's transpose would save about 10 copies. ⍝⍝⍝ held macos 35.3: it forms U and multiplies it out, as above; no Linux expected cost yet for rustymath's own kernels, which take the wasm expected cost here ⌹t -⍝⍝: 32 100 88 instructions linux 117.338 allocated linux 69.75 +⍝⍝: 32 100 88 instructions macos 66.232 linux 117.338 allocated macos 42.622 linux 69.75 ⍝ On macOS, Accelerate runs most of `((n÷4)↑w)⌹tq`, as it does `"qr"•decompose tq` below, at about half the rates above, so its macOS expected cost is the 53 it measures. (n÷4 ↑ w)⌹tq -⍝⍝: 80 270 300 instructions linux 415.253 allocated linux 223.668 +⍝⍝: 80 270 300 instructions macos 133.807 linux 415.253 allocated macos 55.239 linux 223.668 ⍝ On macOS, Accelerate's Gram products and triangular solves take about four fifths of `"qr"•decompose tq`'s time and our own code under 1% (`profile`, 29 October 2026). They run at about half the rates above, so its macOS expected cost is the 72 it measures. "qr"•decompose tq -⍝⍝: 110 390 410 instructions linux 501.105 allocated linux 322.479 +⍝⍝: 110 390 410 instructions macos 90.22 linux 501.105 allocated macos 99.307 linux 322.479 ⍝ `"qr"•decompose⍤2 ms` measured about 126 on macOS and 225 on CI before 22 October 2026, about twice its expected cost. Each 8×8 cell retires about 24,000 instructions. `leaf_qr`'s reflections take about 40% of them, two `Zip` adapters that the test profile leaves out of line about 12%, and allocations about 6%, most of them basedpl's, which builds a Q and an R array for each cell of the nested result. Since 22 October 2026 a stack's cells factor in one reused `QrWork` and write into the two outputs directly, which cut the case's instructions by 12%. On 23 October 2026 it measures 46 on macOS, and CI's last two runs measured 65 and 68 on Linux and 61 and 68 in wasm. Every budget is twice its expected cost. "qr"•decompose⍤2 ms -⍝⍝: 90 89 77 instructions linux 220.656 allocated linux 21.833 +⍝⍝: 90 89 77 instructions macos 186.167 linux 220.656 allocated macos 21.567 linux 21.833 "qr"•decompose m -⍝⍝: 84 210 300 instructions linux 306.275 allocated linux 152.089 +⍝⍝: 84 210 300 instructions macos 101.284 linux 306.275 allocated macos 71.682 linux 152.089 "cholesky"•decompose⍤2 sps -⍝⍝: 60 51 45 instructions linux 104.193 allocated linux 26.846 +⍝⍝: 60 51 45 instructions macos 97.146 linux 104.193 allocated macos 26.845 linux 26.846 ⍝ `"svd"•decompose⍤2 ms` runs rustymath's own SVD on every platform, because 8×8 lies below rustymath's `GESDD_FROM`. On macOS it measures about 509, against 775 on Accelerate's `?gesdd`, and CI measured 608. "svd"•decompose⍤2 ms -⍝⍝: 600 430 390 instructions linux 914.832 allocated linux 114.274 +⍝⍝: 600 430 390 instructions macos 970.586 linux 914.832 allocated macos 114.273 linux 114.274 ⍝ `"svd"•decompose⁻ f` costs a 256×256 product plus four passes. On CI it measured 90.5 when `m+.×m` measured 79.6. Its expected cost used to take the product from rustymath's `linalg::PlusTimes`, 21 copies on Linux, a figure that basedpl's single-threaded product doesn't reach. "svd"•decompose⁻ f -⍝⍝: 26 85 160 instructions linux 106.351 allocated linux 48.308 +⍝⍝: 26 85 160 instructions macos 35.999 linux 106.351 allocated macos 32.058 linux 48.308 ⍝ `["kind":"svd" "rank":8]•decompose m` in wasm: 82 copies, as CI's x86 V8 measured it on 22 October 2026, at 15.7 ns an item against a copy of 0.192 ns. That's about 2.8 flops in a copy's time, where the group's wasm rate of 4 would give 58. Thin products take 70% of its instructions on rustymath's own kernels, at 70 to 80% of a square product's flops per instruction. CI measured 79 to 92 in seven runs from 22 October 2026. This Mac's V8 takes about the same time, 14.3 ns an item, against a copy 1.6 times faster than CI's, and measures 118 to 121 copies, up to 153 under load. ⍝ CI measured 74.5 to 83.5 in wasm in runs 52 to 62. ["kind":"svd" "rank":8]•decompose m -⍝⍝: 44 84 120 instructions linux 247.441 allocated linux 140.483 +⍝⍝: 44 84 120 instructions macos 124.491 linux 247.441 allocated macos 29.457 linux 140.483 ⍝⍝⍝ held macos 76.5: about a third of its macOS time is our own code, building vectors and rustymath's LU (`profile`, 29 October 2026) "eigen"•decompose⁻ h -⍝⍝: 81 240 390 instructions linux 344.501 allocated linux 130.776 +⍝⍝: 81 240 390 instructions macos 138.5 linux 344.501 allocated macos 75.37 linux 130.776 ⍝ `"eigen"•decompose⍤2 ss` runs rustymath's own Hermitian solver on every platform, because 8×8 lies below rustymath's `HEEVD_FROM`. On macOS it measures about 298, against 392 on Accelerate's `?heevd`. ⍝ CI measured 241 to 252 on Linux in runs 56 to 61, and 442 in run 62, whose host was busy. "eigen"•decompose⍤2 ss -⍝⍝: 450 270 250 instructions linux 557.885 allocated linux 48.393 +⍝⍝: 450 270 250 instructions macos 583.994 linux 557.885 allocated macos 48.392 linux 48.393 "eigen"•decompose⍤2 ms -⍝⍝: 910 670 720 instructions linux 1422.279 allocated linux 130.784 +⍝⍝: 910 670 720 instructions macos 1373.757 linux 1422.279 allocated macos 110.783 linux 130.784 ⍝ `⌹zm` measures 28.4 on macOS and 94 on CI, about 1.35 times its expected cost on both. Its complex inverse costs 3.4 to 4 times a real one of the same size, as its four real multiply-adds for each complex one predict, and less than a solve with as many right-hand sides. A 128×128 inverse runs at about 0.7 of the rate a 256×256 one reaches, on Accelerate and on rustymath's own kernels alike. ⌹zm -⍝⍝: 42 96 130 instructions linux 172.24 allocated linux 56.833 +⍝⍝: 42 96 130 instructions macos 38.25 linux 172.24 allocated macos 9.396 linux 56.833 -.×m -⍝⍝: 48 72 76 instructions linux 80.984 allocated linux 24.77 +⍝⍝: 48 72 76 instructions macos 39.151 linux 80.984 allocated macos 11.395 linux 24.77 -.×⍤2 ms -⍝⍝: 40 48 46 instructions linux 82.107 allocated linux 0.522 +⍝⍝: 40 48 46 instructions macos 77.051 linux 82.107 allocated macos 0.366 linux 0.522 ⍝⍝ exact •prefs ["mathish":$f] ⍝ Plain IEEE comparisons and `floor`, with nothing to scale by a tolerance. ⍝ Expected: `⌊v` 1.5 copies: a floor, a conversion to a 64-bit integer, a range check, and a select and a success flag for an item that doesn't fit, about 6 vector operations an item, with an 8-byte write; `v0.5` 0.6 copies natively: one read of memory, about 0.5, with the call over each block, folded in the cache, overlapping it; 0.9 in wasm, for one call; `+⌿img×2` 0.65 copies natively: as `+/flat>0.5`, with the column totals; 1 copy in wasm. +/flat>0.5 -⍝⍝: 1.2 1.1 2.1 instructions linux 2.897 allocated linux 1.083 +⍝⍝: 1.2 1.1 2.1 instructions macos 3.232 linux 2.897 allocated macos 1.083 linux 1.083 ⍝ On 30 October 2026 `+⌿img` cost 0.83 on macOS. The one group of `+⌿img×2`, `×+⌿`, fuses and compiles, and stores nothing. ⍝⍝⍝ held macos 1.85: the cause isn't known yet +⌿img×2 -⍝⍝: 1.3 1.4 1.9 instructions linux 2.341 allocated linux 8.186 +⍝⍝: 1.3 1.4 1.9 instructions macos 4.332 linux 2.341 allocated macos 8.186 linux 8.186 ⍝ `+⌿img` folds 2000 rows of 4,500 items read from memory. Each block of 8,192 column totals steps by four rows at a time, so each row's items are read in the order they lie (`reduce::columns_by`). Strips of 32 columns, each folded down every row, took 3.2 times as long (27 October 2026). ⍝ Expected: 0.6 copies: one read of memory, about 0.5, and the steps of the column totals in the cache, about 0.1. +⌿img -⍝⍝: 1.2 1.1 1 instructions linux 1.285 allocated linux 0.16 +⍝⍝: 1.2 1.1 1 instructions macos 2.414 linux 1.285 allocated macos 0.16 linux 0.16 ⍝ `+/img` folds each pixel's 3 channels. Each length of lane up to 15 items has a loop of its own. The reshape shares `flat`'s storage. ⍝ Expected: `+/img` 0.67 copies: one read, and a third as many items written; `+/[1125000 8]⍴flat` 0.56 copies: one read, and an eighth as many items written. +/img -⍝⍝: 1.4 1.6 1.9 instructions linux 1.949 allocated linux 2.764 +⍝⍝: 1.4 1.6 1.9 instructions macos 2.21 linux 1.949 allocated macos 2.764 linux 2.764 +/[n÷8;8]⍴flat -⍝⍝: 1.2 1.2 1.3 instructions linux 2.626 allocated linux 1.109 +⍝⍝: 1.2 1.2 1.3 instructions macos 2.957 linux 2.626 allocated macos 1.109 linux 1.109 ⍝ The comparison's Booleans count into a byte for each pixel. ⍝ Expected: 0.6 copies natively: one read of memory, about 0.5, with the count into a byte for each pixel overlapping it; 0.9 in wasm, for one call. +/img>0.5 -⍝⍝: 1.2 1.3 2.5 instructions linux 4.243 allocated linux 4.124 +⍝⍝: 1.2 1.3 2.5 instructions macos 4.107 linux 4.243 allocated macos 4.124 linux 4.124 ⍝ A comparison that a later call reads runs in that call's program, writing 1 and 0 as floats rather than storing Booleans. ⍝ Expected: 0.7 copies natively: one read of `flat`, which both calls share, about 0.5, with their passes over each block overlapping it; 1.3 in wasm, for two calls. ⍝ On 30 October 2026 `+/flat>0.5` cost 0.85 on macOS. The one group of `+/flat×flat>0.5`, `>×+/`, fuses and stores nothing, but doesn't compile, as `+/flat>0.5`'s doesn't. +/flat×flat>0.5 -⍝⍝: 1.4 1.8 2.3 instructions linux 5.401 allocated linux 9.767 +⍝⍝: 1.4 1.8 2.3 instructions macos 9.088 linux 5.401 allocated macos 9.767 linux 9.767 ⍝ A square and a halving in one compiled program. `²` lowers to multiplications, and `⊽⁻`'s division by 2 lowers because zero divided by zero can't arise with a nonzero constant divisor. Either one left to rustymath's kernel splits the stage, which then runs uncompiled at about 2.6 times the cost (30 October 2026, 4M items). ⍝ Expected: 0.67 copies natively: one read of memory, about 0.5, and three compiled calls over each block, folded in the cache. In wasm, which compiles nothing, 1.3 copies, as for `+/flat×flat>0.5`. +/⊽⁻(flat-0.5)² -⍝⍝: 1.4 2.7 2.8 instructions linux 5.421 allocated linux 24.131 +⍝⍝: 1.4 2.7 2.8 instructions macos 7.209 linux 5.421 allocated macos 24.131 linux 24.131 ⍝ A select by a comparison that a program would run reads a sample of the comparison's items to choose between a select and a gather. A sparse mask gathers the few items it picks. A dense one runs as one fused select. ⍝ Expected: `-@[0.99<]flat` 2.3 copies: a fused comparison reading `flat`, about 0.9, then a copy of `flat` with the picked items negated, about 1.4 with its pass over each block; `*@[0.3<]flat` 3.8 copies: one pass over memory, a comparison and a blend over each block, and exponentials of every item, whatever share picks `*`, about 2 copies. ⍝⍝⍝ allows forced "-@[0.99↣<]": a sparse mask computes its comparison to find the items it picks, then gathers them -@[0.99<]flat -⍝⍝: 4.6 6.2 5.6 instructions linux 6.485 allocated linux 9.424 +⍝⍝: 4.6 6.2 5.6 instructions macos 8.838 linux 6.485 allocated macos 9.424 linux 9.424 ⍝ `*@[0.3<]flat` replaced `*@[0.5<]flat` on 21 October 2026. A mask that picks half the items sits within the sampling noise of the select's crossover, about 47% for `*`, so that case gathered in about one run in six. ⍝ Natively its exponentials cost about 2 copies of memory on CI's runner and about 3.8 on this Mac, whose memory is fast beside its cores, so its macOS expected cost is 5.6. In wasm they cost twice that, because wasm's SIMD has no fused multiply-add, and it expects 5.8. *@[0.3<]flat -⍝⍝: 12 6.7 16 instructions linux 15.572 allocated linux 17.085 +⍝⍝: 12 6.7 16 instructions macos 29.937 linux 15.572 allocated macos 17.085 linux 17.085 ⍝ Here the calls hold three blocks of results at once, so the block stage runs smaller blocks, whose buffers stay in the cache. ⍝ Expected: 1.25 copies natively; 3.8 in wasm, for seven calls. (flat-0.5 × flat+0.5) + flat+1 × flat-1 -⍝⍝: 2.5 3.5 5.8 instructions linux 8.703 allocated linux 56.173 +⍝⍝: 2.5 3.5 5.8 instructions macos 13.27 linux 8.703 allocated macos 56.173 linux 56.173 diff --git a/tests/speed/maps.bpl b/tests/speed/maps.bpl index b7d1e6d6..084ca16c 100644 --- a/tests/speed/maps.bpl +++ b/tests/speed/maps.bpl @@ -64,10 +64,10 @@ Y←¿[(4096 size 8) 8]⍴0 ⍝ Expected: `{⍵×⍵+1}¨v` 2.5 copies: as `v×v+1`; `v×v+1` 2.5 copies: two passes below the fusion threshold, one copy then two reads and a write; `{+/,⍵}⌺[3 3] sg` 2 copies: nine reads for each of 16,384 windows, and their sums; `+/+/¯3 ¯3↕sg` 0.75 copies: a padded copy and two sliding passes over 16,384 items. ⍝⍝⍝ route batching {⍵×⍵+1}¨v -⍝⍝: 5 6.9 6.8 instructions linux 15.191 allocated linux 16.826 +⍝⍝: 5 6.9 6.8 instructions macos 21.74 linux 15.191 allocated macos 16.826 linux 16.826 v×v+1 -⍝⍝: 5 6.3 6.2 instructions linux 10.7 allocated linux 16.621 +⍝⍝: 5 6.3 6.2 instructions macos 16.566 linux 10.7 allocated macos 16.621 linux 16.621 ⍝ Each over a nested array whose 10,000 items are vectors of 8 small integers. Each passes its operand each item, as Rank passes each row of the matrix that holds them. Where every item is a compact array of one layout and dtype, a batched map stacks the items into one array and runs as Rank does. ⍝ Expected: `{⍵+1ₓ}¨nest` and `{+/⍵}¨nest` 2.5 copies: as `{⍵+1ₓ}⍤1⁝irow` and `+/⍤1 irow`, which cost 1.88 and 2.02 on 30 October 2026, and a copy of the 80,000 one-byte items into one array. @@ -75,116 +75,116 @@ v×v+1 ⍝⍝⍝ held macos 62.9: stacking and assembling the items; Linux and wasm hold estimates of 1.7 and 1.4 times the macOS cost ⍝⍝⍝ route batching {⍵+1ₓ}¨nest -⍝⍝: 5 760 720 instructions linux 261.597 allocated linux 31.784 +⍝⍝: 5 760 720 instructions macos 260.262 linux 261.597 allocated macos 31.784 linux 31.784 ⍝⍝⍝ held macos 24.7: stacking and assembling the items; Linux and wasm hold estimates of 1.7 and 1.4 times the macOS cost ⍝⍝⍝ route batching {+/⍵}¨nest -⍝⍝: 5 570 570 instructions linux 97.45 allocated linux 3.459 +⍝⍝: 5 570 570 instructions macos 95.507 linux 97.45 allocated macos 3.459 linux 3.459 ⍝ Each over a nested array whose 4,096 items are matrices of 2 rows and 1 to 15 columns, ragged cells in mixed storage, with a constant vector added along each item's rows. The batched try declines on ragged cells, which aren't compact (`batch::mapped`: "an argument that isn't compact"). The dfn then runs once for each of the 4,096 cells, at about 18,600 instructions a call. ⍝ Expected: 10 copies, from a batched map that gathers the cells into one leaf and runs the pervasive call once on it, with each item of the constant repeated over its cell's row through one `#`. That route is Later work. ⍝⍝⍝ held linux 627: the dfn runs once for each ragged cell until batching takes ragged cells, which is later work ⍝⍝⍝ route batching {⍵+10 20}¨rag -⍝⍝: 20 420 400 instructions linux 2593.752 allocated linux 191.807 +⍝⍝: 20 420 400 instructions macos 3674.203 linux 2593.752 allocated macos 191.807 linux 191.807 ⍝⍝⍝ held macos 5.37: open.md lists the stencils among the speed audit's outliers that compiling didn't fix ⍝⍝⍝ gap forced "{+/,⍵}⌺[3 3]": a dfn operand's fold over windows should slide over the source {+/,⍵}⌺[3 3] sg -⍝⍝: 4 12 15 instructions linux 50.681 allocated linux 25.295 +⍝⍝: 4 12 15 instructions macos 62.599 linux 50.681 allocated macos 25.295 linux 25.295 ⍝⍝⍝ held macos 4.19: open.md lists wide and 2-D window folds among the speed audit's outliers +/+/¯3 ¯3↕sg -⍝⍝: 1.5 7.3 7.8 instructions linux 51.714 allocated linux 31.885 +⍝⍝: 1.5 7.3 7.8 instructions macos 62.781 linux 51.714 allocated macos 31.885 linux 31.885 ⍝ The Game of Life on a 128 by 128 Boolean grid, as a stencil and with padded windows. ⍝ Expected: `life⌺[3 3] g` 1.5 copies: a few passes over nine 1-byte reads for each of 16,384 windows; `life⌺["size":[3 3] "edge":"wrap"] g` 1.5 copies: as the stencil with fill; `lifew g` 0.75 copies: sliding sums and comparisons over 16,384 items. ⍝⍝⍝ held macos 4.54: open.md lists the stencils among the speed audit's outliers that compiling didn't fix life⌺[3 3] g -⍝⍝: 3 15 13 instructions linux 82.039 allocated linux 11.215 +⍝⍝: 3 15 13 instructions macos 105.821 linux 82.039 allocated macos 11.215 linux 11.215 ⍝⍝⍝ held macos 4.8: open.md lists the stencils among the speed audit's outliers that compiling didn't fix life⌺["size":[3 3] "edge":"wrap"] g -⍝⍝: 3 15 15 instructions linux 87.687 allocated linux 11.85 +⍝⍝: 3 15 15 instructions macos 114.052 linux 87.687 allocated macos 11.842 linux 11.85 ⍝⍝⍝ held macos 4.15: open.md lists wide and 2-D window folds among the speed audit's outliers lifew g -⍝⍝: 1.5 9.7 8.2 instructions linux 69.371 allocated linux 17.089 +⍝⍝: 1.5 9.7 8.2 instructions macos 85.208 linux 69.371 allocated macos 17.089 linux 17.089 ⍝ A guard on each cell's value runs as `⍚` runs a selector for each cell. Each side runs once, on the cells that take it. In the first case every cell takes the second body. In the second, about half the cells take each body, as in the `⍚` beside it. ⍝ Expected: `{⍵<0?0;⍵}¨v` 1.5 copies: one tolerant comparison, then the whole argument as the result; `{⍵<0.5?0;√⍵}¨v` 6 copies: a tolerant comparison, a split and a join of each side, and square roots of half the items; `(0.5≤)⍚[0 √] v` 4 copies: a tolerant comparison, square roots of every item, and a blend. ⍝⍝⍝ allows forced "{⍵<0?0;⍵}": a guard reads its predicate's items to split the cells {⍵<0?0;⍵}¨v -⍝⍝: 3 6.2 9.5 instructions linux 18.668 allocated linux 6.696 +⍝⍝: 3 6.2 9.5 instructions macos 24.21 linux 18.668 allocated macos 6.696 linux 6.696 ⍝⍝⍝ allows forced "{⍵<0.5?0;√⍵}": a guard reads its predicate's items to split the cells {⍵<0.5?0;√⍵}¨v -⍝⍝: 12 23 25 instructions linux 50.729 allocated linux 47.531 +⍝⍝: 12 23 25 instructions macos 64.437 linux 50.729 allocated macos 47.531 linux 47.531 ⍝⍝⍝ held macos 7.66: open.md lists the masked selects among the speed audit's outliers (0.5≤)⍚[0 √] v -⍝⍝: 8 18 21 instructions linux 39.678 allocated linux 39.126 +⍝⍝: 8 18 21 instructions macos 50.11 linux 39.678 allocated macos 39.126 linux 39.126 ⍝ The gradient of a loss mapped over the rows of `X`, whose parameter every row shares, then per-example gradients, then a Hessian–vector product through the mapped loss. Each sits beside its form with `+.×`, or its closed form. ⍝ Expected: `loss∂ thz` 4 copies: the primal over 32,768 items, about 1 copy, with sines of 4,096 items at 7 copies for each 65,536, about 0.45, and a transpose of about 3; `lossw∂ thz` 3 copies: a product over 32,768 items, `1○` over 4,096 items, about 0.45, and its transpose; `{f∂ ⍵}⍤1 X` 9 copies: sines and cosines of 32,768 items, half of `n`, at 7 copies each as `1○v` counts them, then the squares, the row sums and each item's product of sine and cosine, about 2; `2×(1○X)×2○X` 7.5 copies: sines and cosines of 32,768 items, half of `n`, at 7 copies each as `1○v` counts them, and two products; `hv∂ thz` 8 copies: about twice the gradient; `hvw∂ thz` 6 copies: about twice the gradient. ⍝ On 27 October 2026 `loss∂ thz` measured 6.02 on macOS. Its wasm budget is twice its native budget, as `1○v`'s are. On 22 October 2026 CI measured 8.09 in wasm. loss∂ thz -⍝⍝: 8 15 18 instructions linux 46.722 allocated linux 19.423 +⍝⍝: 8 15 18 instructions macos 72.231 linux 46.722 allocated macos 19.423 linux 19.423 ⍝ On 27 October 2026 `lossw∂ thz` measured 5.22 on macOS, close to twice its expected cost. Each of its two products of the 4096×8 `X` and a vector costs about 1.6 copies, where reading `X` costs 0.25: `Xb+.×thz` measured 1.59. On 22 October 2026 CI measured 8.39 on Linux and 7.21 in wasm. lossw∂ thz -⍝⍝: 6 7.5 11 instructions linux 46.542 allocated linux 19.423 +⍝⍝: 6 7.5 11 instructions macos 71.702 linux 46.542 allocated macos 19.423 linux 19.423 ⍝ On 27 October 2026 `{f∂ ⍵}⍤1 X` measured 12.38 on macOS. Its budgets are twice the expected cost: 18 on macOS and Linux, and 36 in wasm, where `1○v`'s budgets in arrays.bpl give sine and cosine twice their native cost. On 22 October 2026 CI measured 11.6 on Linux and 20.6 in wasm. {f∂ ⍵}⍤1 X -⍝⍝: 18 23 53 instructions linux 86.376 allocated linux 31.755 +⍝⍝: 18 23 53 instructions macos 124.486 linux 86.376 allocated macos 31.765 linux 31.755 ⍝ `•explain` shows no fused stage: `1○X` and `2○X` each run over all of `X`, one after the other. On 27 October 2026 it measured 8.24 on macOS. The budgets are twice the expected cost: 15 on macOS and Linux, and 30 in wasm, where `1○v`'s budgets in arrays.bpl give sine and cosine twice their native cost. On 22 October 2026 CI measured 16.1 in wasm. 2× 1○X × 2○X -⍝⍝: 15 13 44 instructions linux 31.612 allocated linux 17.071 +⍝⍝: 15 13 44 instructions macos 49.062 linux 31.612 allocated macos 17.071 linux 17.071 hv∂ thz -⍝⍝: 16 29 36 instructions linux 81.529 allocated linux 21.407 +⍝⍝: 16 29 36 instructions macos 123.006 linux 81.529 allocated macos 21.407 linux 21.407 hvw∂ thz -⍝⍝: 12 11 16 instructions linux 81.311 allocated linux 21.407 +⍝⍝: 12 11 16 instructions macos 123.006 linux 81.311 allocated macos 21.407 linux 21.407 ⍝ An attached rule inside Each, beside the same rule on the whole array. ⍝ Expected: `{+/sq¨⍵}∂ v` 3 copies: as `{+/sq ⍵}∂ v`; `{+/sq ⍵}∂ v` 3 copies: the primal and the attached rule over every item. ⍝⍝⍝ gap stopped "a read of a traced value": Each should trace the attached derivative rule over the whole array {+/sq¨⍵}∂ v -⍝⍝: 6 22 19 instructions linux 8.991 allocated linux 24.572 +⍝⍝: 6 22 19 instructions macos 13.867 linux 8.991 allocated macos 24.572 linux 24.572 {+/sq ⍵}∂ v -⍝⍝: 6 5.4 5.7 instructions linux 8.991 allocated linux 24.572 +⍝⍝: 6 5.4 5.7 instructions macos 13.865 linux 8.991 allocated macos 24.572 linux 24.572 ⍝ 1000 Each calls on one atom, in turn. A map over one cell of atoms calls its operand without trying a batched call. ⍝ Expected: 27 copies: 1,000 calls of about 200 ns each. ⍝ Linux: 46 copies, because scalar work costs about 1.7 times as many copies on CI's x86 runner, as `v⍳w`'s comment in `arrays.bpl` says. {⍵+1}¨⍣map_steps,0 -⍝⍝: 54 63 60 instructions linux 142.885 allocated linux 3.191 +⍝⍝: 54 63 60 instructions macos 166.653 linux 142.885 allocated macos 3.191 linux 3.191 ⍝ 16 steps of an elementwise body. After its first step, a loop runs such a body as ordinary code, so each step's add waits and all 16 run as one block stage, as the chain written out does (`v+v+v` and so on). ⍝ Expected: 8.4 copies: one interpreted block stage of 16 adds over `v`, about half a copy for each add. On this Mac on 1 November 2026 the chain written out measured 8.39. {⍵+v}⍣16 v -⍝⍝: 17 66 59 instructions linux 75.474 allocated linux 131.822 +⍝⍝: 17 66 59 instructions macos 107.032 linux 75.474 allocated macos 131.822 linux 131.822 ⍝ 100 steps of gradient descent on `lossw`. Each step after the first replays the recording of `descend`'s body, the gradient and the update, as one program. ⍝ Expected: 300 copies: 100 gradients at `lossw∂ thz`'s 3 copies each. On this Mac on 1 November 2026 it measured 217, 2.17 a step, against 2.40 for one call of `lossw∂ thz`. (descend⍣descent_steps)thz -⍝⍝: 450 600 710 instructions linux 831.121 allocated linux 316.357 +⍝⍝: 450 600 710 instructions macos 1206.832 linux 831.121 allocated macos 316.357 linux 316.357 ⍝ The gradient of a sum through a loop with a predicate, which halves `v` until every item is under 1e¯3: 10 steps, a number the data decide. `∂` gives two loop nodes: the forward loop, and one that reruns it from its first carry, keeping the carries, then folds the body's VJP back over them. ⍝ Expected: 26 copies: the rerun's 10 steps at about 1.5 copies each (each halving writes `v` once, and the predicate's `⌈/|⍺` reads it once), then the backward pass's 10 halvings of the cotangent at about 1 copy each. A replay runs no forward loop of its own: only the loss's value reads it, and the gradient doesn't. On this Mac on 1 November 2026 it measured 28.5, against 30.4 for the gradient through 10 counted steps, `{+/{⍵×0.5}⍣10⁝⍵}∂ v`, whose forward loop keeps a stack. {+/{⍵×0.5}⍣{1e¯3>⌈/|⍺}⁝⍵}∂ v -⍝⍝: 52 120 230 instructions linux 280.654 allocated linux 270.11 +⍝⍝: 52 120 230 instructions macos 373.167 linux 280.654 allocated macos 270.116 linux 270.11 ⍝ A map of 400 scalars against `v`. Repeated for each cell, `v` would hold 26 million items, past the memory limit of a batched run. The map then calls its operand once for each cell, holding one copy of `v` at a time. ⍝ Expected: 1000 copies: 400 cells, each about 2.5 copies in chunks. ⍝ CI measured 812 to 852 on Linux in most runs from 56 to 62, and up to 1,232 in runs whose hosts were busy. {+/ v-⍵ × v-⍵}¨cs -⍝⍝: 1300 900 1700 instructions linux 807.877 allocated linux 26.198 +⍝⍝: 1300 900 1700 instructions macos 1368.041 linux 807.877 allocated macos 26.217 linux 26.198 ⍝ Four cells of 256 by 256 floats. The batched run takes the sines of all four cells as one call, and `⍳` searches each cell's own table on slices of the batch (`searched_cells`). ⍝ Expected: 30 copies: sines of four cells of `n` items at `1○v`'s 7 copies each, 28, and one read of each cell's table for the search, 2. In wasm 58: sines at twice their native cost, as `1○v` expects. The expected cost was 18 until 31 October 2026, when cf2_ measured the sines at 73% of the case's instructions; 18 had counted about 4 copies a cell for them. @@ -193,94 +193,94 @@ hvw∂ thz r←1○⍵ ,r ⍳ 0.5 }⍤2 big -⍝⍝: 60 130 190 instructions linux 72.719 allocated linux 32.932 +⍝⍝: 60 130 190 instructions macos 121.528 linux 72.719 allocated macos 32.932 linux 32.932 ⍝ Rank of a dfn on each row of `X` makes one batched call. The subtraction pads each row's mean, `+/÷≢ ⍵`, over the row. ⍝ Expected: 1 copy each: row sums of 32,768 items and one padded subtraction. ⍝⍝⍝ held macos 1.85: as `X - +/X ÷ 8`, the subtraction broadcasts each row's mean over its 8 items, one `pairs` call per row (open.md, row means, centring) {⍵ - +/÷≢ ⍵}⍤1 X -⍝⍝: 2 5 5.5 instructions linux 27.953 allocated linux 11.053 +⍝⍝: 2 5 5.5 instructions macos 36.401 linux 27.953 allocated macos 11.053 linux 11.053 X - +/X ÷ 8 -⍝⍝: 2 4.6 4.9 instructions linux 21.649 allocated linux 10.635 +⍝⍝: 2 4.6 4.9 instructions macos 28.558 linux 21.649 allocated macos 10.635 linux 10.635 ⍝ An outer product of a dfn makes one batched call, with both arguments laid over the result's frame. ⍝ Expected: 1 copy: the operand on views laid over the frame, and one write of `n` items. ⍝⍝⍝ held macos 4.54: both arguments are laid over the 65,536-item frame, so `⍵+1` runs at every frame position (open.md, dfn operands of scans, folds, outer products); stage 6 keeps each framed value to its own frame axes (main's decision 23) ⍳outer_side {⍺×⍵+1}⊗ ⍳outer_side -⍝⍝: 2 11 9.1 instructions linux 30.648 allocated linux 35.486 +⍝⍝: 2 11 9.1 instructions macos 38.896 linux 30.648 allocated macos 35.486 linux 35.486 ⍝ Expected: 0.73 copies: 0.5 for writing 8 bytes an item, and one node, about 0.23 copies of 65,536 floats (`fusion::MAC_NODE_NS`, 1,800 ns), because the lone outer product waits under the cost rule and nothing fuses with it (decision 13), as for its twin `⍳√n ×⊗ ⍳√n` in arrays.bpl. In the browser the call runs at once, because the cost rule's limit there is about 830 KB, above the call's 512 KB. The expected cost was 0.5 until 31 October 2026, when cf2_ found that the two cases time alike (1.31 and 1.27 copies under load). ⍳outer_side ×⊗ 1+⍳outer_side -⍝⍝: 1.5 1.5 1.9 instructions linux 18.965 allocated linux 26.328 +⍝⍝: 1.5 1.5 1.9 instructions macos 27.093 linux 18.965 allocated macos 26.328 linux 26.328 -⍝ A roll in a batched map draws once, on the whole frame. Each cell's item reads the word at its place in one stream that the map's cells share, the word that the cell reads when drawn alone (`rustymath::random::Items::cells`), so the map costs what one roll on its frame costs. -⍝ Expected: 28 copies each: about 13 for ChaCha8's words, about 11 for `rand`'s mapping of each word into the range, one draw at a time, and about 4 for the addition and the result. They measure about 31 on macOS. `rustymath::random::roll` draws every word first, then maps the words into their ranges by Lemire's method in a separate pass, and the bounds convert to integers in one fold. Drawing each word through `next_u64` still costs about 15 instructions. `rand`'s `fill` draws the same words, but copies each 4-byte word through a call to `memmove`, which costs several times as much. -⍝ CI measured 23.6 and 23.5 on Linux on 22 October 2026, and their Linux budgets follow their expected cost. -⍝⍝⍝ route batching +⍝ Each executes this body and its draw once per item. Draws are effects and do not batch; the bulk spelling below draws one whole array. +⍝⍝⍝ allows itemwise "batch cells": random draws run separately for each item {⍵+¿10}¨v -⍝⍝: 56 2300 2000 instructions linux 2226.287 allocated linux 72.614 +⍝⍝: 56 2300 2000 instructions macos 2331.689 linux 2226.287 allocated macos 72.614 linux 72.614 +⍝ Expected: 28 copies for the bulk form: about 13 for ChaCha8's words, about 11 for `rand`'s mapping of each word into the range, one draw at a time, and about 4 for the addition and the result. The bulk form measured about 31 on macOS. `rustymath::random::roll` draws every word first, then maps the words into their ranges by Lemire's method in a separate pass, and the bounds convert to integers in one fold. Drawing each word through `next_u64` still costs about 15 instructions. `rand`'s `fill` draws the same words, but copies each 4-byte word through a call to `memmove`, which costs several times as much. +⍝ Older batched-form measurements were 23.5–23.6 copies on CI's Linux on 22 October 2026; the bulk form's Linux budget follows its expected cost. v+¿n⍴10 -⍝⍝: 56 48 90 instructions linux 62.433 allocated linux 40.54 +⍝⍝: 56 48 90 instructions macos 69.361 linux 62.433 allocated macos 40.54 linux 40.54 ⍝ The join rule runs a join of each row with itself as one call on `X`. ⍝ Expected: 1 copy each: reads 32,768 items twice and writes 65,536. ⍝ `{⍵,⍵}⍤1 X` measures 0.9 on macOS since a join writes each row whole (`gather::interleave`, 22 October 2026), down from 2.4 to 2.7. In wasm it measured 0.91 to 1.02 in seven CI runs from 22 October 2026. CI measured 0.94 on Linux on 22 October 2026. {⍵,⍵}⍤1 X -⍝⍝: 2 2.6 2.7 instructions linux 10.203 allocated linux 8.893 +⍝⍝: 2 2.6 2.7 instructions macos 15.139 linux 10.203 allocated macos 8.893 linux 8.893 ⍝ `X,X` measures 0.85 on macOS since a join writes each row whole, a lane from each argument in turn, straight into the result (`gather::interleave`, 22 October 2026), down from 2.3 to 2.6. In wasm it measured 0.67 to 0.74 in seven CI runs from 22 October 2026. CI measured 0.71 on Linux on 22 October 2026. X,X -⍝⍝: 2 2.1 2 instructions linux 5.541 allocated linux 8.482 +⍝⍝: 2 2.1 2 instructions macos 9.393 linux 5.541 allocated macos 8.482 linux 8.482 ⍝ A matrix product of each 4 by 4 matrix in a stack with itself runs as one call of the stacked kernel. ⍝ Expected: 3 copies each: one stacked call of 8 flops an item, and the stack's overhead. ⍝ `{⍵+.×⍵}⍤2 st` cost 15.6 on macOS before 22 October 2026. It now runs as `+.×⍤2⍨ st` does. The comment on `+.×⍤2⍨ st` below names their shared gap. ⍝⍝⍝ held macos 4.48: the call for each cell and the zeroing, as `+.×⍤2⍨ st`'s comment says {⍵+.×⍵}⍤2 st -⍝⍝: 6 11 13 instructions linux 50.941 allocated linux 12.512 +⍝⍝: 6 11 13 instructions macos 59.043 linux 50.941 allocated macos 12.512 linux 12.512 ⍝ `+.×⍤2⍨ st` cost 15.3 on macOS before 22 October 2026. That day `direct` began folding four results of a row side by side in registers without checking each index, `Frames` stopped listing the cells of an argument whose frame is the result's, and the call for each cell inlined `checked_into` and `inner_into`. Each 4×4 cell now costs about 370 instructions: about 250 in `direct` for its 64 multiply-adds, about 70 in the call that locates and checks the cell, and about 25 in zeroing the output before the cells write it. The call for each cell and the zeroing, about 95 of those 370, are the gap between its cost and its expected 3. Both cases' costs also depend on the process: on 30 October 2026 they cost 4.7 to 5.5 in some processes and 7.0 to 7.6 in others, on builds from before and after that day's changes. A speed test retries a case in the same process, so a slow process fails every retry. The cause isn't placed. ⍝⍝⍝ held macos 4.43: the call for each cell and the zeroing +.×⍤2⍨ st -⍝⍝: 6 9.6 13 instructions linux 51.975 allocated linux 12.719 +⍝⍝: 6 9.6 13 instructions macos 63.727 linux 51.975 allocated macos 12.719 linux 12.719 ⍝ A fold or scan by a dfn along the rows of `X` runs as one batched map over the rows. Each stage is one call, on every row at once. Each sits beside its primitive form. ⍝ Expected: `{⍺+⍵}/X` 1.5 copies: seven batched calls over 4,096 rows, about a microsecond each; `+/X` 0.25 copies: reads 32,768 items; `{⍺+0.9×⍵}\X` 2 copies: seven batched calls over 4,096 rows, and the scan's writes; `+\X` 0.75 copies: reads and writes 32,768 items, with lanes of 8 waiting on additions. ⍝⍝⍝ held macos 3.71: each batched step first copies a column of `X`, read at stride 8, out contiguously (open.md, dfn operands of scans, folds, outer products); stage 6's loop nodes {⍺+⍵}/X -⍝⍝: 3 4.6 3.9 instructions linux 8.688 allocated linux 1.502 +⍝⍝: 3 4.6 3.9 instructions macos 13.356 linux 8.688 allocated macos 1.502 linux 1.502 +/X -⍝⍝: 0.5 0.83 0.78 instructions linux 8.837 allocated linux 1.467 +⍝⍝: 0.5 0.83 0.78 instructions macos 13.155 linux 8.837 allocated macos 1.467 linux 1.467 ⍝⍝⍝ held macos 5.92: as `{⍺+⍵}/X`, plus a `memmove` that places each step's column in the result (open.md, dfn operands of scans, folds, outer products); stage 6's loop nodes {⍺+0.9×⍵}\X -⍝⍝: 4 9.2 8.7 instructions linux 92.935 allocated linux 29.062 +⍝⍝: 4 9.2 8.7 instructions macos 115.94 linux 92.935 allocated macos 29.062 linux 29.062 +\X -⍝⍝: 1.5 1.6 1.1 instructions linux 6.455 allocated linux 4.311 +⍝⍝: 1.5 1.6 1.1 instructions macos 9.667 linux 6.455 allocated macos 4.311 linux 4.311 ⍝ A dfn under `⍠` along the first axis of `X`, beside the primitive form. ⍝ Expected: `{+/⍵}⍠0 X` 0.5 copies: as `+/⍠0 X`, which reads 32,768 items and measured 0.32 on 30 October 2026. ⍝⍝⍝ held macos 1.91: the dfn's `+/` folds the transposed view of `X` in place along strided lanes, where `+/⍠0` runs column totals (open.md, dfn operand under `⍠`); Linux and wasm hold the macOS cost as an estimate {+/⍵}⍠0 X -⍝⍝: 1 3.5 2.8 instructions linux 19.296 allocated linux 5.872 +⍝⍝: 1 3.5 2.8 instructions macos 28.004 linux 19.296 allocated macos 5.872 linux 5.872 ⍝ Row-wise dot products. `+.×` under Rank pairs each pair of rows' items in one call and folds along them, as `+/X×Y` does. ⍝ Expected: `X+.×⍤1Y` 1 copy: as `+/X×Y`, two passes below the size at which fusion waits (`fusion::waits`). The product reads 65,536 items and writes 32,768, then the fold reads them. On 30 October 2026 it measured 1.71 on macOS, and `+/X×Y` 1.5. X+.×⍤1Y -⍝⍝: 2 170 190 instructions linux 15.914 allocated linux 6.06 +⍝⍝: 2 170 190 instructions macos 23.297 linux 15.914 allocated macos 6.06 linux 6.06 ⍝ An inner product with a dfn `g` calls `g` once, on every triple of a row, a position along the contraction and a column. `f` then folds along the contraction. ⍝ Expected: `(⍉X) +.{⍺×⍵} X` 3 copies: the operand on 262,144 triples, about 2 copies, and a fold along the contraction; `(⍉X)+.×X` 1.5 copies: on Linux and in wasm `⍉X` computes at once below the size at which fusion waits there (`fusion::waits`) and writes a copy of `X`, about 0.5 copies, then the product reads `X` and the copy; on macOS `⍉X` waits and the product reads it through its map, and its 524,288 flops are few. ⍉X +.{⍺×⍵} X -⍝⍝: 6 12 12 instructions linux 31.137 allocated linux 17.912 +⍝⍝: 6 12 12 instructions macos 35.131 linux 31.137 allocated macos 7.912 linux 17.912 ⍝ The product on a stored transpose costs about 1 copy. `⍉X` costs about 2.9 copies of its own 32,768 items on macOS, because its eight result rows lie exactly 32 KB apart and writing them in turn stalls (22 October 2026). On 31 October 2026 the case measured 1.11 to 1.16 on macOS, within its rule budget. ⍉X +.× X -⍝⍝: 3 8 7.4 instructions linux 32.789 allocated linux 18.021 +⍝⍝: 3 8 7.4 instructions macos 36.625 linux 32.789 allocated macos 8.021 linux 18.021 ⍝ A small convolutional layer, written for one image and mapped over a batch of 64 images of 32 by 32 pixels and 3 channels: a 3 by 3 convolution to 32 channels through windows and a product, a bias and ReLU, 2 by 2 max pooling, and a normalization of each channel over the pooled image. ⍝ Expected: 118 copies on macOS, 257 on Linux and 474 in wasm: products of 99.5 million flops, 1,518 an item, which take 51 at Accelerate's rank-k rate, 190 at 8 in a copy's time for rustymath's own kernels and 380 at 4 in wasm; one fused pass for the bias and ReLU over 1,843,200 items, about 28; the pooling's one fold along both axes of each window, which reads those items once and writes a quarter as many, about 18; and the normalization's four passes over 460,800 items, about 21. In wasm these passes cost about 94. @@ -290,7 +290,7 @@ X+.×⍤1Y ⍝⍝⍝ allows forced "+.×": a matrix product reads its arguments whole ⍝⍝⍝ allows forced "⌈/⍠[2 3]ₓ": a fold of windows reads its argument whole cnn⍤3 imgs -⍝⍝: 180 770 880 instructions linux 1290.515 allocated linux 1023.13 +⍝⍝: 180 770 880 instructions macos 963.712 linux 1290.515 allocated macos 1014.521 linux 1023.13 ⍝⍝ static •prefs ["kinds":"static" "int":32 "float":32] ⍝ Expected at 32 bits: 59 copies on macOS, 129 on Linux and 237 in wasm, half its 64-bit cost. CI measured 295 to 305 on Linux in runs 59-61, and up to 626 in runs 56-58 and 62, as at 64 bits. @@ -298,4 +298,4 @@ cnn⍤3 imgs ⍝⍝⍝ allows forced "+.×": a matrix product reads its arguments whole ⍝⍝⍝ allows forced "⌈/⍠[2 3]ₓ": a fold of windows reads its argument whole cnn⍤3 imgs -⍝⍝: 89 390 570 instructions linux 945.438 allocated linux 524.888 +⍝⍝: 89 390 570 instructions macos 753.564 linux 945.438 allocated macos 520.603 linux 524.888 diff --git a/tests/speed/models.bpl b/tests/speed/models.bpl index 756ad238..7928d63a 100644 --- a/tests/speed/models.bpl +++ b/tests/speed/models.bpl @@ -102,32 +102,32 @@ heat←{ ⍝ Expected: 61 copies on macOS, 108 on Linux and 214 in wasm: a product of 33.5 million flops, 512 an item, which takes 17 at Accelerate's rank-k rate, 64 at 8 in a copy's time for rustymath's own kernels and 128 at 4 in wasm; `7○` on 262,144 items, 40 natively, at the 10 copies for each 65,536 items that rustymath's speed test expects of `Tanh`, and 80 in wasm, where vector arithmetic costs about twice; and the other passes, about 4 natively and 6 in wasm. ⍝⍝⍝ allows forced "+.×": a matrix product reads its arguments whole [mx my] mlp mp -⍝⍝: 92 120 330 instructions linux 272.348 allocated linux 124.321 +⍝⍝: 92 120 330 instructions macos 176.198 linux 272.348 allocated macos 100.071 linux 124.321 ⍝ ADBench's Gaussian mixture objective, for 1000 points in 32 dimensions and 25 components, without its constant terms and with its Wishart prior at gamma 1 and m 0. It's written in index notation. Each component's precision factor has the exponentials of `q` on its diagonal and `l` below it. ⍝ Expected: 63 copies on macOS, 135 on Linux and 247 in wasm: 25 products of 1000×32 by 32×32, 781 flops an item in all, which take 26 at Accelerate's rank-k rate, 98 at 8 in a copy's time for rustymath's own kernels and 195 at 4 in wasm; and three passes over 800,000 items, about 12 copies each, 37 natively and 52 in wasm. ⍝ On 27 October 2026, after a change to its period step, the index form measured 72.5 on macOS, against 72 for the tacit form earlier that day, and up to 99 during a video call. CI measured 198 to 237 on Linux in most runs from 56 to 62. gx gmm gp -⍝⍝: 95 240 400 instructions linux 265.107 allocated linux 161.973 +⍝⍝: 95 240 400 instructions macos 280.837 linux 265.107 allocated macos 136.945 linux 161.973 ⍝ The same objective in rank notation (`gmmr`). ⍝ Expected: 63 copies on macOS, 135 on Linux and 247 in wasm, as in index notation. On 1 November 2026 it measured 77.5 to 82.1 on macOS, against 74.6 to 82.6 for the index form. Its `+.×⍤2` computes the 800,000-item difference once, as the index form's contraction writes its laid-out copy of it once. ⍝⍝⍝ allows forced "+.×⍤2": a product reads its arguments whole gx gmmr gp -⍝⍝: 95 230 370 instructions linux 173.933 allocated linux 147.072 +⍝⍝: 95 230 370 instructions macos 177.259 linux 173.933 allocated macos 122.572 linux 147.072 ⍝ The k-means cost sums the squared distance from each of 16384 points to the nearest of 64 centres, in 32 dimensions. ⍝ Expected: 84 copies on macOS, 178 on Linux and 326 in wasm: a product of 64×32 by 32×16384, 67 million flops, 1,024 an item, which takes 34 at Accelerate's rank-k rate, 128 at 8 in a copy's time for rustymath's own kernels and 256 at 4 in wasm; and passes over a million distances, about 50 natively and 70 in wasm. ⍝ CI measured 247 to 267 on Linux in runs 56 and 59-61, and 311 to 338 in wasm in most runs from 56 to 62. kx kmeans kc -⍝⍝: 130 270 460 instructions linux 79.84 allocated linux 86.19 +⍝⍝: 130 270 460 instructions macos 67.787 linux 79.84 allocated macos 69.627 linux 86.19 ⍝ The same cost in index notation. Both forms are kept because they trade speed for accuracy. The tacit form expands each squared distance as |x|²+|c|²−2x·c, which runs as one matrix product and stays faster, but loses accuracy where a point is near a centre, because the terms nearly cancel. The index form subtracts first, so its error stays proportional to the distance. ⍝ Expected: 208 copies natively and 410 in wasm: 33.5 million differences, each squared and summed, 1,536 flops an item, at 8 in a copy's time for rustymath's own kernels and 4 in wasm; and the million distances, written and then read by the `⌊` fold, about 16 natively and 24 in wasm. ⍝ On 27 October 2026 it measured 1,089 on macOS, while its fold copied `x.[n d]` across all 33,554,432 items of the grid ("repeats of each item"). Since that fold reads the repeats in place, it measures about 780. ⍝⍝⍝ held macos 781: the fold over the grid runs at about 0.2 ns an item, where a fold that kept its sums in registers would come near the expected cost kx kmeansi kc -⍝⍝: 320 1100 2200 instructions linux 387.535 allocated linux 24.623 +⍝⍝: 320 1100 2200 instructions macos 405.564 linux 387.535 allocated macos 24.619 linux 24.623 ⍝ A pre-norm transformer decoder block over 4 sequences of 64 tokens: width 128, 4 heads of width 32, an MLP of width 512 with GELU in its tanh form, and a cross-entropy loss over 512 tokens. Its parameters are in a record. `dec` gives the mean loss, and is written in index notation. ⍝ Expected: 155 copies on macOS, 354 on Linux and 684 in wasm: products of 142.6 million flops, 2,176 an item, which take 72.5 at Accelerate's rank-k rate, 272 at 8 in a copy's time for rustymath's own kernels and 544 at 4 in wasm; and the elementwise passes, about 82 natively and 140 in wasm. Those are GELU's `7○` and its polynomial on 131,072 items, about 30; the cross-entropy's exponentials on 131,072 items, about 18; the attention's scaling, mask and softmax on 65,536 items, about 13; three layer norms, about 11; and the bias adds and residuals, about 10. @@ -135,20 +135,20 @@ kx kmeansi kc ⍝ CI measured 390 to 448 on Linux in runs 56-61, and 639 to 667 in wasm in runs 57-59 and 61. ⍝⍝⍝ allows forced "+.×⍤2": a matrix product reads its arguments whole [tid ttg] dec tp -⍝⍝: 240 480 790 instructions linux 3840.821 allocated linux 1423.529 +⍝⍝: 240 480 790 instructions macos 2066.642 linux 3840.821 allocated macos 1000.483 linux 1423.529 ⍝ A 3 by 3 blur of a 128 by 128 grid. `⌺` calls its operand once, on a batch of all the windows. ⍝ Expected: 1.5 copies: nine multiply-adds for each of 16,384 windows, read from the cache. ⍝⍝⍝ held macos 6.07: open.md lists the stencils among the speed audit's outliers that compiling didn't fix {+/,sk×⍵}⌺3 3 sg -⍝⍝: 3 15 16 instructions linux 31.014 allocated linux 25.868 +⍝⍝: 3 15 16 instructions macos 40.571 linux 31.014 allocated macos 25.868 linux 25.868 ⍝ One step of heat diffusion on a 512 by 512 grid, 4 times `n`, reading each cell's four neighbours through `•edge`'s clamped positions. ⍝ Expected: 9 copies: one fused pass of seven calls over 262,144 items, which reads each neighbour from forward runs, as `hu+0.2×(hu+hu+hu+hu)-4×hu` measures it. Wasm, twice that. ⍝ On 27 October 2026 it measured 26 to 31 on macOS. ⍝⍝⍝ held macos 26: its two column-shifted reads through `•edge`'s clamped positions copy their whole input, because a run of positions doesn't end at a row's edge, and ending runs there costs more than the copy saves heat hu -⍝⍝: 18 58 94 instructions linux 94.234 allocated linux 182.146 +⍝⍝: 18 58 94 instructions macos 130.672 linux 94.234 allocated macos 182.123 linux 182.146 ⍝ The loss of one convolutional layer with tanh units: 8 images of 3 channels, each 32 by 32, into 16 channels through a 3 by 3 kernel, against the targets `cy`. ⍝ Expected: 29 copies on macOS, 39 on Linux and 78 in wasm: the windows' copy, 221,184 items, about 3.4; a product of 3.5 million multiply-adds, 108 flops an item, which takes 3.6 at Accelerate's rank-k rate, 13.5 at 8 in a copy's time for rustymath's own kernels and 27 at 4 in wasm; `7○` on 131,072 items, 20 natively and 40 in wasm; and the loss's pass, about 2. @@ -156,42 +156,42 @@ heat hu ⍝⍝⍝ held macos 55: the gap from the expected 29 isn't placed ⍝⍝⍝ allows forced "+.×": a matrix product reads its arguments whole cx closs ck -⍝⍝: 58 110 190 instructions linux 268.612 allocated linux 150.113 +⍝⍝: 58 110 190 instructions macos 288.556 linux 268.612 allocated macos 146.179 linux 150.113 ⍝⍝ derivatives ⍝ `∂` beside the function it differentiates. A unit argument takes one pass, whose tangent is an array of the result's shape. The bar's "Primal cost" row in the design notes asks that the primal keeps its kernels, and the tangent adds whole-array calls beside it. ⍝ Expected: `1○dx` 112 copies: sines of a million items, 16 times `n`, at 7 copies each, as `1○v` counts them; `{1○dx+⍵}∂0` 116 copies: cosines of a million items at 7 copies each, with the sum fused into their pass. `∂` runs no sines, because its result is the tangent alone. ⍝ In wasm both expect twice their native costs, 224 and 232, as `1○v` does. 1○dx -⍝⍝: 170 120 470 instructions linux 53.277 allocated linux 32.119 +⍝⍝: 170 120 470 instructions macos 93.045 linux 53.277 allocated macos 32.119 linux 32.119 ⍝ CI measured 124 to 128 on Linux in runs 56 and 59-61, and 389 to 423 in wasm in runs 59-61. On 30 October 2026 it measured 144 on macOS. {1○dx+⍵}∂0 -⍝⍝: 180 170 580 instructions linux 62.442 allocated linux 36.253 +⍝⍝: 180 170 580 instructions macos 107.898 linux 62.442 allocated macos 36.253 linux 36.253 ⍝ A chain of fusible calls, whose tangent fuses too. ⍝ Expected: `(dx-0.5)×dx+0.5` 24 copies: one fused pass over a million items, 16 times `n`; `{(dx-⍵)×dx+⍵}∂0.5` 24 copies: the tangent's fused pass. `∂` skips the primal, because the result holds only the tangent. ⍝ CI measured 17 to 21 on Linux in most runs from 56 to 62. dx-0.5 × dx+0.5 -⍝⍝: 48 53 94 instructions linux 17.285 allocated linux 96.462 +⍝⍝: 48 53 94 instructions macos 26.586 linux 17.285 allocated macos 96.462 linux 96.462 {dx-⍵ × dx+⍵}∂0.5 -⍝⍝: 48 53 120 instructions linux 29.646 allocated linux 38.296 +⍝⍝: 48 53 120 instructions macos 40.359 linux 29.646 allocated macos 38.296 linux 38.296 ⍝ Gradients of sums of an elementwise function of 65,536 items. ⍝ Expected: `{+/√⍵}∂ dv` 3 copies: a square root and a division of each item, in one pass; `{+/|⍵}∂ dw` 1.2 copies: the sign of each item times the cotangent, in one pass. Their forward folds `+/√dv` and `+/|dw` cost 2.39 and 1.61 copies on 30 October 2026. ⍝⍝⍝ gap stopped "a result whose type depends on values": the gradient of a sum of square roots should keep a guarded real trace {+/√⍵}∂ dv -⍝⍝: 6 19 17 instructions linux 27.43 allocated linux 41.761 +⍝⍝: 6 19 17 instructions macos 38.021 linux 27.43 allocated macos 41.768 linux 41.761 {+/|⍵}∂ dw -⍝⍝: 2.4 14 27 instructions linux 7.053 allocated linux 20.277 +⍝⍝: 2.4 14 27 instructions macos 10.821 linux 7.053 allocated macos 20.277 linux 20.277 ⍝ `√ₐ` gives a real result whatever the values, so its gradient keeps its trace under dynamic kinds and replays as one compiled stage with its two recorded selects. Expected: `{+/√ₐ⍵}∂ dv` 3 copies, as `{+/√⍵}∂ dv` expects: a square root and a division of each item, in one pass. `+/√ₐdv` costs what `+/√dv` costs: 3.14 and 3.04 copies on 1 November 2026. {+/√ₐ⍵}∂ dv -⍝⍝: 6 20 16 instructions linux 19.041 allocated linux 31.326 +⍝⍝: 6 20 16 instructions macos 28.452 linux 19.041 allocated macos 31.326 linux 31.326 +/√ₐdv -⍝⍝: 6 5.7 4.7 instructions linux 7.263 allocated linux 8.461 +⍝⍝: 6 5.7 4.7 instructions macos 11.904 linux 7.263 allocated macos 8.461 linux 8.461 ⍝ A JVP of the MLP's loss in `[objectives]` along a fixed direction `dp`, which takes one pass through `+.×`, Rank, `7○` and `+/`. ⍝ Expected: 122 copies on macOS, 216 on Linux and 428 in wasm: the primal and one tangent pass of the same size. @@ -199,28 +199,28 @@ dx-0.5 × dx+0.5 ⍝⍝⍝ allows itemwise "item pairs": `mp+⍵×dp` pairs the parameter record's few arrays one at a time, each by a whole-array call ⍝⍝⍝ gap stopped "a result whose type depends on values": the MLP JVP should trace record-valued parameter arithmetic; a pervasive call on one item inside `∂`'s rule for `mp+⍵×dp` plans no settled type, under static kinds too {[mx my] mlp mp+⍵×dp}∂0 -⍝⍝: 190 230 500 instructions linux 560.694 allocated linux 218.994 +⍝⍝: 190 230 500 instructions macos 322.269 linux 560.694 allocated macos 170.494 linux 218.994 ⍝ Gradients of the MLP, Gaussian mixture and k-means objectives in `[objectives]`, each from one run that records its linear calls and one transpose of the record. The bar's "Gradient cost" row asks for a small multiple of the objective's own cost. ⍝ Expected: `[mx my] mlp∂ mp` 120 copies on macOS, 261 on Linux and 520 in wasm: the primal, a transpose with two products, which take 17 each at Accelerate's rank-k rate, 64 on rustymath's own kernels and 128 in wasm, and `7○`'s derivative with the rest, about 25 natively and 50 in wasm; `gx gmm∂ gp` 189 copies on macOS, 405 on Linux and 741 in wasm: about three times the objective; `kx kmeans∂ kc` 252 copies on macOS, 534 on Linux and 978 in wasm: about three times the objective. [mx my] mlp∂ mp -⍝⍝: 180 260 530 instructions linux 900.047 allocated linux 495.039 +⍝⍝: 180 260 530 instructions macos 660.617 linux 900.047 allocated macos 429.907 linux 495.039 ⍝ The tacit form that this index form replaced measured up to 932 on Linux on CI on 22 and 23 October 2026, more than twice its expected cost. CI measured the index form at 484 to 511 on Linux in most runs from 56 to 62, and 602 to 963 in wasm. ⍝ On 27 October 2026 the index form measured 345 to 375 on macOS during a video call, against 239 for the tacit form earlier that day. Both forms trace under dynamic kinds too, since range facts show that `⍟` reads a non-negative argument. Traced, under static kinds, the index form's gradient is the faster. ⍝⍝⍝ allows forced "+.×⍤2": a matrix product reads its arguments whole gx gmm∂ gp -⍝⍝: 290 730 920 instructions linux 301.063 allocated linux 284.71 +⍝⍝: 290 730 920 instructions macos 250.523 linux 301.063 allocated macos 225.177 linux 284.71 ⍝ The gradient of the rank spelling. ⍝ Expected: 189 copies on macOS, 405 on Linux and 741 in wasm, as in index notation. On 1 November 2026 it measured 173 to 179 on macOS, against 152 to 158 for the index form, with one more contraction; once a fold of a product sums out the points first, 153.0 against 148.4 (plan, gs13_'s entry). ⍝⍝⍝ allows forced "+.×⍤2": a matrix product reads its arguments whole gx gmmr∂ gp -⍝⍝: 290 670 990 instructions linux 366.331 allocated linux 325.228 +⍝⍝: 290 670 990 instructions macos 253.56 linux 366.331 allocated macos 242.228 linux 325.228 ⍝ CI measured 459 to 489 on Linux in most runs from 56 to 62. kx kmeans∂ kc -⍝⍝: 380 730 1000 instructions linux 139.244 allocated linux 139.153 +⍝⍝: 380 730 1000 instructions macos 106.299 linux 139.244 allocated macos 105.591 linux 139.153 ⍝ The gradient of the index form of the k-means cost in `[objectives]`. ⍝ Expected: 624 copies natively and 1,230 in wasm: about three times the objective. @@ -228,7 +228,7 @@ kx kmeans∂ kc ⍝⍝⍝ held macos 1942: its gradient stores the 268 MB difference grid (open.md, "kmeans' difference grid") ⍝⍝⍝ allows forced "+.×⍤2": a matrix product reads its arguments whole kx kmeansi∂ kc -⍝⍝: 820 6300 6000 instructions linux 654.044 allocated linux 305.659 +⍝⍝: 820 6300 6000 instructions macos 782.68 linux 654.044 allocated macos 305.649 linux 305.659 ⍝ The gradient of the decoder block in `[objectives]`, from one run that records its linear calls and one transpose of the record. ⍝ Expected: 465 copies on macOS, 1060 on Linux and 2050 in wasm: about three times the objective. @@ -237,43 +237,43 @@ kx kmeansi∂ kc ⍝⍝⍝ allows forced "+.×⍤2": a matrix product reads its arguments whole ⍝⍝⍝ gap forced "+/": the gradient's fold of 65,536 items should run in its pending argument's fused pass [tid ttg] dec∂ tp -⍝⍝: 700 1500 2400 instructions linux 18200.413 allocated linux 8143.266 +⍝⍝: 700 1500 2400 instructions macos 11875.721 linux 18200.413 allocated macos 6953.7 linux 8143.266 ⍝ The gradient of the convolutional layer's loss in `[objectives]`, with respect to its kernel. ⍝ Expected: 87 copies on macOS, 117 on Linux and 234 in wasm: about three times the objective. ⍝ On 27 October 2026 it measured 112 to 122 on macOS. CI measured 180 to 188 on Linux in most runs from 56 to 62, and 227 to 234 in wasm. ⍝⍝⍝ allows forced "+.×⍤2": a matrix product reads its arguments whole cx closs∂ ck -⍝⍝: 140 300 380 instructions linux 351.103 allocated linux 264.819 +⍝⍝: 140 300 380 instructions macos 303.36 linux 351.103 allocated macos 224.399 linux 264.819 ⍝⍝ static •prefs ["kinds":"static" "int":32 "float":32] ⍝ Expected at 32 bits: the decoder block 78 copies on macOS, 177 on Linux and 342 in wasm, and its gradient 233, 530 and 1025, half their 64-bit costs. On 27 October 2026 the index form measured 104 and 367 on macOS, against 68 and 190 for the tacit form it replaced. Since a cotangent's sum along adjacent axes runs as one fold, which a product takes (`eval::folded_along`), the gradient measures 187 to 188, and the decoder block measures 102 to 105. The decoder block now measures 101 on macOS. CI measured it at 247 to 284 on Linux, and 341 to 361 in wasm in most runs from 56 to 62. ⍝⍝⍝ allows forced "+.×⍤2": a matrix product reads its arguments whole [tid ttg] dec tp -⍝⍝: 120 320 480 instructions linux 2353.918 allocated linux 764.127 +⍝⍝: 120 320 480 instructions macos 1342.373 linux 2353.918 allocated macos 552.581 linux 764.127 ⍝ CI measured 568 to 647 on Linux in runs 59-61, and 848 to 1,117 in wasm in every run but 58. ⍝⍝⍝ allows forced "+.×⍤2": a matrix product reads its arguments whole [tid ttg] dec∂ tp -⍝⍝: 350 690 1100 instructions linux 6293.95 allocated linux 2240.716 +⍝⍝: 350 690 1100 instructions macos 2598.654 linux 6293.95 allocated macos 1645.815 linux 2240.716 ⍝ Expected at 32 bits: 34 copies on macOS, 80 on Linux and 148 in wasm. The products, the square and fold along `d`, and the rest take half their 64-bit time. CI's widths case `m+.×m` shows this for products: f32 takes 0.47 to 0.60 of the f64 time natively and 0.50 to 0.52 in wasm. Building `r` doesn't take half: on rustymath's own kernels, the products and the building of `r` that they read cost about 24 copies more than one product of the same size from stored matrices, at either width. That's 12 more than half natively, and twice that in wasm. Where those copies go isn't placed. On macOS, where Accelerate computes the products, the case takes 0.53 of its 64-bit time. On 29 October 2026 it measured 38.7 on macOS, and 105 with rustymath's own kernels, against 123 before `rustymath::linalg::product` moved each tile's rows whole. In runs 56 to 62 CI measured 124 to 142 on Linux and 139 to 175 in wasm. The square and fold costs about twice one read of the products' 800,000 results, at both widths. gx gmm gp -⍝⍝: 68 150 210 instructions linux 229.724 allocated linux 89.672 +⍝⍝: 68 150 210 instructions macos 241.835 linux 229.724 allocated macos 77.145 linux 89.672 ⍝ The same objective in rank notation, at 32 bits. ⍝ Expected: 34 copies on macOS, 80 on Linux and 148 in wasm, as in index notation. On 1 November 2026 it measured 40.6 on macOS, against 40.5 for the index form. Its `+.×⍤2ₛ` computes the 800,000-item difference here too. ⍝⍝⍝ allows forced "+.×⍤2ₛ": a product reads its arguments whole gx gmmr gp -⍝⍝: 68 130 210 instructions linux 143.507 allocated linux 78.994 +⍝⍝: 68 130 210 instructions macos 143.966 linux 143.507 allocated macos 66.744 linux 78.994 ⍝ Expected at 32 bits: the gradient 95 copies on macOS, 203 on Linux and 371 in wasm, half its 64-bit costs. It runs from a program here, where `⍟` stops the 64-bit section's trace under dynamic kinds. On 29 October 2026 it measured 77 on macOS. CI measured 249 to 298 on Linux in runs 56 to 62. ⍝⍝⍝ allows forced "+.×⍤2": a matrix product reads its arguments whole gx gmm∂ gp -⍝⍝: 150 320 430 instructions linux 233.894 allocated linux 165.67 +⍝⍝: 150 320 430 instructions macos 205.647 linux 233.894 allocated macos 135.637 linux 165.67 ⍝ The gradient of the rank spelling, at 32 bits. ⍝ Expected: 95 copies on macOS, 203 on Linux and 371 in wasm, as in index notation. On 1 November 2026 it measured 83.3 on macOS, against 78.0 for the index form, and 78.0 once a fold of a product sums out the points first (plan, gs13_'s entry). ⍝⍝⍝ allows forced "+.×⍤2": a matrix product reads its arguments whole gx gmmr∂ gp -⍝⍝: 150 410 610 instructions linux 277.243 allocated linux 184.194 +⍝⍝: 150 410 610 instructions macos 207.964 linux 277.243 allocated macos 142.694 linux 184.194 diff --git a/tests/speed/moving.bpl b/tests/speed/moving.bpl new file mode 100644 index 00000000..aefa2e73 --- /dev/null +++ b/tests/speed/moving.bpl @@ -0,0 +1,14 @@ +•prefs ["mathish":$f "seed":0] +⍝⍝ setup +•prefs ["seed":0] ⋄ n←100000 ⋄ v←¿n⍴0 ⋄ w←101 +⍝⍝ moving +⍝ Width 101 uses rustymath's prefix/suffix sliding sum (about three additions/output), then bulk division: O(n), without an n×w window buffer. +⍝ Expected: 6 copy-equivalents for HGW reads/writes, buffer initialization, serial dependencies and bulk division; the shared Linux budget is twice that. macOS/wasm await measurement. +(+/w↕v)÷w +⍝⍝: - 12 - instructions macos 15.874 allocated macos 24.124 + +{(+/⍵)÷≢⍵}⍤1 w↕v ⍝ (+/w↕v)÷w +⍝⍝: - 12 - instructions macos 16.144 allocated macos 24.136 + +(+/÷≢)⍤1 w↕v ⍝ (+/w↕v)÷w +⍝⍝: - 12 - instructions macos 18.141 allocated macos 25.455 diff --git a/tests/speed/steps.bpl b/tests/speed/steps.bpl index 49838e6d..b4f9319b 100644 --- a/tests/speed/steps.bpl +++ b/tests/speed/steps.bpl @@ -7,93 +7,93 @@ loss←{+/⊼1-2×⍵} ⋄ guarded←{0<+/⍵ ? +/⊼1-2×⍵ ; 0} ⋄ rosen←{ ⍝⍝ calls ⍝ A primitive call. +⍣n 0 -⍝⍝: - - - instructions linux 3082.852 allocated linux 24.688 +⍝⍝: - - - instructions macos 3442.289 linux 3082.852 allocated macos 24.688 linux 24.688 ⍝ A dfn call that returns its argument. {⍵}⍣n 0 -⍝⍝: - - - instructions linux 1678.148 allocated linux 9.812 +⍝⍝: - - - instructions macos 1727.492 linux 1678.148 allocated macos 9.812 linux 9.812 ⍝ A dfn that reads a global. {⍵+k}⍣n 0 -⍝⍝: - - - instructions linux 5811.883 allocated linux 41.812 +⍝⍝: - - - instructions macos 6119.898 linux 5811.883 allocated macos 41.812 linux 41.812 ⍝ A local assignment and read. {x←⍵+1 ⋄ x}⍣n 0 -⍝⍝: - - - instructions linux 6387.711 allocated linux 41.812 +⍝⍝: - - - instructions macos 6767.648 linux 6387.711 allocated macos 41.812 linux 41.812 ⍝ A spaced run that applies a train at once. The compiled statement calls the train's tines in place, without building the train (`Op::CallTrain`). {(⍵)+⊢ ⍵}⍣n 0 -⍝⍝: - - - instructions linux 7990.398 allocated linux 137.812 +⍝⍝: - - - instructions macos 8670.359 linux 7990.398 allocated macos 137.812 linux 137.812 ⍝ A bracket literal. Joining its items' kinds, fixedness and weakness in `Promoted::join` added about 125 instructions on 24 October 2026. The memory guard's check on each allocation and the later promotion refactor added about 95 more. Moving `other_step` out of line and the `Value::Pending` arms in the resolving functions added about 220 more. {↑[⍵ 1]}⍣n 0 -⍝⍝: - - - instructions linux 6117.836 allocated linux 297.812 +⍝⍝: - - - instructions macos 7699.484 linux 6117.836 allocated macos 297.812 linux 297.812 ⍝ Each over the 64 strings in `t`, of two lengths, reverses them as ragged cells: one walk of each string's plan over their leaf (`batch::segment_remap`), with the batched run's framing and assembly. ⌽¨⍣n t -⍝⍝: - - - instructions linux 440998.188 allocated linux 37761.812 +⍝⍝: - - - instructions macos 520127.914 linux 440998.188 allocated macos 37761.812 linux 37761.812 ⍝ Rank's general path calls a dfn once for each of the same 64 cells. {⌽⍵}⍤0⍣n t -⍝⍝: - - - instructions linux 664270.078 allocated linux 43355.375 +⍝⍝: - - - instructions macos 678296.812 linux 664270.078 allocated macos 43355.375 linux 43355.375 ⍝ A system function that converts each item on its own runs once on the leaf of the same 64 strings (`batch::leaf_system`). •ucs⍣n t -⍝⍝: - - - instructions linux 241054.875 allocated linux 11984.688 +⍝⍝: - - - instructions macos 240322.578 linux 241054.875 allocated macos 11984.688 linux 11984.688 ⍝ `⇄` by L-BFGS on Rosenbrock's function: 53 evaluations, about 153,500 instructions each. Each evaluation calls `linearized` directly, outside the program cache. The case runs once, so its counts are for a 1,024th of the run. rosen⇄ ¯1.2 1 -⍝⍝: - - - instructions linux 167131.688 allocated linux 27655.133 +⍝⍝: - - - instructions macos 192045.312 linux 167131.688 allocated macos 27658.445 linux 27655.133 ⍝⍝ steps ⍝ A fixed mark on a 4-item float vector, which floors each item to `i32` through `derivative::converted`, the entry point that marks on values and on functions share. Stage 3's step 6 sent value marks there, which cost about 215 instructions. The inline size test in `fusion::convert` and the inline tests of `Value::carried` and `Value::holds_values` recover about 100 of them. On 26 October 2026, single-stepping `binder::derived` under lldb counted every instruction that it and its calls retire. Outside `malloc`, the other 115 lay in four functions: 130 in `derivative::converted`, 17 in an out-of-line `product` for its size test's length, 15 in `Value::converted`'s dispatch, and 47 fewer in `binder::derived`, which no longer reads the settings itself. Of the 130, about 45 read the settings, 25 test for carriers and 12 make the size test. The inline `rustymath::layout::count` and an always-inlined `derivative::plain` then saved 19. v_4ₓ₄ -⍝⍝: - - - instructions linux 6222.758 allocated linux 536 +⍝⍝: - - - instructions macos 8263.07 linux 6222.758 allocated macos 536 linux 536 ⍝ `fib 6ₓ` makes 25 calls, which read `⍵` and `∇`. fib 6ₓ -⍝⍝: - - - instructions linux 338345.82 allocated linux 2704 +⍝⍝: - - - instructions macos 346741.125 linux 338345.82 allocated macos 2704 linux 2704 ⍝ `g 6ₓ` makes 25 calls of a dfn that calls itself by name, with spaces that split its statement into runs. Each call applies a train at once, whose tines the compiled statement calls in place (`Op::CallTrain`). g 6ₓ -⍝⍝: - - - instructions linux 358466.828 allocated linux 3856 +⍝⍝: - - - instructions macos 371339.555 linux 358466.828 allocated macos 3856 linux 3856 ⍝ `fib 6ₓ` under static kinds, whose integer scalar calls run their integer kernel at the `int` width (`Primitive::integer_kernel`). Each step also sets the preference. •prefs ["kinds":"static"] ⋄ fib 6ₓ -⍝⍝: - - - instructions linux 364488.5 allocated linux 6136 +⍝⍝: - - - instructions macos 381466.312 linux 364488.5 allocated macos 6112 linux 6136 ⍝ `fib 6ₓ₂` on 16-bit fixed integers, whose scalar calls run their integer kernel at that width (`Primitive::fixed_number`), and whose comparisons compare the integers directly (`Number::order`). fib 6ₓ₂ -⍝⍝: - - - instructions linux 369395.82 allocated linux 1552 +⍝⍝: - - - instructions macos 361931.453 linux 369395.82 allocated macos 1552 linux 1552 ⍝ A 2×2 float matrix product. m+.×m -⍝⍝: - - - instructions linux 86488.672 allocated linux 15032 +⍝⍝: - - - instructions macos 112926.297 linux 86488.672 allocated macos 14808 linux 15032 ⍝ Rank on a stack of 64 Boolean 2×2 matrices, which runs one or-and product for the whole stack. ∨.∧⍤2⍨s -⍝⍝: - - - instructions linux 117393.383 allocated linux 18354 +⍝⍝: - - - instructions macos 137271.828 linux 117393.383 allocated macos 18354 linux 18354 ⍝ `∂` of a dfn on 4 floats, whose later calls replay the program that its first call traced. loss∂v_4 -⍝⍝: - - - instructions linux 33128.766 allocated linux 34888 +⍝⍝: - - - instructions macos 39399.234 linux 33128.766 allocated macos 34888 linux 34888 ⍝ The same on 16 floats. At this size a replay costs the same per call. loss∂v_16 -⍝⍝: - - - instructions linux 33152.766 allocated linux 35080 +⍝⍝: - - - instructions macos 39468.695 linux 33152.766 allocated macos 35080 linux 35080 ⍝ `∂` with no program. A guard on `⍵` stops the first call's trace ("a read of a traced value"). Each call then runs through duals, a linear record and its transpose (`linear::transpose`). The count includes the guard's `+/` and `<`. guarded∂v_4 -⍝⍝: - - - instructions linux 301455.188 allocated linux 42361 +⍝⍝: - - - instructions macos 379716.836 linux 301455.188 allocated macos 42385 linux 42361 ⍝ The same on 16 floats. guarded∂v_16 -⍝⍝: - - - instructions linux 302444.5 allocated linux 43129 +⍝⍝: - - - instructions macos 380599.977 linux 302444.5 allocated macos 43153 linux 43129 ⍝ Selective assignment through a selecting function on 4 floats, then a read of the result. The target `1↑a` runs under a `Label` level on the simple root `a`, whose labels hold a mapping and no ids. `Selection::write_into` writes the value at the position that the labels give. (1↑a)←0 ⋄ a -⍝⍝: - - - instructions linux 25247.758 allocated linux 4832 +⍝⍝: - - - instructions macos 33168.719 linux 25247.758 allocated macos 4832 linux 4832 ⍝ Under with a selecting function on the same kind of root. `1↑` runs under a `Label` level and `2×` on its result. `Selection::write_into` writes that result at the position that the labels give. (2×)@(1↑)v_4 -⍝⍝: - - - instructions linux 47231.773 allocated linux 7332 +⍝⍝: - - - instructions macos 61749.781 linux 47231.773 allocated macos 7332 linux 7332 diff --git a/tests/speed/structure.bpl b/tests/speed/structure.bpl index aa9cbdde..2ce17897 100644 --- a/tests/speed/structure.bpl +++ b/tests/speed/structure.bpl @@ -22,219 +22,218 @@ rows←["b":0 "h":["i":1 ∞;"y":2 16] "w":3 "c":4]⍉ ⍝ Expected: `2×⌽sv+1` 1.25 copies: one fused pass, with an 8-byte read and write an item, and a block buffer for the reversal; `2×1⌽sv+1` and `2× 10↓sv + 1` 1 copy: one fused pass, with an 8-byte read and write an item; `+/ ⌽sv × sv` 1 copy: two 8-byte reads of `sv` an item, folded. ⍝ CI measured 1.28 to 1.66 on Linux and 2.78 to 3.19 in wasm in most runs from 56 to 62. 2×⌽sv+1 -⍝⍝: 2.5 2.3 5.6 instructions linux 5.218 allocated linux 9.423 +⍝⍝: 2.5 2.3 5.6 instructions macos 7.967 linux 5.218 allocated macos 9.426 linux 9.423 ⍝ CI measured 1.09 to 1.58 on Linux in most runs from 56 to 62. 2×1⌽sv+1 -⍝⍝: 2 2.3 3.6 instructions linux 4.352 allocated linux 9.423 +⍝⍝: 2 2.3 3.6 instructions macos 6.574 linux 4.352 allocated macos 9.426 linux 9.423 ⍝⍝⍝ route fusion 2× 10↓sv + 1 -⍝⍝: 2 2.2 3.5 instructions linux 2.207 allocated linux 24.056 +⍝⍝: 2 2.2 3.5 instructions macos 5.293 linux 2.207 allocated macos 24.056 linux 24.056 +/ ⌽sv × sv -⍝⍝: 2 2.5 4.1 instructions linux 4.299 allocated linux 1.204 +⍝⍝: 2 2.5 4.1 instructions macos 9.201 linux 4.299 allocated macos 1.205 linux 1.204 ⍝ A reversed read moves only under a call that reads one array. `sv×sv+0.5` reads two, so it runs forward first, and `2×` then reads its result backwards. ⍝ Expected: 2 copies: two passes, forward then reversed. ⍝ CI measured 2.12 to 2.32 on Linux and 3.89 to 4.34 in wasm in most runs from 56 to 62. 2×⌽sv×sv+0.5 -⍝⍝: 4 5.6 8.8 instructions linux 7.154 allocated linux 17.497 +⍝⍝: 4 5.6 8.8 instructions macos 10.702 linux 7.154 allocated macos 17.5 linux 17.497 ⍝ A rotate of an overtake that carries only fill across the end composes with it into one padded read of the source (`Steps::read_through`). ⍝ Expected: 1 copy: one pass that reads `sv` through one padded map, with an 8-byte read and write an item. ⍝ On CI's Linux it measured 0.92 copies in v0.1.73, then 1.74 and 1.46 in v0.1.74 and v0.1.75. This Mac retires the same instructions on builds from before and after. The cause is inferred, because this Mac can't run x86-64. The read runs one item behind the write. When glibc maps both 8 MB arrays at the same offset within a page, each load's address matches the low 12 bits of the store one item before it, and Zen 4 stalls on that match (4K aliasing). The allocations and frees before the case decide whether the arrays share an offset. The speed suite's move in v0.1.74 changed them. 2ׯ1⌽(n+2)↑sv -⍝⍝: 2 2.3 2.6 instructions linux 2.809 allocated linux 8.737 +⍝⍝: 2 2.3 2.6 instructions macos 4.35 linux 2.809 allocated macos 8.739 linux 8.737 ⍝ Floor returns exact integers, so this reads a pending copy through an ordinary kernel rather than fusion. ⍝ Expected: 3.25 copies: a reversed copy of each block into a buffer, about 0.5 copies, then a tolerant floor of about 11 vector operations an item, with 64-bit writes. It was 2 copies, from the old count for a tolerant floor. ⍝ CI measured 4.75 to 5.07 in wasm in runs 56-61. -⍝⍝⍝ held linux 4.36: floor's dynamic integer result writes floats, scans them for a width, then converts ⌊⌽sv -⍝⍝: 6.5 3.5 6.7 instructions linux 7.997 allocated linux 9.066 +⍝⍝: 6.5 3.5 6.7 instructions macos 8.95 linux 7.997 allocated macos 9.067 linux 9.066 ⍝ Stride-two columns gather through the shared reader before their contiguous subtraction. ⍝ Expected: 1.25 copies: two strided reads of `sm`'s lines and a write of half as many items. [: 1]⌷sm - [: 0]⌷sm -⍝⍝: 2.5 1.8 4 instructions linux 4.952 allocated linux 12.104 +⍝⍝: 2.5 1.8 4 instructions macos 4.754 linux 4.952 allocated macos 12.104 linux 12.104 ⍝ A join along the last axis writes each row whole, a lane from each argument in turn, where the lanes are short. Lanes of one length up to 8 move as blocks. `sm` has lanes of 2. ⍝ Expected: 2 copies: writes twice as many items as `sm` holds. ⍝ `sm,sm` measures 1.4 on macOS since a join writes each row whole, a lane from each argument in turn, straight into the result (`gather::interleave`, 22 October 2026), down from 4.3 to 4.6, which a fill and a strided pass for each argument cost. CI measured 1.31 to 1.42 on Linux in most runs from 56 to 62. sm,sm -⍝⍝: 4 4.1 4.1 instructions linux 2.941 allocated linux 16.03 +⍝⍝: 4 4.1 4.1 instructions macos 4.088 linux 2.941 allocated macos 16.03 linux 16.03 ⍝ A join of 8-bit integers to floats converts the integers in one pass first, then joins as `sm,sm` does. `ism` holds exact positions, which take a byte each. ⍝ Expected: 2.6 copies: a pass that converts `ism`'s bytes into floats, about 0.6 copies with 1-byte reads and 8-byte writes, then the join's 2 copies, as for `sm,sm`. ⍝ `ism,sm` measures 2.9 on macOS since a join writes each row whole (`gather::interleave`, 22 October 2026), down from 4.2 to 4.4. The conversion pass runs one item at a time, about 3 instructions an item. CI measured 2.23 to 2.44 on Linux and 3.28 to 3.72 in wasm in most runs from 56 to 62. ism,sm -⍝⍝: 5.2 8 11 instructions linux 4.392 allocated linux 24.03 +⍝⍝: 5.2 8 11 instructions macos 5.811 linux 4.392 allocated macos 24.03 linux 24.03 ⍝ Two of the four columns of `s_4`, picked by position, read as a stride. The copy walks each row's two items in one loop of fixed width. A list of positions with uneven steps, and a rotation of each row, take the same loop through a table of offsets. ⍝ Expected: `⌊[:;0 1]⌷s_4` 2.1 copies: the strided gather, about 0.75 copies, as `[:;0 1]⌷s_4` alone costs, then a tolerant floor of about 11 vector operations an item on half the items, with 64-bit writes; `⌊[:;0 1 3]⌷s_4` 2.9 copies: a gather of three quarters of the items, about 0.875 copies, and the floor on them; `⌊1⌽s_4` 3.75 copies: a gather of every item through a table of offsets, about 1 copy, and the floor on all of them. They were 1.5, 1.7 and 2.5 copies, from narrow writes and the old count for a tolerant floor. ⌊[:;0 1]⌷s_4 -⍝⍝: 4.2 2.3 3.5 instructions linux 5.445 allocated linux 4.563 +⍝⍝: 4.2 2.3 3.5 instructions macos 6.082 linux 5.445 allocated macos 4.563 linux 4.563 ⌊[:;0 1 3]⌷s_4 -⍝⍝: 5.8 4.9 7.6 instructions linux 14.443 allocated linux 6.813 +⍝⍝: 5.8 4.9 7.6 instructions macos 13.046 linux 14.443 allocated macos 6.813 linux 6.813 ⍝ CI measured 4.28 to 4.54 on Linux in runs 56 and 59-61. ⌊1⌽s_4 -⍝⍝: 7.5 5.3 8.6 instructions linux 16.558 allocated linux 9.073 +⍝⍝: 7.5 5.3 8.6 instructions macos 13.891 linux 16.558 allocated macos 9.074 linux 9.073 ⍝ A write of two columns goes through the same loop. It overwrites `w_4` in place. ⍝ Expected: 1 copy: each 64-byte line of `w_4` holds two rows, and the write replaces half of each row, so it reads and writes every line of `w_4` once. [:;0 1]⌷w_4 ←p_2 -⍝⍝: 2 2 2.7 instructions linux 3.898 allocated linux 0.099 +⍝⍝: 2 2 2.7 instructions macos 4.224 linux 3.898 allocated macos 0.099 linux 0.099 ⍝ Booleans written into floats would leave the array mixed, as a literal of the same items would, so the case converts them first, as the style page says. `1×bp_2` computes the floats, and the write copies them into two columns. ⍝ Expected: 1.5 copies. Computing `1×bp_2` reads 1 byte and writes 8 bytes for each of half the items, about 0.5. Each 64-byte cache line of `w_4` holds two rows whose first two items the write replaces, so the write reads and writes every line of `w_4` once, about 1 copy. ⍝ In wasm it measured 1.64 to 2.12 in seven CI runs from 22 October 2026. [:;0 1]⌷w_4 ←1×bp_2 -⍝⍝: 3 2.8 4.6 instructions linux 5.102 allocated linux 4.615 +⍝⍝: 3 2.8 4.6 instructions macos 6.878 linux 5.102 allocated macos 4.615 linux 4.615 ⍝ A write of two columns of 32-bit floats into 64-bit floats converts each item as the same loop writes it, and the array stays 64-bit floats. ⍝ Expected: 1 copy. Each 64-byte cache line of `w_4` holds two rows whose first two items the write replaces, so the write reads and writes every line of `w_4` once. Reading 4 bytes of `f_2` for each of half the items adds little beside that. [:;0 1]⌷w_4 ←f_2 -⍝⍝: 2 2.1 2.8 instructions linux 4.554 allocated linux 0.599 +⍝⍝: 2 2.1 2.8 instructions macos 4.905 linux 4.554 allocated macos 0.599 linux 0.599 ⍝ A fold by a reduction reads a pending copy through its map. A copy that reads a contiguous block of its source with its axes in another order, such as a transpose, a reversal, or 2 by 2 windows that don't overlap, folds that block in its own order, at the cost of folding the source. Totals that come out in another order stay a pending copy, which the next fold reads the same way. ⍝ Expected: 0.5 copies each: reads 8 bytes an item and writes nothing. ⍝ Wasm on CI's x86 runner: under Node 26's V8, these folds cost what they cost natively, 0.36 to 0.46 copies in the runs of 28 and 29 October 2026. ⍝⍝⍝ route mapped fold +/⍉sq -⍝⍝: 1 1.1 0.98 instructions linux 1.791 allocated linux 0.196 +⍝⍝: 1 1.1 0.98 instructions macos 3.179 linux 1.791 allocated macos 0.197 linux 0.196 +⌿⊖sq -⍝⍝: 1 1.1 1 instructions linux 1.661 allocated linux 0.18 +⍝⍝: 1 1.1 1 instructions macos 2.943 linux 1.661 allocated macos 0.181 linux 0.18 +/⌽sv -⍝⍝: 1 0.99 0.89 instructions linux 1.298 allocated linux 0.106 +⍝⍝: 1 0.99 0.89 instructions macos 2.415 linux 1.298 allocated macos 0.107 linux 0.106 ⍝ A copy whose runs hold at least 128 items, rustymath's `reduce::RUN`, folds each run where it lies, with no copy. Half the columns of `sq` give runs of 512. ⍝ Expected: 0.5 copies: reads 8 bytes for each of half the items, and the call into the fold for each run costs about what reading its items costs. ⍝⍝⍝ route in place +/,[:;⍳halfₓ]⌷sq -⍝⍝: 1 0.69 0.66 instructions linux 1.798 allocated linux 4.097 +⍝⍝: 1 0.69 0.66 instructions macos 2.886 linux 1.798 allocated macos 4.097 linux 4.097 ⍝ Expected: 0.5 copies. ⍝⍝⍝ held macos 1.56: open.md lists wide and 2-D window folds among the speed audit's outliers ⌈/⌈/[[2 2]⋄[2 2]]↕sq -⍝⍝: 1 2.8 2.9 instructions linux 12.876 allocated linux 10.371 +⍝⍝: 1 2.8 2.9 instructions macos 14.522 linux 12.876 allocated macos 10.372 linux 10.371 +/⍉cimg -⍝⍝: 1 1.1 0.99 instructions linux 1.929 allocated linux 0.464 +⍝⍝: 1 1.1 0.99 instructions macos 3.495 linux 1.929 allocated macos 0.465 linux 0.464 ⍝ A fold of windows slides over its source and never builds the windows. Windows of up to 8 rows fold directly, finishing each block of a lane while it is in cache. Wider windows take van Herk-Gil-Werman, at a cost that doesn't grow with the width. Windows of a matrix fold in two separable passes. -⍝ Expected: `+/3↕sv` and `⌈/3↕sv` 1 copy: one read and one write per window, sliding; `+/50↕sv` 1 copy: a running sum, with one addition and one subtraction per window; `⌈/50↕sv` 2 copies: van Herk-Gil-Werman, with two passes of running maxima and a combine; `+/+/3 3↕sq` 2 copies: two separable sliding passes. +⍝ Expected: `+/3↕sv` and `⌈/3↕sv` 1 copy: one read and one write per window, sliding. Both `+/50↕sv` and `⌈/50↕sv` use van Herk-Gil-Werman: suffix and prefix folds plus a combine, with O(n) work and a length-n scratch buffer, not an add/subtract rolling sum. `+/+/3 3↕sq` 2 copies: two separable sliding passes. ⍝ On 25 October 2026 `+/3↕sv` measured 3.66 on macOS, for the reason `⌈/3↕sv`'s comment gives, and it measures 1.45 after the same change. CI then measured 0.85 on Linux and 1.26 in wasm, against 5.09 and 3.41 on 22 October 2026. All three budgets are twice its expected cost. ⍝⍝⍝ route sliding fold +/3↕sv -⍝⍝: 2 2.1 2.8 instructions linux 2.742 allocated linux 8.179 +⍝⍝: 2 2.1 2.8 instructions macos 5.07 linux 2.742 allocated macos 8.18 linux 8.179 ⍝ On 25 October 2026 `⌈/3↕sv` measured 3.66 on macOS, because the direct folds took one pass over the whole lane for each row of a window, which reads and writes `sv`'s 8 MB three times. Direct folds now finish each block of a lane in cache, with a window's first three rows in one pass, and `⌈/3↕sv` measures 1.55 on macOS. CI then measured 0.86 on Linux and 1.33 in wasm, against 5.85 and 2.6 on 23 October 2026. All three budgets are twice its expected cost. ⌈/3↕sv -⍝⍝: 2 2.1 2.6 instructions linux 3.491 allocated linux 8.179 +⍝⍝: 2 2.1 2.6 instructions macos 7.195 linux 3.491 allocated macos 8.18 linux 8.179 ⍝ CI measured 5.15 to 5.36 on Linux and 4.23 to 4.41 in wasm in most runs from 56 to 62. ⍝⍝⍝ held macos 3.85: open.md lists wide and 2-D window folds among the speed audit's outliers; busy hosts reach the Linux and wasm costs +/50↕sv -⍝⍝: 2 8.9 13 instructions linux 10.785 allocated linux 16.173 +⍝⍝: 2 8.9 13 instructions macos 16.08 linux 10.785 allocated macos 16.174 linux 16.173 ⍝ CI measured 4.93 to 5.4 on Linux and 7.41 to 7.7 in wasm in most runs from 56 to 62. ⍝⍝⍝ held macos 5.29: open.md lists wide and 2-D window folds among the speed audit's outliers; busy hosts reach the Linux and wasm costs ⌈/50↕sv -⍝⍝: 4 9.7 14 instructions linux 17.467 allocated linux 16.173 +⍝⍝: 4 9.7 14 instructions macos 24.766 linux 17.467 allocated macos 16.174 linux 16.173 ⍝ CI measured 2.18 to 2.54 on Linux and 2.3 to 2.6 in wasm in most runs from 56 to 62. +/+/3 3↕sq -⍝⍝: 4 5.3 6.9 instructions linux 6.059 allocated linux 16.256 +⍝⍝: 4 5.3 6.9 instructions macos 10.346 linux 6.059 allocated macos 16.258 linux 16.256 ⍝ Padded windows of a vector fold in a block stage that reads their source in place, through the padding of each edge mode. ⍝ Expected: about 1 copy each: one block stage reads the source once for each of a window's 3 positions, in cache, and writes the sums. ⍝ CI measured 1.82 to 2.55 on Linux and 2.46 to 2.72 in wasm in most runs from 56 to 62. ⍝ On CI's Linux the same code measured 7.97 copies on 22 October 2026, 4.77 on 23 October and in v0.1.73, then 6.98 and 6.75 in v0.1.74 and v0.1.75. The cause is inferred, as for `2ׯ1⌽(n+2)↑sv`. The padded copy writes each item one place after where it reads it. When glibc maps the source and the copy at the same offset within a page, Zen 4 stalls on 4K aliasing. +/¯3↕sv -⍝⍝: 2 2.8 4.5 instructions linux 3.063 allocated linux 16.244 +⍝⍝: 2 2.8 4.5 instructions macos 7.112 linux 3.063 allocated macos 16.246 linux 16.244 ⍝⍝⍝ held macos 2.13: open.md lists wide and 2-D window folds among the speed audit's outliers +/["size":3 "edge":"wrap"]↕sv -⍝⍝: 2 2.8 4.5 instructions linux 3.489 allocated linux 16.284 +⍝⍝: 2 2.8 4.5 instructions macos 7.785 linux 3.489 allocated macos 16.285 linux 16.284 ⍝ A copy's gradient reads its cotangent: a reversal's through the inverse map, windows' through a correlation that reads the cotangent's frames at each window position, and a sum of windows' through a sliding sum of the padded cotangent. ⍝ Expected: `{+/sv×⌽⍵}∂ sv` 2 copies: the primal's pass and a reversed read of the cotangent; `{+/, 3↕sv × 3↕⍵}∂ sv` 6 copies: the primal and a correlation of the cotangent, each a few sliding passes; `{+/,+/50↕⍵}∂ sv` nothing: its gradient depends on neither `⍵` nor any other array, so its trace holds it as one constant; `{+/⊼+/50↕⍵}∂ sv` 5.5 copies: the primal's running sum, about 1, its squared sum, about 0.5, the cotangent of the window sums, about 1, and a sliding sum of the padded cotangent, about 3. ⍝ CI measured 1.84 to 1.96 on Linux and 2.05 to 2.3 in wasm in most runs from 56 to 62. ⍝⍝⍝ route trace {+/sv×⌽⍵}∂ sv -⍝⍝: 4 5.5 5.2 instructions linux 2.528 allocated linux 16.081 +⍝⍝: 4 5.5 5.2 instructions macos 4.102 linux 2.528 allocated macos 16.082 linux 16.081 ⍝ CI measured 13.2 to 15.8 on Linux and 12.3 to 13.9 in wasm in most runs from 56 to 62. ⍝⍝⍝ held macos 13.1: the cause isn't known yet; busy hosts reach the Linux and wasm costs {+/, 3↕sv × 3↕⍵}∂ sv -⍝⍝: 12 33 32 instructions linux 25.266 allocated linux 88.131 +⍝⍝: 12 33 32 instructions macos 26.782 linux 25.266 allocated macos 88.132 linux 88.131 ⍝ `{+/,+/50↕⍵}∂ sv` checks that a linear function's gradient becomes a constant. Its gradient depends on neither `⍵` nor any other array, so its trace holds it as one constant, `v0 ← held [1 2 … 50 50 …]`, and each call returns it, at about 0.004 copies on every platform. Its budgets are the 0.5 floor. A trace that computed the gradient on every call would cost several copies and fail them. {+/,+/50↕⍵}∂ sv -⍝⍝: 0.5 0.5 0.5 instructions linux 0.199 allocated linux 0.016 +⍝⍝: 0.5 0.5 0.5 instructions macos 0.368 linux 0.199 allocated macos 0.016 linux 0.016 ⍝ On 29 October 2026 this case cost 10.2 on macOS, spelled then with `⊼`. Since 30 October, `²` under `∂` costs what `⊼` does. CI hasn't timed it. Its Linux and wasm estimates scale its macOS cost by `+/50↕sv`'s highest ratios to macOS in runs 56 to 62, 3.6 on Linux and 4.9 in wasm. ⍝⍝⍝ held macos 10.2: its window sums, forward and transposed, run at the cost of `+/50↕sv`, which open.md lists among wide window folds ⍝⍝⍝ gap forced "{+/(+/50↕⍵)²}∂": the transpose's sliding sum should read the padded cotangent in place, as a fold of padded windows does. The sum waits and runs when `∂`'s code reads it, so this call computes the padded cotangent ⍝⍝⍝ route trace {+/(+/50↕⍵)²}∂ sv -⍝⍝: 11 55 84 instructions linux 25.159 allocated linux 64.929 +⍝⍝: 11 55 84 instructions macos 40.249 linux 25.159 allocated macos 64.931 linux 64.929 ⍝ A product of windows reads them a block of rows at a time, straight into the matrix product, and never builds the windows matrix. A product of a single-channel image's windows with one filter adds each column of the windows, read where it lies, into the result. Its gradient for the kernel reads them again in blocks. Its gradient for the image is one product of the windows of the zero-padded cotangent with the kernel reversed. ⍝ Expected: `[64516 144]⍴3 3↕cimg +.× ck` 19 copies on macOS, 71 on Linux and 142 in wasm: a product of 595 million flops, 567 an item, at Accelerate's rank-k rate, at 8 in a copy's time for rustymath's own kernels and at 4 in wasm. Gathering the windows adds nothing: on 30 October 2026 the case cost 24.5 on macOS and 92 with rustymath's own kernels, against 24.8 and 92 for a product from the stored windows matrix. `{+/, [64516 144]⍴3 3↕cimg +.× ⍵}∂ ck` costs the same, because the function is linear in the kernel: `∂`'s program runs no primal, only one product, of the transposed windows with the broadcast seed. `{+/, [64516 144]⍴3 3↕⍵ +.× ck}∂ cimg` 27 copies on macOS, 106 on Linux and 212 in wasm, because the function is linear in the image: its program runs one product, of 604 million flops, between the windows of the zero-padded cotangent and the kernel reversed. That product has its own shape, 65,536 rows of 288 items times 16 columns: from a stored windows matrix it cost 27 on macOS and 106 with rustymath's own kernels on 30 October 2026, where the forward product's rate gives 19 and 72. Linux and wasm scale 72 and 144 by that 106 against 72. ⍝ CI measured 70 to 74 on Linux in runs 56 and 59-61. ⍝⍝⍝ route mapped product [conv_rows 144]⍴3 3↕cimg +.× ck -⍝⍝: 38 86 130 instructions linux 200.807 allocated linux 56.234 +⍝⍝: 38 86 130 instructions macos 56.079 linux 200.807 allocated macos 47.586 linux 56.234 ⍝ 3.25 copies, for 9 multiply-adds per window (2.25), one read and one write. On macOS, 5.5 copies, because NEON's two lanes of f64 take twice the multiply-adds' time (4.5). `([1044484 9]⍴3 3↕sq)+.×kq` measured 7.49 on macOS on 22 October 2026, until rustymath's `by_columns` kept a strip of 8 results in registers while all nine columns add into it. On 27 October 2026 rustymath widened the strip to 32 results (`COLUMN_STRIP`), which took it from 6.11 to 5.23 in alternating runs. Strips of 8 for the 30 results that strips of 32 leave in each 1,022-wide row, with each result written rather than added to the zeroed output, brought it to 4.35 to 4.49 on 28 October 2026. On 22 October 2026 CI measured 5.36 on Linux and 5.85 in wasm. In runs 56 to 62 it measured 3.38 to 3.63 on Linux, and 4.98 in run 62, whose host was busy, because its data passes the runner's 1 MB L2. ⍝ In runs 56 to 62 CI measured 8.61 to 9.69 in wasm, where it measured 5.85 on 22 October 2026. Its expected cost there is 5.5, as on macOS, since wasm's SIMD also has two lanes of f64. ⍝⍝⍝ route mapped product [window_rows 9]⍴3 3↕sq +.× kq -⍝⍝: 11 4.3 7.5 instructions linux 12.626 allocated linux 8.157 +⍝⍝: 11 4.3 7.5 instructions macos 17.558 linux 12.626 allocated macos 8.157 linux 8.157 ⍝ CI measured 80 to 87 on Linux in runs 56 and 59-61. ⍝⍝⍝ route mapped product {+/, [conv_rows 144]⍴3 3↕cimg +.× ⍵}∂ ck -⍝⍝: 38 100 140 instructions linux 238.533 allocated linux 119.276 +⍝⍝: 38 100 140 instructions macos 48.647 linux 238.533 allocated macos 84.403 linux 119.276 ⍝ On 30 October 2026 it cost 48.9 on macOS and 125 with rustymath's own kernels, against 27 and 106 for its product from a stored matrix, while its program built the 144 MiB windows matrix of the padded cotangent whole. CI measured 190 on Linux in run 61. Before run 61, whose speed test first asked for huge pages through the memory guard, it measured 443 to 470 there. In wasm it measured 204 to 276 in runs 56 to 62. Later on 30 October 2026, with the program's copy of those windows left pending and read through their map, it cost about 41.3 on macOS (6,173 to 5,218 ps an item). On 1 November 2026 the backward product's cotangent, a broadcast of 1, sums `ck`'s rows once and repeats the result instead of making a [64516 144] product (plan, gs13_'s entry): 7,246 → 5,914 ps an item. {+/, [conv_rows 144]⍴3 3↕⍵ +.× ck}∂ cimg -⍝⍝: 54 190 210 instructions linux 460.047 allocated linux 154.379 +⍝⍝: 54 190 210 instructions macos 112.876 linux 460.047 allocated macos 58.248 linux 154.379 ⍝ A transpose of a million floats is a pending copy. A product reads it where it lies, as a transposed matrix. A fused call reads it in blocks of whole rows, as many as a cache line holds items. ⍝ Expected: `⍉sq +.× vq` 0.5 copies: reads the matrix once; `2×⍉sq` 1.25 copies: one read and one write per item, through a blocked transpose. ⍝ On macOS, Accelerate's product reads the matrix about four times as fast as a copy on one core, at 0.11 copies, so its macOS expected cost is 0.12. ⍉sq +.× vq -⍝⍝: 0.5 0.99 1.4 instructions linux 2.154 allocated linux 0.311 +⍝⍝: 0.5 0.99 1.4 instructions macos 2.329 linux 2.154 allocated macos 0.249 linux 0.311 ⍝ On 27 October 2026 `2×⍉sq` measured 2.2 to 2.6 on macOS, with readings up to 3.5 while a video call ran. On 22 October 2026 CI measured 4.18 on Linux and 2.76 in wasm. ⍝⍝⍝ held macos 2.2: today's route transposes into a block buffer of 128 KB, which doesn't fit in L1, and reads each source line twice, so the transpose costs about 2 copies rather than 1.25 (open.md, "Tiled transpose"); Linux and wasm hold estimates that keep their budgets from before 22 October 2026, since CI hasn't measured it since 2×⍉sq -⍝⍝: 2.5 5 4.9 instructions linux 5.008 allocated linux 8.705 +⍝⍝: 2.5 5 4.9 instructions macos 5.279 linux 5.008 allocated macos 8.706 linux 8.705 ⍝ `[1 2 0]⍉cimg` moves `cimg`'s contiguous axis first, and its last two axes merge into one axis of 65,536 items a stride of 16 apart. A block stage would read each block as one strided run, so it gathers the whole copy once through the tiled transpose first (`Mapping::tiles_whole`). ⍝ Expected: 2.25 copies: the transpose into the gathered copy, 1.25 as for `2×⍉sq`, then one fused pass over it. On 27 October 2026 it measured 3.4 to 3.6 on macOS, against 7.5 without the gathered copy, and later that day 3.6 to 4.2, with readings up to 5.2 while a video call ran. Nearly all its cost is the same transpose as `2×⍉sq`'s, which CI measured at 4.18 on Linux and 2.76 in wasm, against 2.2 on macOS. Scaling 3.6 by those ratios gives estimates of about 6.9 on Linux and 4.5 in wasm. 2×[1 2 0]⍉cimg -⍝⍝: 4.5 7.9 6.8 instructions linux 5.087 allocated linux 16.253 +⍝⍝: 4.5 7.9 6.8 instructions macos 5.702 linux 5.087 allocated macos 16.254 linux 16.253 ⍝ `⍉⁻` splits and merges axes as einops does, and the result stays a view. `split` cuts 8 images of 224×224×3, 1,204,224 items, into 196 patches of 16×16×3, which reorders them. `rows` cuts each image's rows into 14 runs of 16 without reordering. ⍝ Expected: `+/,2×split⁝images` 0.9 copies: one fused pass over 1.15 times `n`, which reads each item once and writes nothing, about 0.6, and a block buffer that gathers the patches' runs of 48 items, about 0.3, at the 0.25 copies for each `n` items of `2×⌽sv+1`'s buffer; `+/,2×rows⁝images` 0.6 copies: the same pass, reading `images` in place. Wasm, twice the native cost, as `+/v`'s fold. ⍝ On 27 October 2026, with other work running, `+/,2×split(images)` measured 2.0 copies against 1.0 for `+/,2×images`, so its buffer cost about twice what `2×⌽sv+1`'s did in the same run. +/,2×split⁝images -⍝⍝: 1.8 2.1 4.2 instructions linux 4.796 allocated linux 12.215 +⍝⍝: 1.8 2.1 4.2 instructions macos 7.076 linux 4.796 allocated macos 12.213 linux 12.215 +/,2×rows⁝images -⍝⍝: 1.2 1.3 2 instructions linux 2.149 allocated linux 6.125 +⍝⍝: 1.2 1.3 2 instructions macos 3.668 linux 2.149 allocated macos 6.124 linux 6.125 ⍝ A select by a comparison runs as one program with the comparison and the cases: `0.5 setTimeout(resolve, pause * 1000)); warm(); for (const c of failures) console.log(`${line(c)}: over, timed again after ${pause} s`);