diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 00000000..873f1f5f --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,39 @@ +# `cargo nextest run` runs each test in its own process and every binary at once. The threads of one process take turns at its one session (`src/eval.rs`), so separate processes are what run tests in parallel. +[profile.default] +# A test running past 5 s is reported as slow. One still running at 30 s is stopped and fails. +slow-timeout = { period = "5s", terminate-after = 6 } +fail-fast = false + +[test-groups] +timed = { max-threads = 1 } + +# The reference runner starts one child process per core. It starts first and runs with nothing beside it, so no timing runs beside it. +[[profile.default.overrides]] +filter = 'binary(reference)' +threads-required = 'num-cpus' +priority = 100 + +# Memory-bound timings run with nothing beside them. +[[profile.default.overrides]] +filter = 'binary(speed) and test(=timed_images)' +threads-required = 'num-cpus' +test-group = 'timed' +priority = -100 + +# Other timings run one at a time, beside light correctness tests, after every other test has started. +[[profile.default.overrides]] +filter = 'binary(speed) and test(/^timed_/)' +test-group = 'timed' +priority = -100 + +# Optional selection excluding timings, used by focused cross-repo diagnostics. It gives reference one slot because this selection contains no timing tests. +[profile.untimed] +default-filter = 'not (binary(speed) and test(/^timed_/))' + +[[profile.untimed.overrides]] +filter = 'binary(reference)' +threads-required = 1 + +# Optional timing-only selection. An explicit cross-repo ('untimed', 'timed', 'speed') run uses this after correctness and before rustymath timings. Plain run_tests uses default and includes everything. +[profile.timed] +default-filter = 'binary(speed) and test(/^timed_/)' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a079e039..ae5db329 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,6 +17,7 @@ jobs: # `tests/highlighting.mjs` reads the TextMate grammar through `shiki`, which `package.json` pins. - run: npm ci - uses: dtolnay/rust-toolchain@stable + - uses: taiki-e/install-action@nextest # The repository has no `Cargo.lock`. Creating one first makes rust-cache's key change when a dependency publishes a new version. - run: cargo generate-lockfile - uses: Swatinem/rust-cache@v2 @@ -30,7 +31,7 @@ jobs: - run: node tests/input.mjs - run: node tests/highlighting.mjs if: ${{ !cancelled() }} - - run: cargo test --release + - run: cargo nextest run --cargo-profile release -E 'not binary(speed)' if: ${{ !cancelled() }} - run: pytest -q if: ${{ !cancelled() }} diff --git a/Cargo.toml b/Cargo.toml index 6711ceec..bd2443d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["cli", "py", "wasm", "xtask"] +members = ["cli", "image", "jit", "py", "testkit", "wasm", "xtask"] default-members = [".", "cli", "py"] [workspace.metadata.fastfmt] @@ -12,14 +12,16 @@ rust-version = "1.98" license = "Apache-2.0" [workspace.dependencies] -rustymath = { version = ">=0.1.55, <1", default-features = false } +rustymath = { version = ">=0.1.56, <1", default-features = false } rustymath-buffer = ">=0.1.55, <1" +rustymath-timing = ">=0.1.56, <1" pyo3 = ">=0.29.2, <1" pyo3-build-config = ">=0.29.2, <1" ctrlc = ">=3.5.2, <4" kernmini = ">=0.1.22, <1" # Custom bindings override Emacs Alt-digit prefixes in this fork. rustyline = { git = "https://github.com/AnswerDotAI/rustyline.git", branch = "master", default-features = false, features = ["custom-bindings", "with-file-history"] } +flate2 = { version = ">=1.1.10, <2", default-features = false, features = ["zlib"] } [package] name = "basedpl" @@ -34,11 +36,6 @@ doctest = false # Without a unit-test build, `cargo test` compiles the library only once. test = false -# Plain `cargo test` skips the timing suite; `cargo test --test speed` runs it. -[[test]] -name = "speed" -test = false - [profile.test] inherits = "release" @@ -52,6 +49,10 @@ incremental = true # Line tables change with every shifted line and recompile the code below it. debug = false +[profile.quick] +inherits = "release" +lto = "off" + [profile.dist] inherits = "release" lto = "thin" @@ -76,11 +77,12 @@ serde = ">=1.0.229, <2" serde_json = { version = "1", features = ["arbitrary_precision", "preserve_order"] } unicode-width = { version = ">=0.2.2, <1", default-features = false } num-rational = ">=0.4.2, <1" -num-complex = { version = ">=0.4.6, <1", features = ["bytemuck"] } +num-complex = ">=0.4.6, <1" num-bigint = ">=0.4.6, <0.5" num-traits = ">=0.2.19, <1" -half = { version = ">=2.7.1, <3", features = ["num-traits", "bytemuck"] } +half = { version = ">=2.7.1, <3", features = ["num-traits"] } rustymath.workspace = true +basedpl-image = { path = "image" } strength_reduce = ">=0.2.4, <1" rand = { version = ">=0.10.2, <1", features = ["chacha"] } # AnswerDotAI's fork measures text with a canvas, which works in a Web Worker: plotters-rs/plotters#746. @@ -101,13 +103,10 @@ nix = { version = ">=0.31.3, <1", default-features = false, features = ["user"] [target.'cfg(target_vendor = "apple")'.dependencies] # The Mac build links the zlib that macOS ships, in place of compiling `miniz_oxide`. -flate2 = { version = ">=1.1.10, <2", default-features = false, features = ["zlib"] } +flate2.workspace = true [target.'cfg(not(target_vendor = "apple"))'.dependencies] flate2 = ">=1.1.10, <2" -# Macs decode and encode images through ImageIO instead. -image = { version = ">=0.25.6, <1", default-features = false, features = ["png", "jpeg"] } -png = ">=0.18.1, <1" [target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies] chrono = { version = ">=0.4.45, <0.5", features = ["unstable-locales"] } @@ -115,12 +114,7 @@ icu_normalizer = ">=2, <3" fancy-regex = "0.20" walkdir = ">=2.5.0, <3" globset = ">=0.4.20, <1" -# Cranelift compiles a typed program's loops while BPL runs. The browser build has no JIT. -cranelift-codegen = { version = "0.137", default-features = false, features = ["std", "host-arch"] } -cranelift-frontend = "0.137" -cranelift-jit = "0.137" -cranelift-module = "0.137" -cranelift-native = "0.137" +basedpl-jit = { path = "jit" } # The browser build. `build.rs` sets `cfg(web)` for the same target. [target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] @@ -133,5 +127,9 @@ wasm-bindgen = ">=0.2.129, <0.3" # `eval::room` reads the stack pointer for the room left on the browser's main stack, where stacker reports none. psm = ">=0.1.32, <0.2" +[dev-dependencies] +basedpl-testkit = { path = "testkit" } +rustymath-timing.workspace = true + [build-dependencies] serde_json = "1" diff --git a/DEV.md b/DEV.md index 0449e485..0a27e558 100644 --- a/DEV.md +++ b/DEV.md @@ -20,6 +20,8 @@ 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. +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. 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: @@ -66,11 +68,12 @@ 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. -- `cargo test --release`: correctness checks while working; one test binary for focused checks. Bare `cargo test` builds the `test` profile, which has `release`'s settings but compiles everything again into `target/debug/`. Both skip the timing suite (`[[test]] speed`, `test = false` in `Cargo.toml`): `BASEDPL_COSTS=1 cargo test --release --test speed` runs it, logging every cost. Kernel: `fastws.cargo.run_tests(root, *cargo_options, test_args=(), timeout=30)` defaults to `release`, builds native test executables once, with no build limit, then runs each executable directly within its own `timeout`. A binary that runs out of time is killed and counts as a failed binary, with its suite's `timed_out` set. Filters and libtest flags go in `test_args`. Results describe that compiled build, not later working-tree edits. It accepts seconds or `None`, not `worker.Limit`; waits inside a test binary count. 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. +- 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. - Unix cancellation: one process-group owner per job (`fastws.procs.bounded`, or each standalone `prerelease` step). Nested commands inherit the marked job's group; let their timeout/error end the job so its owner cleans up descendants. Do not detach commands. `Worker` is a protocol transport, not a supervisor: it inherits its caller's group and `close()` stops only the worker. Standalone callers needing descendant cleanup put the call inside an owned job. -- Full checks, in the release's iteration stage and before a release (CI's, plus targets this Mac's daily builds skip): `cargo develop`, `cargo test --release`, `cargo test --release --test speed` (the timing suite), `pytest -q`, `bpl-nb`, `nbdev-test`, `node tests/input.mjs`, `node tests/highlighting.mjs`; then `bpl-tests target_builds /tmp/_wasm` (basedpl's and rustymath's Linux x86-64 libraries plus the browser package); then `wasm_test(DIR)` and `wasm_test(DIR, speed=True)` on it. A build stuck on one core for minutes: see "Codegen and rebuilds". +- Full checks, in the release's iteration stage and before a release (CI's, plus targets this Mac's daily builds skip): `cargo develop`, the default test command (both repositories' tests, timings included), `pytest -q`, `bpl-nb`, `nbdev-test`, `node tests/input.mjs`, `node tests/highlighting.mjs`; then `bpl-tests target_builds /tmp/_wasm` (basedpl's and rustymath's Linux x86-64 libraries plus the browser package); then `wasm_test(DIR)` and `wasm_test(DIR, speed=True)` on it. A build stuck on one core for minutes: see "Codegen and rebuilds". Build locations and profiles follow the current integration process in `meta/process.md`; documented browser and x86 diagnostics below remain available. Do not use `maturin develop`: its configuration does not reuse the normal local build. The paired build worktrees isolate compiles from unfinished live edits; do not wait for the shared live tree to become quiet. - Running Python processes keep their loaded extension. Kernel: `from basedpl.worker import fresh; fresh(code)` checks the installed `bpl` in a fresh worker (no kernel restart); BPL errors raise `BplError`; `fresh(code, build=BASE)` runs another build (baseline: below); a list runs each expression in its own worker, one row each (output or error); `prefs=` runs first in each worker: `fresh(cases, prefs='•prefs ["kinds":"static"]')`. Check one expression and read its result before building on it. - `fresh`, `time_cases`, and `check_file`/`check_page` failures show each input's linear form (`basedpl.syntax.linear`): its grouping, bracketed. `basedpl.bpltests.check_file`, `check_page`: check reference files and docs through the worker, no Rust build; `build=` checks another build without installing it. @@ -81,22 +84,22 @@ Main reviews coherent milestones against the plan and `owners.md`: architectural Builds and integration follow the integrator process in the required section above. Source owners keep writing useful independent work while the integrator builds an exact paired snapshot. Compile failures may justify small compiler-driven corrections; a substantive interface issue goes back to the connected owner rather than growing an adapter. Release 1's 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`. -Costs in copies: `BASEDPL_COSTS=1 cargo test --release --test speed`, `basedpl.bpltests.time_cases(cases)`, or `time_cases(cases, section='models.bpl:static')` with a speed file's setup and a section's preferences. A copy: time to write the data's item count of negated floats to a second buffer. +Costs in copies: `BASEDPL_COSTS=1 cargo nextest run --cargo-profile release --test speed --no-capture` (calibration: every cost and count), `basedpl.bpltests.time_cases(cases)`, or `time_cases(cases, section='models.bpl:static')` with a speed file's setup and a section's preferences. A copy: time to write the data's item count of negated floats to a second buffer. Timings use the installed build, or a kept build passed through `build=`. -- `rustymath::timing::COPIES`: stored copy times per speed-test machine, by CPU, item size and item count; the slow end of normal; for copies whose source plus destination fit in cache, 64 MiB (`CACHED_BYTES`). Stored because a 65,536-item copy timed beside each case varied over 3× on CI's runner (source in L3 shared with other VMs) while functions varied at most 1.18×. Larger copies run at memory speed, varying up to 2× between runs on a shared runner, as do those cases: measured once per run. A cached size with no entry: measured once, printing a line to add to `COPIES`. Browser build: own entries (copy under the JavaScript engine); it can't read the CPU name, so `tests/wasm-pkg.mjs` passes Node's. +- `rustymath_timing::COPIES`: stored copy times per speed-test machine, by CPU, item size and item count; the slow end of normal; for copies whose source plus destination fit in cache, 64 MiB (`CACHED_BYTES`). Stored because a 65,536-item copy timed beside each case varied over 3× on CI's runner (source in L3 shared with other VMs) while functions varied at most 1.18×. Larger copies run at memory speed, varying up to 2× between runs on a shared runner, as do those cases: measured once per run. A cached size with no entry: measured once, printing a line to add to `COPIES`. Browser build: own entries (copy under the JavaScript engine); it can't read the CPU name, so `tests/wasm-pkg.mjs` passes Node's. - Logs: each case's line ends with its time per item (`logged_costs`: per row); one copy line per data size (`copies`). -- Footprint: data plus peak bytes held (`rustymath::memory::peak_held`). Over 1 MiB (`rustymath::timing::CORE_CACHE_BYTES`, one core's L2 on CI's runner), a case also times a copy of its footprint beside it, appending footprint and that copy's time per item to its line (`logged_costs`: `footprints`). Past L2, data reaches L3 and memory shared with other VMs: cases from about 2 MB swung up to 4× between runs, a different few each run; up to 1.5 MB held. Costs still divide by the data's copy; the footprint copy shows whether a case's time follows it. +- Footprint: data plus peak bytes held (`rustymath::memory::peak_held`). Over 1 MiB (`rustymath_timing::CORE_CACHE_BYTES`, one core's L2 on CI's runner), a calibration run, or the retime of a case over budget, also times a copy of the case's footprint beside it, appending footprint and that copy's time per item to its line (`logged_costs`: `footprints`). Past L2, data reaches L3 and memory shared with other VMs: cases from about 2 MB swung up to 4× between runs, a different few each run; up to 1.5 MB held. Costs still divide by the data's copy; the footprint copy shows whether a case's time follows it. - Budgets are single-thread. `thread_scaling()` checks threads: a large product, 512×512 SVD and Hermitian eigendecomposition, a stack of QR decompositions, a product of a convolution's windows, a copy of those windows, a large elementwise function, each on 1 and t = min(4, physical cores) threads. Serial fraction s (share of one-thread time that doesn't split) gives expected speedup S = 1/(s + (1 − s)/t) (Amdahl). s: first principles for our kernels; measured for Accelerate on macOS (product, SVD, eigendecomposition, product of windows) and for the copy of windows (memory bandwidth limits copies). The product of windows fails if its rows stop splitting across threads; the copy of windows if a copy's items stop splitting. Failure: below 1 + (S − 1)/2 (2.5 on 4 cores, 1.5 on 2, for full splitting), or below 0.9 for s = 1. Accelerate's SVD and eigendecomposition: s = 1 (GESDD, SYEVD run on one thread whatever its threading setting). Run `thread_scaling()` on the authoritative host. Linux counts one core per set of hardware threads sharing a core; a VM not reporting the sets counts each vCPU. - `speed_costs()`: cases in `tests/speed/*.bpl`; `cases=` expressions; `wasm=DIR` browser build; `build=` another build (e.g. a kept build's id); `native=True` times rustymath's own linear algebra, which dev builds compile in; `prefs=` BPL run before each section header (`•prefs ["kinds":"static"]`). `logged_costs(log)`: timing output, text or file. `cargo_costs(path)`: either project's Rust speed test; `build=EXE` a saved copy of its executable. `pytest_costs(path)`: rustymath's Python speed test. -Instruction counts: code layout alone moves timings more than a few percent, hiding per-call changes in copies; `instructions(cases)` counts instructions retired, which load and layout don't move. `instructions`, `profile` and `call_profile` run loops as ordinary code (`BASEDPL_LOOPS=ordinary`): counts measure a call's interpretation, which a case's own `⍣N` loop would otherwise replay. `replays=True` lets loops replay their recordings, as real runs do: a case that names `N`, such as `{0.5×⍵+⌽⍵-(+/⍵)÷4}⍣N 1 2 3 4`, then counts its loop's replayed steps (22,473 instructions a step, against 26,825 as ordinary code, on 1 November 2026). +Instruction counts: code layout alone moves timings more than a few percent, hiding per-call changes in copies; instruction counts don't move with load or layout. The speed tests count every speed case (below); `instructions(cases)` counts any expression in fresh processes. Both count loops as real runs make them. - 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. -- `tests/instructions.txt`: instructions per step per case, macOS on Apple silicon. Most cases make one call per step (`{⍵}⍣N 0` to `fib`); three map over the same 64 cells each step via Each, Rank's general path, and a system function's item map. `instruction_budgets()` counts each case through `instructions`: `over` if more than 3% above its count, `stale` if more than 3% below; `tests/test_instructions.py` fails on either, skips other platforms. In the change that makes a call cheaper, lower each count it moves. A new compiler version can move counts by inlining differently: then set them again from `bpl-tests instruction_budgets`. CI runs no performance-budget suites. +- 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. - `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. @@ -106,7 +109,7 @@ Syd1 selects the installed perf 6.11.11 through `~/.local/bin/perf`; its HWE too 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`, `instruction_budgets`, `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`). +`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. Syd1 is authoritative for x86 performance budgets; Jeremy's laptop is authoritative for macOS/ARM. Run the existing speed suites and `thread_scaling()` on those hosts, outside CI. `bpl --costs PATH` times a speed file or directory and logs costs without failing budgets. `logged_costs` reads those logs and their CPU/copy records. Timings warm recording and compilation before measurement; recurring dispatch/replay work remains included. Do not subtract a universal fixed-call constant. Compare baseline and candidate on the same host before interpreting a change; the old CI thresholds are historical reference values until reviewed. @@ -115,31 +118,32 @@ Reviewing a run: find costs over budget and costs changed more than 20% since th Budgets: - Budget = expected cost × multiplier: 2 under 50 copies, 1.5 for 50-500, 1.3 over 500 (cheap cases vary most). Expected cost: first-principles analysis of what the case should cost; a later one comes from fuller understanding (a platform difference, a missed cost), recorded in the case's comment. The comment above each case or group gives the expected cost and reason, and any platform's own cost and reason. One expected cost serves every platform until a specific platform difference is found (e.g. macOS runs matrix products and decompositions on Accelerate, Syd1 on rustymath's kernels). -- A BPL speed file's `⍝⍝:` footer: rule budgets (expected × multiplier) for macOS on Apple silicon, Linux on Syd1, then (basedpl only) wasm under Node on Syd1. +- A BPL speed file's `⍝⍝:` footer: rule budgets (expected × multiplier) for macOS on Apple silicon, Linux on Syd1, then (basedpl only) wasm under Node on Syd1, with `-` for no budget; then its counts (Instruction counts, above). - A case over its rule budget gets a directive holding the column: `⍝⍝⍝ held linux 15.6 wasm 18.9: REASON` (a code gap; each held column with its held cost) or `⍝⍝⍝ busy linux 2.7: REASON` (data past the 1 MiB footprint threshold, which busy hosts slow in some runs). The multiplier applies to the held cost; the case's line shows both (`budget 32 held from 16`). A gap's hold goes stale at 0.9× its rule budget or less (inside the 10% margin a review holds a column at); runs print it `stale`, and its `held` line can go. Busy holds never go stale. - A case within 10% of its budget on a checking platform gets a dated `held` line raising that column 10% as a margin against timing noise: held cost = the one whose budget, rounded up as `budget` rounds, is at least 10% higher (`⍝⍝⍝ held wasm 17.6`: 32 → 36); stale by the 0.9× rule. - A platform cost no run has measured (a Linux or wasm estimate) keeps twice the estimate until the authoritative host measures it, marked `~` in held lines (`⍝⍝⍝ held linux ~107: REASON`). Rustymath's tables still sit at twice expected cost (old rule). -- `rustymath::timing`: the multiplier (`budget`); reads (`hold`) and applies (`Budget`) these lines for every speed test. Rustymath's budget files write them as `# held ...`, `# busy ...` above a case's line. +- `rustymath_timing`: the multiplier (`budget`); reads (`hold`) and applies (`Budget`) these lines for every speed test. Rustymath's budget files write them as `# held ...`, `# busy ...` above a case's line. - Over budget: study it, finding a speed bug or something the analysis missed. Avoid raising a budget to match a run without knowing why it costs that. -- `rustymath::timing::BUDGET_CPU` selects the development Mac for macOS/ARM and Syd1 for Linux/x86 and wasm. Only that CPU fails a speed test on costs still over budget after retries. Other CPUs print `over` without failing. Existing Linux/wasm thresholds came from the old CI machine and need review against matched Syd1 measurements; changing the authoritative host does not approve new thresholds. +- `rustymath_timing::BUDGET_CPU` selects the development Mac for macOS/ARM and Syd1 for Linux/x86 and wasm. Only that CPU fails a speed test on costs still over budget after retries. Other CPUs print `over` without failing. Existing Linux/wasm thresholds came from the old CI machine and need review against matched Syd1 measurements; changing the authoritative host does not approve new thresholds. - `set_budgets(expected)` applies the multiplier and writes rule budgets for `expected`'s cases (dict: case code → expected cost), leaving `held` and `busy` lines. `builds=`: one platform's column; `estimate=True`: twice the cost, for estimates; `sections=`: named sections only (a case can be in several, e.g. `copies`, `widths`); `path=`: a BPL speed file or directory, or rustymath's `tests/speed.txt`, `tests/speed_python.txt`. +- `set_budgets` keeps a footer's count fields. `set_counts(log, path=, platform=)` writes the counts that a `BASEDPL_COSTS` log gives (`logged_counts`) into `platform`'s column, this machine's by default, keeping the time budgets and other columns. It takes the same paths. Routes: -- `tests/explain.rs`'s `speed_cases_take_their_routes` captures every speed case after warm-up at normal setup and size, forcing results outside timing (`timing::routes`), then each reference case with route directives once under ordinary preferences. Plain `cargo test` runs it. `check_file` checks a file's route directives likewise, on one thread. Route checks decide which calls wait with the Mac's costs on every platform (`fusion::Waits::Reference`, set in `timing::route`), so a directive observed on the Mac holds on CI; real runs and timings use each platform's own costs. `⍝⍝⍝ route NAME` above a case requires that fast route. A permitted slow event never excuses a missing required route. +- `tests/reference.rs`'s `reference_cases` checks routes through `reference::route_check`: each case with route directives once under ordinary preferences, and each timed speed case after warm-up with small fixture dimensions under `Sizes::SMALL`, forcing results outside timing. It prints each route and kind of event in rustymath's inventory (`events::Route`) that no routed case exercises. `check_file` checks a file's route directives likewise, on one thread. `⍝⍝⍝ route NAME` above a case requires that fast route. A permitted slow event never excuses a missing required route. +- Speed setups write scalable dimensions as `full size small`. The timing session supplies dyadic `size` as `⊣` for full-size timings, `⊢` for small route checks, and for counts the power of two nearest the pair's geometric mean (`Size::Counted`), after leading preferences have set the parser's numeric kinds. Fixed feature and window dimensions stay fixed. `timing::costs` captures the two full-size warm-up runs through `Capture::violations`, then times uncaptured evaluations: the fastest of at least five that take 5 ms together, or of two once they take 20 ms (`rustymath_timing::fastest` with `LEAST`). A file's `widths` cases take their `exact` times together after its other cases, since each change of section reruns the setup. Native and WASM cost records retain and display route obligations. `Sizes::batch` owns the repeated-constant chunk bound: 2²¹ by default and 4096 under `SMALL`. - Unlisted slow events fail: `stored` (pending items computed outside a fused group), `forced` (a call computing pending items it reads as items; rows with reason `read as values`, an argument such as a count, or `an effect`, the pending arrays that an effect computes first, aren't slow), `fallbacks` (batched fallback), `mixed` (a write leaving mixed storage), `itemwise`, `stopped` (stopped trace). Name them as `•explain` does: function (`forced`), code (`mixed`), route (`itemwise`), reason (the rest). `⍝⍝⍝ allows RECORD "EVENT": principle` permits work (a matrix product reading its arguments whole); `⍝⍝⍝ gap RECORD "EVENT": what should happen` names a fast-path gap. A stopped trace takes `allows` only for a by-design stop (dynamic kinds' value-dependent types), else `gap`. `declined` (a considered route not taken) is informational; a `gap` or `allows` can name its route and reason (`fusion: an exact argument`). Every annotation must match an observed event; remove it when the event disappears. `cargo test -- --nocapture` prints remaining gaps. -- Native selectors (`reference`, `explain`, `speed`): `BASEDPL_SOURCE` matches a source name, with or without `.bpl`; `BASEDPL_SECTION` matches a section prefix; `BASEDPL_CASE` matches an ID or code prefix. A selector matching no cases fails. Filtering precedes setup and case execution. Explain stories use source `explain`, with sections `idioms`, `records`, `read-back`, `accumulation`, `reads`, `declines`, `stops`, `memory`. - `tests/skipped.txt`: full-size cases explicitly set aside, one per line: test name, tab, section (`*` for every section), tab, case key, tab, the open item that records why. Coverage and unit suites use the case's code as its key. Reference-file rows use `reference:.bpl`, the exact section and the existing case ID, or the first comment line when the ID is empty. `reference::mark_skipped` defines that key and rejects ambiguous or stale entries against the full file before selection. Rust checks and worker inventories share the matcher. `check_file` retains skipped identities, reasons and modes; skips are unresolved, not passes. Remove an entry when its case is ready to run again. -- Events: Rust in both repositories reports routes taken, declines and stopped traces via `rustymath::events`; basedpl records itemwise routes in `•explain`'s rows. Uncaptured reporting costs one atomic load. The route test compares observed successes, declines and fixed itemwise routes with the declared IDs (idiom cases and annotated references included); the inventory includes warm-up, route contracts only the warmed run. It checks event-name coverage, not every branch at an emission site. Study an unexpected event before annotating it, as for a case over budget. +- Events: Rust in both repositories reports routes taken, declines and stopped traces via `rustymath::events`; basedpl records itemwise routes in `•explain`'s rows. Uncaptured reporting costs one atomic load. `reference_cases` compares each routed case's and speed case's observed successes, declines and fixed itemwise routes with its directives; the inventory counts warm-up runs too, while directives check only the warmed run. It checks event-name coverage, not every branch at an emission site. Study an unexpected event before annotating it, as for a case over budget. -Retries: speed tests and `thread_scaling` retime each failing case after the rest, after each pause in `rustymath::timing::RETRY_PAUSES` (0.1, 1, 10 s; 10 s outlasts longer load bursts from other VMs on CI's runner), failing only cases that fail every time. Before each retry, speed tests and `bpl --costs` keep the thread busy 0.1 s (`rustymath::timing::warm`): idle cores start slow (on this Mac, small cases timed straight after the 10 s pause cost up to 3× without it). Retries print old and new costs (speedups in `thread_scaling`), even in passing runs; `logged_costs` keeps a retried case's lowest. Study a case that needs retries in most runs, as a slowdown. +Retries: an ordinary speed run times under a shared hold of the timing lock on one thread, then retimes each case over budget once under the exclusive hold where no build holds the lock (`rustymath_timing::try_exclusive`), failing only cases still over. Where builds hold the lock, it prints those cases `unconfirmed` and fails none. Calibration runs (`BASEDPL_COSTS`) and `thread_scaling` hold the lock exclusively and retime each failing case after each pause in `rustymath_timing::RETRY_PAUSES` (0.1, 1, 10 s; 10 s outlasts longer load bursts from other VMs on CI's runner), failing only cases that fail every time. Before each retry, speed tests and `bpl --costs` keep the thread busy 0.1 s (`rustymath_timing::warm`): idle cores start slow (on this Mac, small cases timed straight after the 10 s pause cost up to 3× without it). Retries print old and new costs (speedups in `thread_scaling`), even in passing runs; `logged_costs` keeps a retried case's lowest. Study a case that needs retries in most runs, as a slowdown. Before and after: for a change meant to speed something up, or to code every call goes through, time affected cases before and after. Other changes need only the speed test, which fails only past budget, missing smaller slowdowns. -- `cargo develop` keeps each installed build read-only under an id hashing its inputs and files, installing by repointing one link (`fastws.store`). `fastws.store.builds(root)`: the newest builds of the project at `root`, as records: id, when and by whom kept, input files changed since the previous build, inputs possibly modified since build started (mtime diagnostics, not proof of what the compiler read), `*` on the installed one. `bisect(case, good, bad, by=)`: the adjacent kept builds where a case's verdict changes, by `check` (a reference case passes), `value` (an expression prints what it prints on `good`), `instructions` or `time` (cost nearer `good`'s than `bad`'s), with the input files changed between them and every session's journal edits to those files (a shell has no journal, and `bisect` says so); `explain_diff(case, base)`: how `•explain`'s records of `case` differ between two builds, each captured as the route test captures it (the worker's `route` request, `timing::warmed`), with listing diffs; a `good` newer than `bad` finds a fix. Note the installed id before changing anything. Don't restore old code in the shared tree for a baseline. +- `cargo develop` keeps each installed build read-only under an id hashing its inputs and files, installing by repointing one link (`fastws.store`). `fastws.store.builds(root)`: the newest builds of the project at `root`, as records: id, when and by whom kept, input files changed since the previous build, inputs possibly modified since build started (mtime diagnostics, not proof of what the compiler read), `*` on the installed one. `bisect(case, good, bad, by=)`: the adjacent kept builds where a case's verdict changes, by `check` (a reference case passes), `value` (an expression prints what it prints on `good`), `instructions` or `time` (cost nearer `good`'s than `bad`'s), with the input files changed between them and every session's journal edits to those files (a shell has no journal, and `bisect` says so); `explain_diff(case, base)`: how `•explain`'s records of `case` differ between two builds, each captured as `reference_cases` captures it (the worker's `route` request, `timing::warmed`), with listing diffs; a `good` newer than `bad` finds a fix. Note the installed id before changing anything. Don't restore old code in the shared tree for a baseline. - Every timing tool's `build=`: a kept build's id prefix, a `bpl` path, or `None` (installed; `installed` from the shell), resolved once at start, so another agent's `cargo develop` can't change a run. Time both builds in one call: kernel `time_cases(cases, build=[base, None])` (`base`: the noted id); shell `bpl-tests time_cases CASE --build BASE NEW`. Builds run in fresh workers taking turns in alternating order, `rounds` times; the result holds each case's fastest time per item per build, in picoseconds. `compare_costs(*result)` lists cases that moved. -- `compile=` and `defer=` on `fresh`, `time_cases`, `instructions`, `speed_costs` and `cargo_costs` set the environment of the processes they start, with `native=` (`worker.environment`, which each takes through `@delegates`). `compile=` sets `BASEDPL_COMPILE`: `always` compiles every block stage on its first run, `never` compiles none, None keeps the compile policy (`settings::Compiles`). `defer=` sets `BASEDPL_DEFER`: `all` makes every call wait, as fusion at any size runs; `reference` takes the Mac's costs; None, this platform's (`settings::waits`). Shell: `--compile never`, `--defer all`. -- Timing sessions, and the processes `instructions`, `profile` and `call_profile` start, seed random generators at 0: every process and build draws the same data. Builds from before a `rustymath::timing::COPIES` change divide by a different copy. +- The shared `native=` and `compile=` options come from `worker.environment`, which `fresh`, `time_cases`, `instructions`, `speed_costs` and `cargo_costs` take through `@delegates`. `compile=` sets `BASEDPL_COMPILE`: `always` compiles every block stage on its first run, `never` compiles none, and None keeps the compile policy (`settings::Compiles`). Shell: `--compile never`. For a controlled deferral experiment, set `BASEDPL_MIN_ITEMS` in the worker's environment before starting it: 1 lowers the result-count threshold to every nonempty fusible call; a value above the workload size keeps those calls eager. There is no `defer=` helper option. +- Timing sessions, and the processes `instructions`, `profile` and `call_profile` start, seed random generators at 0: every process and build draws the same data. Builds from before a `rustymath_timing::COPIES` change divide by a different copy. - On this Mac a fused case on a large input can cost up to about 15% more in one process than another (depending on whether `malloc` starts the input on a 32 KB boundary, where every block of a block stage then ends): retime with more `rounds` before calling a lone difference that size a regression. - `pytest_costs`, `run_pytest`: `build=` takes a rustymath build id, for rustymath Python API changes. macOS native timings of matrix products and decompositions mostly measure Accelerate; rustymath's DEV.md says how to time its own kernels. - Compare static with dynamic kinds, or programs with the interpreter, only on code written for static kinds, with explicit sizes on literals and data: dynamic-kinds code can be wrong under static kinds, and its timings don't show static cost. @@ -150,17 +154,23 @@ Timing lock `/tmp/aai-ws-timing.lock`: - Exclusive: timings. Shared: every rustc call (via `.cargo/rustc-lock`, the wrapper `ws-sync` writes into the workspace's Cargo config, so builds wait behind timings however started); reference and explain tests, pytest, `check_file`, `check_page`, `bpl-nb`, `instructions`, `profile`, `wasm_test` without `speed`. - `/tmp/aai-ws-timing.gate`: held by a waiting timing; shared holds pass it first → no new build or test starts while a timing waits; the timing starts once current holds end. -- Rust takes it via `rustymath::timing`, Python via `fastws.timelock`. A kernel's `time_cases` or native `speed_costs`: the `bpl` worker takes it, not the kernel. Node can't lock files: `wasm_test` and `speed_costs(wasm=DIR)` hold it in Python while Node runs. +- Rust takes it via `rustymath_timing`, Python via `fastws.timelock`. A kernel's `time_cases` or native `speed_costs`: the `bpl` worker takes it, not the kernel. Node can't lock files: `wasm_test` and `speed_costs(wasm=DIR)` hold it in Python while Node runs. - Every acquisition fails after 5 minutes rather than proceeding unlocked. After 30 s an exclusive wait prints processes with the lock file open (possibly mere waiters). Same-thread shared nesting reuses its hold; any nesting involving an exclusive hold is rejected immediately. - A process holding the lock shared must not start a build or timing: it would wait behind a timing waiting on that hold, for the full 5 minutes. Never take the lock yourself in a kernel process. Never coordinate timings by hand: the lock schedules builds and timings. A hold outlasting its call blocks timings later calls launch. - Native timings: one thread, user-interactive macOS QoS (favours performance cores; not CPU affinity). -Browser: during development, run `cargo wasm` only for browser work. Agents use `cargo wasm --out DIR` (not replacing a module another agent uses), then `basedpl.bpltests.wasm_test(DIR)` (`tests/wasm.mjs`) and `wasm_test(DIR, speed=True)` (`tests/wasm-speed.mjs`; on this Mac it prints every cost and marks cases over wasm budget without failing: wasm budget authority is Syd1). Other edits under `cfg(web)` or `host_web.rs`: check with `cargo cs -p basedpl-wasm --target wasm32-unknown-unknown` (browser wrapper crate plus basedpl's browser code); plain `cargo cs` compiles neither. +Browser: during development, run `cargo wasm` only for browser work. Agents use `cargo wasm --out DIR` (not replacing a module another agent uses), then the report-printing commands `basedpl.bpltests.wasm_test(DIR)` (`tests/wasm.mjs`) and `wasm_test(DIR, speed=True)` (`tests/wasm-speed.mjs`; on this Mac it prints every cost and marks cases over wasm budget without failing: wasm budget authority is Syd1). Other edits under `cfg(web)` or `host_web.rs`: check with `cargo cs -p basedpl-wasm --target wasm32-unknown-unknown` (browser wrapper crate plus basedpl's browser code); plain `cargo cs` compiles neither. ### Machine code `bpl-tests disasm PATTERN`: machine code (`objdump`, installed `bpl`) of the function whose demangled name matches regex `PATTERN`; `--build BUILD`: another build; several matches → lists names. Always-inlined functions have no code of their own: read their callers. +`prerelease` reruns the existing `arrays.bpl:copies` cases `-v`, `v+w` and `v×2` with compilation disabled, against their existing budgets. This checks native execution without JIT-generated arithmetic. It uses the same build and speed-test executable as the ordinary speed checks. + +For an uncompiled slowdown, measure the affected existing cases with `compile="never"` on the pinned normal development build, then profile that build. Follow the complete emitted item-loop call chain with `disasm`. A formula can inline while a nested helper remains a per-item call. Even tiny non-generic helpers can hide constant branches across `rustymath-core`/`rustymath-kernels`: trig's `Kind::{scale,offset}` did this after its formula and polynomial helpers had inlined. Put an ordinary `#[inline]` hint on the shared helper's definition, then check the emitted loop and existing row costs again in the normal profile. Keep rare repair routines out of line. Don't add blanket `inline(always)` hints or rely on shipping LTO to remove the calls. + +A high copy count does not by itself show extra memory passes. Compare raw ns/item and a one-read/one-write control at nearby sizes in the same execution mode. Addition touches three arrays; a negating copy touches two. They can cross cache tiers at different sizes. [The addition diagnosis](meta/elementwise-add-cost.md) records a controlled example. + JIT kernels exist only while a process runs. `BASEDPL_JIT_DISASM` set → `bpl` prints each kernel it compiles to stderr: a description line, then machine code. `bpl-tests jit_disasm CASE --setup SETUP --prefs PREFS` runs a case so: `bpl-tests jit_disasm '+/ux>127ₓ₁' --setup 'ux←(¿(2*22)⍴256)ₓ₁' --prefs '•prefs ["kinds":"static"]'`. A stage compiles on its first run only where that run repays compiling (`blocks::Blocks::repays`): its items × the passes its parts save × `SAVED_NS` exceed its parts' compile cost, which is higher for a fold or a comparison. `--x86`: the x86-64 code CI and Linux run. First builds basedpl's library and its linked rustymath library for Linux x86-64 into `/tmp/aai-ws-x86`: minutes after a change → only for an x86-code question. Unfinished edit stops basedpl compiling → reads rustymath's library alone, saying so first. Linux: macOS builds omit rustymath's native linear algebra kernels. Libraries only: the Python extension crates can't build for Linux on this Mac. Their `release` profile matches CI's `test` profile in x86-64 flags and optimization. The build sets `CC_x86_64_unknown_linux_gnu=clang` itself; don't move it into `.cargo/config.toml` (CI's Linux release builds read that file in a manylinux container without `clang`). Don't use `--emit asm`: it splits code into other codegen units and inlines differently. @@ -173,19 +183,20 @@ JIT kernels exist only while a process runs. `BASEDPL_JIT_DISASM` set → `bpl` | `cli/` (`basedpl-cli`) | `bpl`, REPL, Jupyter kernel, JSON-lines worker | | `py/` (`basedpl-py`) | unpublished PyO3 extension `basedpl._core` | | `wasm/` (`basedpl-wasm`) | browser bindings | +| `testkit/` (`basedpl-testkit`) | reference checks and speed measurements for tests, worker test requests, the Python test helpers and the browser speed test; outside the library, so editing them rebuilds no library code | | `xtask/` | `cargo wasm`: builds the browser package | Default workspace members: core, CLI, Python; wasm and xtask build only on request. Frontends use the core's public API. Rust tests: `tests/` (core), `cli/tests/` (CLI). Libraries set `test = false` (no separate `cfg(test)` builds) → no build runs `#[cfg(test)]` code → libraries hold no inline tests or test-only counters. Internal behaviour worth a test (e.g. whether an expression fuses) is worth knowing for every user: `tests/` checks it through the public API (`•explain`, `•storage`, `•memory`). PyO3: its own crate, not an optional core feature. The core uses `serde_json` for the process protocol and data functions. Profiles: Rust 1.98 or later (algebraic float operations). `test` inherits `release`: opt-level 3, incremental, thin LTO across each crate's codegen units; no debug information, overflow checks or debug assertions → a `debug_assert!` never runs: write a check that matters as an always-on test. `.cargo/config.toml`: `-Zshare-generics` on every target and x86-64-v3 on x86, in `target` entries Cargo joins. `dist`: no incremental compilation; strips symbols. Keep these aligned with rustymath's. -Packaging: `fastws`'s `cargo develop` builds the project-selected profile (`quick` by default), installs the extension, stages `bpl` into the venv. `py/`: PyO3's `extension-module`, link arguments without libpython. Editable installs: `fastws.build_backend` installs metadata and a path to `python/`, no Rust compile → after a standalone `uv sync` or editable pip install, run `cargo develop` (`ws-sync` does after syncing). Wheels: maturin for the extension, `cargo stage` for the Python-free CLI; Linux staging inside the manylinux container. +Packaging: `fastws`'s `cargo develop` builds the project-selected profile, installs the extension, stages `bpl` into the venv. `py/`: PyO3's `extension-module`, link arguments without libpython. Editable installs: `fastws.build_backend` installs metadata and a path to `python/`, no Rust compile → after a standalone `uv sync` or editable pip install, run `cargo develop` (`ws-sync` does after syncing). Wheels: maturin for the extension, `cargo stage` for the Python-free CLI; Linux staging inside the manylinux container. Features: basedpl's crates pass on rustymath's default `native` feature (rustymath's DEV.md): each depends on the crate below with `default-features = false` and makes `native` its own default. Release staging and wheel builds pass `--no-default-features` → released Mac builds omit rustymath's own linear algebra. The Python binding inherits the CLI's pure-Rust frontend dependencies → identical shared dependency features in release builds. `flate2`: the system zlib on Apple targets; elsewhere its Rust backend, `miniz_oxide`. 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 test --release`, 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. CI runs no performance-budget suites 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 --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. `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`. @@ -193,6 +204,8 @@ CI: `Test` runs correctness checks on pushes to main and pull requests. Native c `cargo develop` and `fastws.cargo.run_tests(root, ...)` build the `release` profile, selected by `[tool.fastws] dev-profile = "release"` in `pyproject.toml`, so development and routine tests share one build in `target/release/`. It uses optimization level 3, local ThinLTO (`lto = false`) and incremental compilation. Bare `cargo test` still uses Cargo's built-in `test` profile, which has `release`'s settings but builds again in `target/debug/`; pass `--release` when not using `run_tests`. +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. + 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. @@ -276,11 +289,12 @@ A small repro must retain the concrete types, widths, generic caller chain, inli | `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` | | `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.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.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` | 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`) | | `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 | -| `protocol.rs`, `reference.rs`, `timing.rs`, `settings.rs` | process encoding, semantic checks, measurements; the settings `•prefs` changes | +| `protocol.rs`, `settings.rs` | process encoding; the settings `•prefs` changes | +| `testkit/src/reference.rs`, `testkit/src/timing.rs` | semantic checks and measurements, in crate `basedpl-testkit` | | `host.rs`, `host_web.rs`, `js.rs` | platform services; JavaScript values and functions | | `symbols.rs`, `inspection.rs` | shared names, shortcuts, syntax and help metadata, inspection | | `cli/src/`, `py/src/lib.rs`, `wasm/src/lib.rs` | frontend boundaries | @@ -363,14 +377,14 @@ Rank tries `whole` before `each_pair`. `whole`, in order: Axis nodes select or move axes, reusing Rank where the function has no direct axis meaning. Vector-axis reduction: the general composed-ravel rule, keeping axis order and the whole seed; single-axis reductions keep direct kernels. Layout-only changes share storage. Structural results keep renderers where rank permits; scans and in-place changes copy complete layouts. Match ignores renderers. -Inverse nodes propagate fixed-argument sides through composition. `f⁻¹←g`, `f∂←g` replace the binding with a `Ruled` node: the function plus its hand-written inverse and derivative; calling it calls the function; `⁻¹` and `∂` use the rules; Under counts an attached inverse as keeping every item. Candidate-building primitive inverses validate by applying the primitive again. Inverse scans share forward scan's seed and axis validation. Agenda calls its selector once, then passes the original arguments to the selected branch. Computed operands of Rank, Axis, Stencil and Agenda: shared operand evaluation. Python builds these nodes directly, not by generating source. +Inverse nodes propagate fixed-argument sides through composition. `f⁻←g`, `f∂←g` replace the binding with a `Ruled` node: the function plus its hand-written inverse and derivative; calling it calls the function; `⁻` and `∂` use the rules; Under counts an attached inverse as keeping every item. Candidate-building primitive inverses validate by applying the primitive again. Inverse scans share forward scan's seed and axis validation. Agenda calls its selector once, then passes the original arguments to the selected branch. Computed operands of Rank, Axis, Stencil and Agenda: shared operand evaluation. Python builds these nodes directly, not by generating source. Form rewriter (`eval/rewrite.rs`): the one home of fast combinations. - Rule: a row of BPL text, a pattern over `⍵` and `⍺`, beside the hidden `Special` that computes it (`⍟1+⍵` beside `Log1p`); `rules` adds the selections, one per order and pick. A rule's functions must be glyphs and its constants literals: a name holding `1` or `⍟` isn't recognised. - The rewriter reads a function and an expression as one tree of calls (`Form`), replacing each subtree a rule matches with a call of the rule's function. `Function::new` rewrites a function's monadic and dyadic forms once, keeping the results (`rewritten`); `dispatch` calls them in the function's place; inverses, Under and display read the original node. `Binder::reduced` rewrites each list of nodes after `joined` and `members_with`. Every spelling, expressions included, takes the combination from its first call. `calls_tines` builds a train whose tines `may_combine` accepts; `Function::new` then rewrites it. - `combined` runs the structural replacements (selections, ranks, Key's counts and folds) before any running level. An algorithm unsuited to the arguments, or a running level carrying one → `combined` calls the functions the combination replaces, which the level then sees. -- Each combination has a row under "Fast combinations" on its glyph pages. `tests/explain.rs` checks via `•explain` that every documented spelling of a structural combination takes it; `core.bpl` reference cases check each gives what its functions give in turn. The coverage matrix runs each rule whose function maps its argument (`mapping_patterns`), respelling any form's own case a rule would capture (`rewrites`). +- Each combination has a row under "Fast combinations" on its glyph pages. `tests/explain.rs` checks via `•explain` that every documented spelling of a structural combination takes it; `core.bpl` reference cases check each gives what its functions give in turn, and its route cases require each combination's route. The coverage matrix runs each rule whose function maps its argument (`mapping_patterns`), respelling any form's own case a rule would capture (`rewrites`). - A combination chooses an algorithm, which stays in rustymath or the kernel modules. A whole-array path that follows from an operator's own definition (Rank's for pervasive operands) belongs with the operator. ## Numbers and backend ownership @@ -421,7 +435,7 @@ Rustymath owns mathematical algorithms; basedpl owns pervasion, storage, numeric Hash maps and sets: `rustymath::hashing`'s `HashMap`, `HashSet` and `Hashing` (rustymath's DEV.md), not another hasher → every lookup in both projects inlines its hash the same way. -`•decompose`/`•decompose⁻¹` share domain reading and `Factor` dispatch, with options for kind, vectors and complete QR. Polynomial adapters: compact evaluation or generic field operations; gradients differentiate coefficients before evaluating. Number-theory adapters: cancellation checks, result cell assembly. Distribution constructors expose native density, CDF, quantile and sample functions; compact continuous calls use slice objects; discrete samples and finite discrete quantiles are exact integers. +`•decompose`/`•decompose⁻` share domain reading and `Factor` dispatch, with options for kind, vectors and complete QR. Polynomial adapters: compact evaluation or generic field operations; gradients differentiate coefficients before evaluating. Number-theory adapters: cancellation checks, result cell assembly. Distribution constructors expose native density, CDF, quantile and sample functions; compact continuous calls use slice objects; discrete samples and finite discrete quantiles are exact integers. `•rand seed`: a key and a count of draw calls behind a mutex (`rustymath::random::Counter`), shared by every copy of its record. `¿`, and a distribution's `sample` without a generator, draw from the session's generator; `•prefs ["seed":n]` reseeds it. Draw call c reads stream c under the key from its start. Item k of a call reads its packed words, then its own region (`rustymath::random::Items`), so an item reads the same words on every route. The algorithm is fixed, but range or distribution sampler changes can change draws: the reference sequence detects that. Sampling from a generator record uses its roll function to recover the same generator. @@ -441,9 +455,9 @@ Fusion (`fusion.rs`; its module doc owns eligibility rules and limits: don't cop - 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. - 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.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`. A call waits where fusing it saves more than its node costs (`fusion::waits`: `NODE_NS`, the allowance per call, against `SAVED_NS_PER_BYTE`); `BASEDPL_DEFER=all` makes every call wait, for timing what a node costs. +- 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. -- A structural copy of one source through rustymath `Steps` (`remapped`) large enough to wait (`fusion::waits`) is a pending copy (`fusion::copy`), any compact kind; its map is a `Mapping::Steps`, one entry per result axis, as broadcasting's are. Forcing composes a read of a copy with its map and moves a read with no repeat or fill under the pervasive calls it reaches (`fusion::resolved`): `2×⌽v+1` reads `v` backwards in one program. A copying read (rustymath's `Mapping::copies`) moves only under a one-array call. A call whose two array arguments copy same-layout arrays through one map runs on those arrays, its result copied once (`program::rules`, `copy_lifted`). Other arrays: a `Copy` step, which a block stage reads through without making the copy. A step reads items in order under another shape through its operand's `program::Read`. +- A structural copy of one source through rustymath `Steps` (`remapped`) large enough to wait (`fusion::defers`) is a pending copy (`fusion::copy`), any compact kind; its map is a `Mapping::Steps`, one entry per result axis, as broadcasting's are. Forcing composes a read of a copy with its map and moves a read with no repeat or fill under the pervasive calls it reaches (`fusion::resolved`): `2×⌽v+1` reads `v` backwards in one program. A copying read (rustymath's `Mapping::copies`) moves only under a one-array call. A call whose two array arguments copy same-layout arrays through one map runs on those arrays, its result copied once (`program::rules`, `copy_lifted`). Other arrays: a `Copy` step, which a block stage reads through without making the copy. A step reads items in order under another shape through its operand's `program::Read`. - A block stage shrinks blocks whose buffers wouldn't fit in cache. Rustymath's `agreement::expanded` reads a forward run in place, else copies via `gather::walk_plan`. A `Mapping::Steps` keeps the plan `Steps::simplify` makes on its first walk (`agreement::Route`) for every later block. A copy no call reads through is walked as `remapped` walks it. - Fused and ordinary folds share rustymath's `reduce::along`/`fold_into` traversal. A block stage of one compiled part folds the last step's items itself, handing `reduce::fold_pieces` each block's total (`reduce::Part`): sums, float products, maxima, minima. A float fold keeps as many running results as rustymath's folds (`reduce::running`). Boolean folds, float `⌈`/`⌊` folds, and blocks crossing a lane go through the last step's block. - On the pending route, a fold of a windows copy along its own axis slides over the copy's source (`pervasive::slid`, `mapped_fold`: rustymath's `reduce::fold_route` with the copy's source, `structure::Slide`, `reduce::sliding`); a fold along two window axes folds both in one pass (`sliding_2d`). A short fold a stage folds as one step per window position (`blocks::short_window`) runs as a block stage reading the copy's source at each position through the copies' maps: in a dfn's program, and on the pending route where the copy reads another pending copy (e.g. padding) into a result whose last axis exceeds `DIRECT` (`fusion::staged`). Under `∂`, `fold_rule` takes a maximum's or minimum's positions in such windows from the source (`pervasive::mapped_picks`); under a trace, `Step::Picks` gives each lane's pick, via the untraced mask's function (`derivative::chosen`). A traced `⌈`/`⌊` scan, or picks from sliding windows, stops the trace. @@ -557,7 +571,7 @@ Recursion limits: CI's `Test` wasm job takes Node's version from `.node-version` Browser `Session.run` streams MIME events and returns a protocol error or null. `eval` transfers values via `js.rs`, discards output, can't export BPL functions or operators; `bind` imports page values and functions. Typed arrays transfer without another page-worker copy. JavaScript functions live in a thread-local registry and core function nodes hold slots → values stay Send/Sync. `host_web.rs`: the browser's clock, synchronous I/O and fetch, RegExp behaviour. File and URL reads resolve against the configured base. Unsupported native filesystem and locale operations stay unavailable; browser `•nput` writes only stdout; `•js` is browser-only. -The playground and gallery load released assets from jsDelivr, or local `nbs/playground/` assets on localhost. Shared `bpl.js` owns editors, worker startup and output. The authored ES modules and font in `python/basedpl/browser/` need no evaluator: `browser/bpl-editor.js` exports grammar, configuration, editor options, font installation and explicit `install()`. Importing data never mounts the bar. Solveit, the playground and the gallery supply a normal-flow `#bpl-toolbar` slot; elsewhere the same component overlays the page without padding or height adjustment. One persisted collapse button keeps keyboard input active. `install()` reuses a connected controller; its `dispose()` closes it. Python magics call Solveit's `window.loadBplInput()` when present, else import the same public module from jsDelivr. Restarting a trapped or stopped worker loses its state. `worker.js`, `page.js` and `gallery.js` own their protocols and UI; `nbs/driver.js` documents canvas drawing data. Gallery demos: `.bpl` sources with shared preferences, checked natively by `language_examples`. Browser recursion tests cover both V8 compilers and fail with LIMIT before stack exhaustion. +The playground and gallery load released assets from jsDelivr, or local `nbs/playground/` assets on localhost. Shared `bpl.js` owns editors, worker startup and output. The authored ES modules and font in `python/basedpl/browser/` need no evaluator: `browser/bpl-editor.js` exports grammar, configuration, editor options, font installation and explicit `install()`. Importing data never mounts the bar. Solveit, the playground and the gallery supply a normal-flow `#bpl-toolbar` slot; elsewhere the same component overlays the page without padding or height adjustment. One persisted collapse button keeps keyboard input active. `install()` reuses a connected controller; its `dispose()` closes it. Python magics call Solveit's `window.loadBplInput()` when present, else import the same public module from jsDelivr. Restarting a trapped or stopped worker loses its state. `worker.js`, `page.js` and `gallery.js` own their protocols and UI; `nbs/driver.js` documents canvas drawing data. Gallery demos: `.bpl` sources with shared preferences, checked natively with the documentation pages by `reference_cases` (`tests/reference.rs`). Browser recursion tests cover both V8 compilers and fail with LIMIT before stack exhaustion. ## System functions and rendering @@ -570,9 +584,9 @@ The playground and gallery load released assets from jsDelivr, or local `nbs/pla `optimize.rs`: `⇄` on `rustymath::optimize`; the left argument holds options. A method that can use a derivative (none named, L-BFGS, Levenberg–Marquardt, CG, MINRES) → one linearisation at the start (`derivative::linearized`) finds whether `∂` differentiates the function and gives its value there. `Goal` evaluates the function at points in the start's layout for `minimize`; under L-BFGS each value comes from a linearisation that `gradient` transposes at the same point (`linearized_at`) → one function run per value and gradient. With `"solve":$t`, `Residual` is the `Mapping` `solve` takes, or `solve_affine` with `"linear":$t` too. Where `∂` differentiates the function, `Residual` gives JVPs and vector–Jacobian products from one record per point, or the start's record for an affine function. A later point where `∂` gives UNSUPPORTED → `Residual::jvp` declines; rustymath takes forward differences for that Jacobian and later ones. A square linear system takes no transpose products → GMRES on A, not the normal equations. Option `precondition`'s function gives the preconditioner. -CSV columns, JSON arrays and Python lists share `data::imported`: a non-fill finite float, or any nonfinite float, promotes to float when every number converts losslessly; finite fills alone don't; otherwise items keep their kinds. JSON objects and Python dicts read as literals do (`keyed::vector`, `Value::from_parts`). Binary data: exact bytes. Writes validate before opening and create exclusively unless overwrite is explicit. Exports: `•csv⁻¹`, `•json⁻¹`, `•xml⁻¹`. +CSV columns, JSON arrays and Python lists share `data::imported`: a non-fill finite float, or any nonfinite float, promotes to float when every number converts losslessly; finite fills alone don't; otherwise items keep their kinds. JSON objects and Python dicts read as literals do (`keyed::vector`, `Value::from_parts`). Binary data: exact bytes. Writes validate before opening and create exclusively unless overwrite is explicit. Exports: `•csv⁻`, `•json⁻`, `•xml⁻`. -`regex.rs` shares host regex state between match and replace native functions; hosts keep their own regex syntax. `xml.rs`: keyed element trees; XML escaping and validation kept apart from HTML raw script/style and void-element rules. SVG has its own renderer. `image.rs` owns pictures, their options and animation timing, as plain pixel buffers; its `codec` module decodes and encodes via ImageIO on Apple targets (`image/imageio.rs`), the `image` and `png` crates elsewhere (`image/codecs.rs`). Native and browser renderers use the MIME and byte formats their frontends support. `plot.rs` stores specs and draws SVG on demand; the renderer owns defaults and validation. Rendering options are documented in their user pages and implementation comments. +`regex.rs` shares host regex state between match and replace native functions; hosts keep their own regex syntax. `xml.rs`: keyed element trees; XML escaping and validation kept apart from HTML raw script/style and void-element rules. SVG has its own renderer. `image.rs` owns pictures, their options and animation timing, as plain pixel buffers; `basedpl-image` decodes and encodes via ImageIO on Apple targets (`image/src/imageio.rs`), the `image` and `png` crates elsewhere (`image/src/codecs.rs`). Native and browser renderers use the MIME and byte formats their frontends support. `plot.rs` stores specs and draws SVG on demand; the renderer owns defaults and validation. Rendering options are documented in their user pages and implementation comments. `display.rs` owns plain, boxed and function display, elision and MIME rendering. Display settings affect implicit presentation, not returned values or `⍕`; explicit output prints all items. Display and source text read items, not storage: `Value::shown_dtype` (`array/text.rs`) gives the dtype compact storage of the items would take (`storage::items_dtype`'s items rule), read by the shared `ₓ` or width marker, plain display's choice between brackets and a strand, the bracketed grid plain display and `⍕` share (`display::table`, `display::bracketed`), a keyed table's corner and the boxed marker. `•mime` calls a renderer with echo off and cancellation active. Failed implicit rendering falls back to text; interrupts and timeouts propagate. `canvas.rs` serializes drawing data and typed bytes; `canvas.js` and `nbs/driver.js` own browser playback. Keep formatting, serialization and rich output separate contracts. @@ -580,20 +594,20 @@ CSV columns, JSON arrays and Python lists share `data::imported`: a non-fill fin `tests/core.rs`: public storage, layout and API behaviour, ownership, parser diagnostics and recovery. `equiv_in!` for session workflows, `fails_in` for errors; each expected expression runs in a fresh session after its code, under the settings the code leaves. `tests/core.rs` never changes settings: they're one process-wide store → checks that change them live in `tests/settings.rs`, run in turn in one test, each from the defaults. Both binaries share `tests/common/mod.rs`. Keep constructors for representation, prototype and buffer checks. Self-contained language cases: `tests/reference/core.bpl`. `cli/tests/cli.rs` and Python's pseudo-terminal tests exercise the installed process and editor boundary. -`tests/jit.rs::compiled_stages_match_interpreted` compares every case at four original sizes. Set `BASEDPL_JIT_SIZE_INDEX` to `0`, `1`, `2` or `3` to check one size before allocating its fixtures: `16×DEFAULT.block` plus `0`, `1`, `7` or `37` items. Invalid indices fail. With the variable unset, all four sizes run. Each completed size reports its item count and dynamic/static case counts. Use the selector to partition the existing checks within a runner's execution budget; it does not resize inputs or skip expressions at the selected size. +`tests/jit.rs`'s `compiled_stages_match_interpreted_plus_N` tests compare every case at `16×DEFAULT.block` plus `N` items, for `N` of 0, 1, 7 and 37: one test per size, so nextest checks the sizes in parallel processes. -`reference.rs` serves Rust tests and worker requests. +`testkit/` (crate `basedpl-testkit`) holds `reference.rs` and `timing.rs`, for Rust tests, `bpl --worker`'s test requests, the Python extension's test helpers and the browser's speed test. The library doesn't depend on it, so editing them rebuilds no library code. The internals they use are `basedpl::hooks` (`doc(hidden)`). -- 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 (`language_examples`). +- 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. -- Each case runs under the normal compile policy, with every available block stage compiled, with compilation disabled, and as the body of a dfn called twice. 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. +- 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. -- Each reference evaluation and its final value read have a two-second deadline in ordinary correctness runs. The managed runner's outer watchdog remains separate. Keep known over-deadline cases at full size in section-specific `tests/skipped.txt` entries linked to their open work; never disable deadline checks globally. +- Each reference evaluation and its final value read have a two-second deadline in ordinary correctness runs. `reference_cases` reruns a case that misses it once, in a fresh child at the end of its queue, and fails only if the rerun misses too; it reports both times. It hands out the cases that took longest in its last run first (`reference-times.json` under `CARGO_TARGET_TMPDIR`). The managed runner's outer watchdog remains separate. Keep known over-deadline cases at full size in section-specific `tests/skipped.txt` entries linked to their open work; never disable deadline checks globally. - Each expectation evaluates after its code, under the settings the code leaves. A float matches within 1e-13, or 8 ulps of its own width where greater, unless the header sets a tolerance (`reference::same_atom`). `.bpl` files define active coverage. Captured foreign expectations have no exactness; local expectations compare exactness and structural values independently, and fix where storage is mixed: the result must be mixed exactly where the expectation is, at each nesting level (`kind_difference`), unless the header carries `mixed`. -- Pending gaps go in inventory JSONL, not passing error tests. Format and review/activation workflow: `tests/reference/README.md`. `python/basedpl/reference.py`, `bpltests.py` and `scripts/reference.py` own import, capture, inventory and worker checks. `lib/*.bpl`: readable dfn examples; modules import other library files with private relative names. +- Pending gaps go in inventory JSONL, not passing error tests. Format and review/activation workflow: `tests/reference/README.md`. `python/basedpl/reference.py` and `bpltests.py` own import, capture, inventory and worker checks. `lib/*.bpl`: readable dfn examples; modules import other library files with private relative names. -`tests/scaling.rs`: time and allocation growth, result-size allocation budgets. `tests/speed.rs` reads native and browser budgets from `speed/*.bpl` via `timing.rs`; the worker and Python measurement tools reuse it. Both, and `tests/reference.rs`'s children, register rustymath's memory guard and turn on its checks (`memory::set_checks`) in one-case-at-a-time processes: a reference case leaving more bytes held in large allocations than it found, on each of two runs, fails as a leak, as does a speed case whose timed runs leave more than its first run. A free or reallocation passing more bytes than its block holds aborts with a backtrace. Python tests cover conversion, cancellation and frontend behaviour, not a second Rust semantic suite. Combine related checks into readable tests. +`tests/speed.rs`: each speed file's `timed_` test checks its cases' costs against native budgets from `speed/*.bpl` via `testkit/src/timing.rs`, and its `counts_` test checks their instructions and allocated bytes for each item (`timing::counts`). `speed/growth.bpl` holds the growth and allocation cases, and `speed/steps.bpl` the per-call cases. The worker and Python measurement tools reuse it. `tests/speed.rs` and `tests/reference.rs`'s children register rustymath's memory guard and turn on its checks (`memory::set_checks`) in one-case-at-a-time processes: a reference case leaving more bytes held in large allocations than it found, on each of two runs, fails as a leak, as does a speed case whose timed runs leave more than its first run. A free or reallocation passing more bytes than its block holds aborts with a backtrace. Python tests cover conversion, cancellation and frontend behaviour, not a second Rust semantic suite. Combine related checks into readable tests. ## Documentation and generated assets @@ -608,7 +622,7 @@ nbdev-test nbs/dyalog.ipynb --flags dyalog nbdev-preview ``` -Quarto renders saved outputs. `bpl-nb --save` updates notebook outputs without changing execution counts: use it for BPL examples and figures, `nbdev-test` for Python notebook code. `bpl-nb --compare` prints a diff for each cell whose shown text differs from its stored outputs, and exits with status 1 if any do. Rust `language_examples` checks BPL page and gallery examples; pytest checks Python page examples; `BASEDPL_PAGE` focuses page checks. Don't run a full documentation build for routine output updates. Before a release, `bpl-tests prerelease` runs the normal development build and nbdev's preparation ("Release"). +Quarto renders saved outputs. `bpl-nb --save` updates notebook outputs without changing execution counts: use it for BPL examples and figures, `nbdev-test` for Python notebook code. `bpl-nb --compare` prints a diff for each cell whose shown text differs from its stored outputs, and exits with status 1 if any do. Rust `reference_cases` checks BPL page and gallery examples, and `bpl-tests check_page PATH` checks one page; pytest checks Python page examples. Don't run a full documentation build for routine output updates. Before a release, `bpl-tests prerelease` runs the normal development build and nbdev's preparation ("Release"). `cargo develop` and `ws-sync` run `basedpl.editors` in a fresh Python process after installing the extension, before copying wheel data: it generates grammars, highlighters, editor templates, keyboard bundles and glyph metadata, Rust-derived `browser/language.js` included. JavaScript UI changes are authored modules Vite reads directly: no generator or npm refresh. `cargo wasm` copies the whole browser directory into the npm package and local playground. `browser/layout.json` owns composed input; captured `keyboards.json` allows generation without macOS; `basedpl.keyboards.capture()` refreshes native data when needed; `keyboards.write()` generates regional macOS bundles and maps. Regenerate keyboard sources through the normal build, not one-off transformations. `basedpl-install` copies packaged fonts and macOS bundles from `python/basedpl/assets` to the user's directories. Target-specific work: `editors/jetbrains/README.md`, `editors/windows/README.md`. Rebuild the common icon only when its design changes. @@ -620,8 +634,8 @@ Before approval: verify successful tests for the release commit and the relevant Before each release, run `bpl-tests prerelease` from basedpl's root, in a shell, not a kernel, while no one else edits either repository. Development builds, native timings and target builds use the `release` profile unless `--profile NAME` selects another. The checked test profile stays separate. It runs both repositories' release checks and saves: -1. Builds, tests and saves run in parallel once their dependencies pass: development installation, managed native correctness tests, `pytest -q`, `nbdev-prepare`, and `cargo test --profile checked` in its own target directory. basedpl's development installation waits for rustymath's. It includes editor generation; basedpl's Node tests, notebooks and `target_builds` wait for it, and browser tests follow `target_builds`. -2. Timings, one at a time, nothing else running: native speed tests in each repository with `_COSTS` set, rustymath's Python speed test, `thread_scaling`, and browser speed tests in selector batches. Each browser batch has the existing 30-second execution budget and verifies every selected `(file, section, code)` identity, including retries. With `--keep-going`, a failed batch does not hide later batches. +1. Builds, checked tests and saves run in parallel once their dependencies pass: development installation, `nbdev-prepare`, and checked-profile native correctness tests in their own target directory. basedpl's development installation waits for rustymath's. It includes editor generation; basedpl's Node tests, saved BPL notebooks and `target_builds` wait for it, and browser tests follow `target_builds`. +2. After those steps, one at a time with nothing else running: one full default `run_tests` per repository (correctness, instructions, allocations and timings), one `pytest -q` per repository (including rustymath's Python speed tests), the three uncompiled native-kernel controls, `thread_scaling`, and browser speed tests in selector batches. Native suites are not repeated in per-file batches. The fresh browser-speed command partitions fixtures after development installation, never through the bootstrap process's old extension. Each completed browser report is flushed to the stage log immediately, without a duplicate final report. Each browser batch has the existing 30-second execution budget and verifies every selected `(file, section, code)` identity, including retries. With `--keep-going`, a failed step does not hide later independent steps. 3. `nbdev-clean` in both repositories, in a `finally` block after failures, launch errors or cancellation. The first failure ends the run unless `--keep-going`. Summary: each step's status, time and log. `--dry-run` lists the steps in order without running them. Cargo builds have no outer deadline; managed tests retain their execution watchdogs. The preflight stops before starting if the pinned Node version is unavailable or Node cannot resolve `shiki`. It notes when basedpl needs an unreleased rustymath version (rustymath then ships first). It never commits, pushes or tags. diff --git a/cli/Cargo.toml b/cli/Cargo.toml index b656f25f..724f43e1 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -21,6 +21,7 @@ async-trait = ">=0.1, <1" ctrlc.workspace = true kernmini.workspace = true rustyline.workspace = true +basedpl-testkit = { path = "../testkit" } rustymath.workspace = true serde = ">=1.0.229, <2" serde_json = "1" diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 2fb702eb..847d3483 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -112,8 +112,10 @@ fn repl(err: &mut impl Write, interactive: bool) -> io::Result { /// Times a BPL speed file or directory, reporting budgets without failing on them. fn costs(path: &str, out: &mut impl Write, err: &mut impl Write) -> io::Result { let mut written = Ok(()); - let measured = basedpl::reference::load(std::path::Path::new(path)).and_then(|files| { - basedpl::timing::measured(&files, true, |line| { if written.is_ok() { written = writeln!(out, "{line}"); } }) + let measured = basedpl_testkit::reference::load(std::path::Path::new(path)).and_then(|files| { + basedpl_testkit::timing::measured(&files, None, true, |line| { + if written.is_ok() { written = writeln!(out, "{line}"); } + }) }); written?; match measured { Ok(_) => Ok(0), Err(e) => writeln!(err, "{e}").map(|_| 1) } diff --git a/cli/src/worker.rs b/cli/src/worker.rs index 06c3ccec..d79f71a9 100644 --- a/cli/src/worker.rs +++ b/cli/src/worker.rs @@ -60,12 +60,12 @@ impl serde::Serialize for Line { fn reply(session: &Session, request: &Value, interrupt: InterruptHandle) -> 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::reference::check(case, &options.interrupt))); } - if let Some(spec) = request.get("timing") { return basedpl::timing::request(spec, &options.interrupt).map(Reply::Json); } - if let Some(spec) = request.get("steps") { return basedpl::timing::steps_request(spec, &options.interrupt).map(Reply::Json); } - if let Some(spec) = request.get("costs") { return basedpl::timing::file_request(spec, &options.interrupt).map(Reply::Json); } - if let Some(spec) = request.get("reference") { return basedpl::reference::file_request(spec, &options.interrupt).map(Reply::Json); } - if let Some(spec) = request.get("page") { return basedpl::reference::page_request(spec, &options.interrupt, options.timeout).map(Reply::Json); } - if let Some(spec) = request.get("route") { return basedpl::timing::route_request(spec, &options.interrupt).map(Reply::Json); } + 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("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); } + if let Some(spec) = request.get("route") { return basedpl_testkit::timing::route_request(spec, &options.interrupt).map(Reply::Json); } basedpl::protocol::request(session, request, options).map(|e| Reply::Response(basedpl::protocol::Response(e))) } diff --git a/editors/jetbrains/BPL.tmbundle/Syntaxes/bpl.tmLanguage.json b/editors/jetbrains/BPL.tmbundle/Syntaxes/bpl.tmLanguage.json index b84a65e7..ec30e69e 100644 --- a/editors/jetbrains/BPL.tmbundle/Syntaxes/bpl.tmLanguage.json +++ b/editors/jetbrains/BPL.tmbundle/Syntaxes/bpl.tmLanguage.json @@ -45,7 +45,7 @@ "name": "string.quoted.single.bpl" }, { - "match": "(?:_\\d+|_\\S|_|[^\\s\\p{Cc}0-9←⎕•⍺⍵⍶⍹∇⍢⍝⋄⁝\\?¯∞⍬\\+\\-×÷⌈⌊\\|\\*⍟○π√!∧∨⊼⊽\\~=≠<≤>≥⍳↦:⍴≢≡,⍪⊂⊃⊆∊∪∩⍋⍒↑↓⌽⊖⍉⊤⊥⍎⍕⌷⌹\\#↕⍭⨸⌻⍸⍷⊢⊣¿¨ᵇ⍨↣⍤∘⍠⍥↢\\.⊗⌸⍣⇄⊘@∂⍚⌺/⌿\\\\⍀⁰¹²³⁴⁵⁶⁷⁸⁹⁻ᵀᵘₓ₀₁₂₃₄₅₆₇₈₉₋ₓ₁ₓ₂ₓ₄ₓ₈ₓₕₛₚₐᵣ\\(\\)\\[\\]\\{\\}'\";\\$_ₑⱼᵣ])+", + "match": "(?:_\\d+|_\\S|_|[^\\s\\p{Cc}0-9←⎕•⍺⍵⍶⍹∇⍢⍝⋄⁝\\?¯∞⍬\\+\\-×÷⌈⌊\\|\\*⍟○π√!∧∨⊼⊽\\~=≠<≤>≥⍳↦:⍴≢≡,⍪⊂⊃⊆∊∪∩⍋⍒↑↓⌽⊖⍉⊤⊥⍎⍕⌷⌹\\#↕⍭⨸⌻⍸⍷⊢⊣¿¨ᵇ⍨↣⍤∘⍠⍥↢\\.⊗⌸⍣⇄⊘@∂⍚⌺/⌿\\\\⍀⁻⁰¹²³⁴⁵⁶⁷⁸⁹⁻ᵀᵘₓ₀₁₂₃₄₅₆₇₈₉₋ₓ₁ₓ₂ₓ₄ₓ₈ₓₕₛₚₐᵣ\\(\\)\\[\\]\\{\\}'\";\\$_ₑⱼᵣ])+", "name": "variable.other.bpl" }, { @@ -94,7 +94,7 @@ "name": "keyword.operator.monadic.bpl" }, { - "match": "[¨ᵇ⍨⊗⌸⇄∂/⌿\\\\⍀⁰¹²³⁴⁵⁶⁷⁸⁹⁻ᵀᵘₓ]", + "match": "[¨ᵇ⍨⊗⌸⇄∂/⌿\\\\⍀⁻⁰¹²³⁴⁵⁶⁷⁸⁹⁻ᵀᵘₓ]", "name": "keyword.operator.monadic.bpl" }, { diff --git a/editors/jetbrains/BPL.tmbundle/Syntaxes/hpl.tmLanguage.json b/editors/jetbrains/BPL.tmbundle/Syntaxes/hpl.tmLanguage.json index 795c239d..1c762385 100644 --- a/editors/jetbrains/BPL.tmbundle/Syntaxes/hpl.tmLanguage.json +++ b/editors/jetbrains/BPL.tmbundle/Syntaxes/hpl.tmLanguage.json @@ -19,7 +19,7 @@ "applyEndPatternLast": true }, { - "match": "(?:_\\d+|_\\S|_|[^\\s\\p{Cc}0-9←是⎕出入•系⍺甲⍵乙⍶丙⍹丁∇递遞⍢循⍝注⋄句⁝隔\\?若¯负負∞永⍬空\\+加\\-减減×乘号號÷除⌈上⌊下\\|绝余絕餘\\*幂冪⍟对對○圆圓π周√根!阶拣階揀∧极且極∨析或⊼方否⊽双莫雙\\~非无無=等≠别初別<小≤限退>大≥达进達進⍳位↦至:键卸鍵⍴形≢数异數異≡同深,连連⍪接矩⊂包⊃选合選⊆巢段∊属展屬∪并独併獨∩交重⍋升⍒降↑取↓去拆⌽转轉⊖纵縱⍉翻⊤编編⊥译譯⍎行⍕文⌷索⌹解逆\\#复複↕窗⍭素⨸因⌻零值⍸处间處間⍷寻群尋⊢右⊣左¿掷抽擲¨遍ᵇ按⍨己换換↣先⍤秩∘承⍠轴軸⍥经經↢后後\\.内內⊗表⌸组組⍣次止⇄优優⊘兼@于於∂导梯導⍚择擇⌺邻鄰/总總⌿汇匯\\\\累⍀续續⁰¹²³⁴⁵⁶⁷⁸⁹⁻ᵀᵘₓ₀₁₂₃₄₅₆₇₈₉₋ₓ₁ₓ₂ₓ₄ₓ₈ₓₕₛₚₐᵣ\\(\\)\\[\\]\\{\\}'\";\\$_ₑⱼᵣ])+", + "match": "(?:_\\d+|_\\S|_|[^\\s\\p{Cc}0-9←是⎕出入•系⍺甲⍵乙⍶丙⍹丁∇递遞⍢循⍝注⋄句⁝隔\\?若¯负負∞永⍬空\\+加\\-减減×乘号號÷除⌈上⌊下\\|绝余絕餘\\*幂冪⍟对對○圆圓π周√根!阶拣階揀∧极且極∨析或⊼方否⊽双莫雙\\~非无無=等≠别初別<小≤限退>大≥达进達進⍳位↦至:键卸鍵⍴形≢数异數異≡同深,连連⍪接矩⊂包⊃选合選⊆巢段∊属展屬∪并独併獨∩交重⍋升⍒降↑取↓去拆⌽转轉⊖纵縱⍉翻⊤编編⊥译譯⍎行⍕文⌷索⌹解逆\\#复複↕窗⍭素⨸因⌻零值⍸处间處間⍷寻群尋⊢右⊣左¿掷抽擲¨遍ᵇ按⍨己换換↣先⍤秩∘承⍠轴軸⍥经經↢后後\\.内內⊗表⌸组組⍣次止⇄优優⊘兼@于於∂导梯導⍚择擇⌺邻鄰/总總⌿汇匯\\\\累⍀续續⁻反⁰¹²³⁴⁵⁶⁷⁸⁹⁻ᵀᵘₓ₀₁₂₃₄₅₆₇₈₉₋ₓ₁ₓ₂ₓ₄ₓ₈ₓₕₛₚₐᵣ\\(\\)\\[\\]\\{\\}'\";\\$_ₑⱼᵣ])+", "name": "variable.other.bpl" }, { @@ -39,7 +39,7 @@ "name": "support.function.bpl" }, { - "match": "[遍按己换換表组組优優导梯導总總汇匯累续續]", + "match": "[遍按己换換表组組优優导梯導总總汇匯累续續反]", "name": "keyword.operator.monadic.bpl" }, { diff --git a/editors/jetbrains/BPL.xml b/editors/jetbrains/BPL.xml index 0cf9d492..21b6b5d9 100644 --- a/editors/jetbrains/BPL.xml +++ b/editors/jetbrains/BPL.xml @@ -524,6 +524,11 @@