From 73cdc25ac136d3d10e88fdf2da5329b2d927095d Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sun, 1 Nov 2026 16:02:11 +1000 Subject: [PATCH 01/47] target --- nbs/target.qmd | 206 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 nbs/target.qmd diff --git a/nbs/target.qmd b/nbs/target.qmd new file mode 100644 index 00000000..e12c1f99 --- /dev/null +++ b/nbs/target.qmd @@ -0,0 +1,206 @@ +# Target: the finished core + +Measure for every step, diff and review in the one-graph release. A diff moves the tree toward a part below, or it doesn't belong. Tree code is no precedent: an existing pattern (a wrapper's match arms, a fast path, a closure, a cache) is a candidate gap, never a reason. This file is the 1 home of the design; `archive/one-graph-2026-10-31.md` keeps reasons, evidence and history. + +## Goals + +- Speed and simplicity (`AGENTS.md`). Workloads: interactive analysis of large data, model inference and training, image processing. A foundation that works on a GPU with no special cases; this release builds the core's part (part 23). +- Fast code: static kinds; branches and updates by `⍚`, `f@g`, `f@[p]`, `x.[p]←`, never dfn guards on traced values. Code written without that care: adequate speed. Dynamic kinds: correct, at most 4× the same code under static kinds at the same size, except where Jeremy agrees (a larger gap where a reasonable rewrite is fast). A guide, "Writing fast BPL", ships with the release: static kinds with explicit sizes on literals and data, `⍚`, `f@g`, `f@[p]`, selective assignment into a preallocated cache (`K.[p]←x+.×Wk`), index notation, shapes kept the same across calls and iterations, `fₐ` where values may leave the real domain; and what ordinary code costs. +- Results agree only within the documented accuracy (4 ulps), across spellings, routes, sizes and threads. +- "The bar" (below) sets the criteria; it changes only with Jeremy's OK. Done: "Acceptance". + +## Non-goals (this release) + +- Speed for dfn guards on traced values. +- Speculated widths for ordinary integers (ranges instead; decision 16). +- Identical bits across routes, sizes, threads or spellings; accuracy past the documented bounds. +- Reusing a graph across calls from a Python loop or a recursive dfn's calls (decision 5): each call rebuilds. +- Recording through `⍎` or an error guard. +- Symbolic lengths that let 1 recording serve every length (decision 18): buckets are a key policy (part 10). +- Complex and f16 steps in block stages (main's decision 13); complex numbers under `∂` until a gradient convention is chosen. +- The GPU backend, array files, an external backend, bf16 (part 23). +- Range inference through `•index ∞` (decision 11). +- Segments with several bounded lengths (continuous batching). + +## Measures + +- 1 mechanism per job; 1 home per fact. A 2nd route for a job is a gap, faster or not: speed = a rule or a scheduler choice over nodes. +- Language rules: the primitive's row (`Primitive::info()`). Math facts: its rustymath object. Nothing else keys on a primitive. +- No decision by spelling (name checks, walks of a function's form), except where meaning comes from form: inverses, `discards`, fill options, display. +- A new kind of thing = data in a table, not arms across match sites. A variant that needs arms in many matches → the enum lacks a shared shape: fix the enum. +- Rust closures never stand in for BPL functions or bodies. A body is a function value or a recording. +- No cache beside the program cache (recordings) and the JIT's code table (compiled code), each its fact's 1 home; no 2nd copy of items. +- A recording, or a forced graph's cached plan, assumes only its key, its captured free values and its speculation boxes (a speculated kind, and each leaf range fact its plan relies on: part 2). +- Data errors: kernels and executors set the session's error slot, which keeps the 1st error and the node that set it; forcing records and never fails; a run that sets the slot replaces no node downstream of the failing one with a result, so those stay pending and a later read runs them again and raises again; a failed kernel writes values inside its node's declared range and reads nothing past its operands, so no later step reads out of bounds; clearing a masked case's error reruns the whole read (parts 13, 14), so an error the masked case hid is still recorded; the executor raises at its next checkpoint: a host read deciding structure (a count, a shape, a guard), an effect, an error guard's exit (leaving an error guard forces the work built inside it, so the guard catches its errors), display, the statement's end. When and how an error surfaces isn't promised. +- 1 mode (tinygrad): every call builds a node and waits; a read forces what it needs and runs its graph's plan from the 1 cache, scheduling only on a miss. Each stage has a budget ("Cost model"); a stage over budget → a cheaper stage, never a 2nd route. +- basedpl and rustymath never hold 2 machineries for 1 task. + +Yes, your outline matches what I'd do. I'd add two steps before the subsystem forks start, plus explicit guards against call-site spread. + +## The build, in order +1. **Interfaces as code first.** One fork writes the target's interfaces as real Rust declarations in their home files, with bodies left as `todo!()`: `Desc`, each `Step` variant's fields, `CallPlan`, `Program`, `Plan` with `refill`, `Node`, `Level`, `FunctionNode` and `OperatorKind::info`, `Rule`, `Programs::serve` with `Key`, and `Session::error`/`raise`. I review it against the target. Prose interfaces leave each fork to invent details, and those inventions collide at compile time as glue at call sites. +2. **A short design note per subsystem before its code.** Each subsystem fork first writes its data structures, its algorithms, and an estimate per operation against its cost-model budget, and I review that. For example, nodes interned by structure already hash themselves when built, so a read's cache key could come from the root's hash rather than a fresh pass over every node. +3. **Subsystem forks write the end state.** Up to five at a time, each owning whole files and writing its subsystem as the target describes, against the declared interfaces. They delete the old code outright, using their part's "Not" list as the checklist. Nothing is compiled. +4. **Compile integration, in dependency order.** A mismatch at a boundary means one side departs from the declared interface, and that side gets fixed. No adapter goes in between. +5. **Correctness, then speed.** First the reference files and "Failing cases". Then each stage's instruction counts against its budget, where a stage over budget gets a cheaper implementation, never a second route. + +### Keeping call sites from spreading again + +- **No threading of cross-cutting concerns.** The target already keeps lengths, errors, levels and settings each in one home, out of engine call sites. Engine code sees only concrete lengths, errors go to the session slot rather than through `Result`s everywhere, and steps carry the settings they read. +- **Changes outside a fork's files only through the declared interfaces.** A fork that needs a new parameter on a widely called function stops and reports. That's an interface change, so it comes to me, and to you if it changes the design. +- **A count in every diff review:** the functions outside the fork's own files that the diff touches. Any number above zero needs a reason. Bounded extents' roughly 90 call sites would have shown up in the first such review. +- **The isolation test applies to everything:** complexity is acceptable only where it's fully isolated behind its home. + +### Existing code: reference, never parts + +- Each subsystem's design note carries a keep list: every existing function the fork keeps or calls, each with the target part that names it as the home. Everything else in the fork's files goes. Main reviews the keep list against the target before any code. +- Code outside a fork's own files is reached only through the declared interfaces. A missing interface is an interface change, which goes to main. +- Deletion is safe by construction: code outside a declared interface has no caller outside its subsystem, so a fork deletes its old machinery outright; integration finds only callers that should use an interface. +- Each diff review checks calls into pre-existing code against the fork's keep list. A call off the list is a departure. + +## Flow + +call → levels (`transform::call`: newest level's rule) → row plan → 1 builder: every call builds a node and waits, so every pending value has its origin (a read replaces a node with its result, except downstream of a data error); nothing in the graph computes at a call, and the reads a call makes of its own operands force them (part 8). A read forces what it needs: its graph's plan from the 1 cache (keyed by structure, leaves as descriptions; a lone node is a graph of 1); a miss schedules (gather → rewrite → order → stages → compile? → plan); refill where lengths changed → run. Data errors: the session's slot, raised at checkpoints. Outside a recording an effect runs at its call, in order among effects (a token); an effect forces only what it reads. A trace is no mode of dispatch: it records the graph built anyway; its end = the scheduler up to the plan, kept as a recording. Replay = the plan run on new arguments, skipping dispatch. + +## Role models + +- tinygrad: 1 lazy graph of interned nodes (`UOp`); every call builds and waits, only a read computes (`realize`); running a graph separate from reading its result (`realize()`, `numpy()`); a rule matcher indexed by root op with early rejection (`PatternMatcher`); stage ends by its indexing rule (`schedule/indexing.py`); compiled kernels cached by structure; recordings from real runs replayed (TinyJit), effects as ordered nodes where TinyJit runs them only while capturing. +- JAX: 1 entry, `Primitive.bind`, interpreted by the innermost level (its trace stack: `Derivative` as `JVPTrace`, `Batched` as `BatchTrace`); derivative rules applied as calls run, linear nodes and their transpose (`jvp`, `linearize`, `transpose`); batching rules beside each primitive (`vmap`); loops carrying batching, JVP and transpose rules beside the node (`scan`, `while_loop`); a select's input gate (`cond` under `grad`); ordered effects (a token threaded through effects only); explicit random state; a cache keyed by signature (`jit`, on the function object; BPL keys a dfn by its definition, part 10) and 1 compiled program per primitive and signature (eager `apply_primitive`), which a read's cached plan follows; older levels' values entering as opaque inputs (lift). Not needed: JAX's buckets and padding for shapes, which exist because XLA recompiles for each shape; BPL's kernels take counts at run time and compiled code is cached by structure, so a new shape costs at most a plan, never a compile. +- Also: XLA (speed rules after the transforms; a cheap producer recomputed in each consumer), Dex (`for` loops, reverse mode through them), DuckDB (ranges as statistics; segments as list vectors), Dynamo (a retrace limit of 8). +- Not taken: tinygrad's gradient after the fact (a read before the loss would cut it); JAX's abstract tracers (BPL traces real runs, decision 6.4) and its eager evaluation (BPL is lazy throughout). + +## Parts + +Each: job; callers; not (what must not exist). + +1. Values (`array.rs`, `array/storage.rs`). `Value`: atom | array (`ArrayData`: compact buffer; segmented: 1 leaf buffer + an entry (offset, length) per item; mixed: host kinds only; pending: a node) | pending atom | carried (a level's value, dual or batch: `Value::Carried`, 1 home). A view of a stored array: a layout over its buffer, no node; of a pending value: a `Copy` node. Segmented storage is built where made (the gather finish applies the compact rule below); writes keep it. Every host read of a node's items passes 1 chokepoint (`ArrayData::storage`, `Node::into_storage`): stops traces, refuses linear reads. A partial read of a pending value (an item, a part) is a `Copy` node read through the chokepoint; the scheduler computes only what it reads, as tinygrad pushes a shrink into its kernel. Kinds in the graph: Boolean, fixed and ordinary integers, floats of 16, 32 and 64 bits, complex, characters; on the host: rationals, big integers, arrays mixing characters with numbers or exact with inexact, functions, records as containers. A record's field or a host array's item that is a compact array enters the graph as a leaf when read. A nested array may hold a pending atom as an item, as a pytree holds a tracer; compact storage resolves one. Segmented: an entry is a contiguous (offset, length) run of 1 leaf buffer, no stride (strided cells are copied into the leaf), with each item's shape for items of rank 2 or more; items of 1 shape need no entries (stacked); a leaf is an ordinary array value (stored, a view, pending) and can itself be segmented; entries are an integer operand, which a recording holds as 1 input node; an empty segmented array keeps its prototype item. Made by the compact rule: every item an array of 1 rank and storage kind, no keys or names on its axes; any other nested array stays mixed, and the walk writes through it level by level (part 16). Pervasive calls between segmented arrays: segmented agreement (each item's lengths checked when the call runs; length-1 items expand; LENGTH ERROR names the call). Frequent readers read segments directly (Each, enlist, mix, pick, first, depth, match, search, unique, sorts, display, Python, `•csv`, `•json`); ragged cells compare through 1 comparator in rustymath `search` (a leaf slice with its shape: equality, hash, `array_order`'s order). A leaf buffer: host memory the allocator owns; later a mapped file or GPU memory (part 23). Not: caches holding items twice; packing per producer; reads past the chokepoint; carriers in storage (`Data::Carried`); level code in storage. +2. Descriptions (`program::Desc`; JAX abstract eval). Kind, atom flag, layout, range. Computed only by `Step::describe` from operands' descriptions. Kinds: Boolean; fixed integer of a width; ordinary integer; float of 16, 32 or 64 bits; complex of either width; character; weak or strong. Joining kinds: 1 home, `Kind::join` (`Storage::compact`'s width for ordinary integers joins through it). Under static kinds, results computed apart (writes, compacting selects, map assembly, `⍣` histories) join to 1 compact kind (`Widening::of_results`), as JAX gives 1 dtype to `vmap`, `where` and `scan` outputs; strands and rows keep each item's kind; dynamic kinds keep each kind. Wider accumulators come only from seeds and conversions (`0ₓ₈+/u`, `(aₓ₄)+.×bₓ₄`); no accumulator field. Kinds that depend on values (dynamic kinds): `√ * ⍟ ○` on reals (complex), `÷` of exact integers (rational, a host kind), `⌊ ⌈` and monadic `×` of floats (exact): each speculated at its call by 1 mechanism, inside and outside traces alike (part 10's speculation). Arithmetic on ordinary integers can widen too, and isn't speculated: ordinary integers stay in the graph where ranges prove a width, at that width; otherwise a host call (part 5; decision 16). Keyed and named layouts are graph values: in keyed agreement a key missing on one side reads as the fill (selecting a missing key is an INDEX error: part 20); keyed agreement is a node whose maps and length come from the keys at run time; a cache key compares key sets by identity before contents; a layout's renderer travels in descriptions. Ranges (DuckDB statistics): each numeric node's range comes from its object's interval rule over its operands' ranges; a leaf's when made, or unknown. Ranges replace rustymath's `nonnegative`, `total_on` and `keeps` facts and descriptions' `nonnegative` mark. They keep `√` and `⍟` real with no speculation where arguments are in range, keep ordinary integers in the graph where they can't overflow, drop exact folds' `fits` checks, and drop a gather's bounds check where its index range lies inside its axis. A plan that relies on a leaf's range (a sign, an integer width, an index inside its axis) keeps each such fact as a speculation box, checked on every hit against the leaf's carried range and the run's lengths; a leaf of unknown range fails the box, under speculation's policy (part 10). An ordinary integer's proved storage width is part of its kind, so keys hold it. A loop carry's range widens to unknown at the loop's head unless the body provably keeps it (abstract interpretation). A fold's range over n items uses its bucket's bound for n (part 10). Not: a description from a value's `layout()` in a trace; parallel description helpers; a 2nd shape rule (`Step::extents`); `narrows`. +3. Graph (`graph.rs`; tinygrad `UOp`). `graph::Node`: a step, its operands each read through a rustymath `Mapping`, its `Desc`; or a leaf: an input (a trace argument or a write root: a key position, no step) or a computed buffer. Interned by structure (`keeping`), leaves included. A view = a `Copy` node composed into its readers' maps. Forced → `result`: a read replaces the node with its result and drops its step and operands (tinygrad's `realize`), so later graphs read it as a leaf and no rule matches across it; a run that sets the error slot replaces no node downstream of the failing one (Measures). Live graph → `program::Program` by 1 conversion; the scheduler, rules, recordings and `verify` read only `Program`. A view is stored, at 1 copy, only for a reader whose kernel can't read through a map. A chain of copies composes into 1 map by 1 home (`Mapping::composed`; `Steps::through` composes by the inner array's axes where the maps don't compose affinely); 1 test decides whether a reader reads a copy's source through its map in place (`reads_through`). A shared pending copy whose readers' maps don't compose: each reader reads the source through both maps; nothing stores it. Not: node builders outside dispatch; scheduling the live graph directly. +4. Steps (`program::Step`): the IR. `Map Convert Copy Gather Fold Product Select At Picks Join Call Draw Effect Loop Index` (`Index`: an affine index step). A variant: `describe`, `run::step`, `glyph`, `operands!`. A step carries its parameters and keeps its origin (primitive, parameters), from which a recording refills its descriptions and maps at replay (part 10); a route is a step, never a builder. A new job takes an existing step where one serves. Batching, JVP and transpose rules: rows, or beside `Loop`; never in steps. Index steps: `⍳` and `•index` give affine index steps, generated, never stored; `+ -`, negation and `×` by a constant keep an index affine; selection plans its `Steps` from the coefficients (`Stride`, `Rotated` for wrapping, `Padded` with `•edge`'s modes), so clamped and reflected reads aren't gathers; `Table::affine` stays only for index arrays that are data; a fill is a parameter of a `Padded` map. `Call` runs a planned primitive that no block stage runs (a sort, a grade, a search, `⌹`) on forced operands, in a stage of its own. `Effect` reads the token of the effect before it and its own operands, so effects keep their order among themselves and nothing else waits on them (JAX's ordered effects); the scheduler ends a stage at it. A step that reads a numeric setting (a comparison's tolerance, Mathish mode) carries it as a parameter, so a later `•prefs` change can't change a pending node. `Join` is the gather from several sources: each item read from 1 source at a position; positions from running the moving primitive on ids; its transpose adds each item's cotangent back into its source; it serves strands, `,`, `⍪`, compaction, Under's writes and joins' transposes. +5. Dispatch (`Operation::call`; JAX `Primitive.bind`; tinygrad's lazy `UOp`). 1 entry for every call of an operation on values: primitives (pairing axis, matrices, at), folds and products by primitives, selects, loops, strands and array notation (`Join`), conversions, draws, effects. Order: the newest level among the args takes it (its rule calls dispatch for its parts) → the row's plan, once: operand roles (data | read at the call: a parameter, values deciding a length; each a read, part 8; `Facts::reads`; a kind that depends on values is speculated, part 2, never read at the call), the step (`Map Copy Gather Fold Product At Join Picks Select Convert Draw Effect Loop Index`, else `Call`: the row's own code runs at the node), result length and bytes → 1 builder (`fusion::built`; `Step::describe` gives its `Desc`, computed only here): every call builds a node and waits, so every pending value has its origin; nothing in the graph computes at a call (tinygrad); a read forces what it needs, the reads a call makes of its own operands included (part 8's list). A host kind (rationals, big integers, mixed arrays, functions, records) isn't in the graph: a call on host values runs its row's code on them. A build too costly for small calls is fixed by a cheaper build (the cost model's dispatch budget), never by a 2nd route. Data errors aren't a reason to compute: executors set the session's error slot (Measures). Engine code calls dispatch, never a builder. A read of a pending value's item or part: a `Copy` through the same builder. Check: dispatch and a read against the cost model's budgets ("Cost model"). A fold of a pending array to an atom gives a pending atom; a call reading it reads its node. Not: a 2nd entry (today `call_axis` beside `Operation::call`'s `node`); a call computed without its node (eager and traced as 2 modes, `fusion::builds`); a force policy choosing per call (`fusion::gate`'s now or wait; the cost rule, `fusion::waits`, `wait_limit`); gating or building in own code (`structural::copying`, `pervasive_apply`'s `defer`, `Operation::node`'s arm per kind, `fusion::node` from `operators.rs`, `derivative.rs`, `distribution.rs`); own code beside a step's executor (`pervasive_apply`, `structural.rs` beside `run::step`'s `Map`, `Copy`); rewrites at dispatch (`taken_right`, the selecting form's copy for the walk); rule marks; pending atoms read by special case (`views`, `pending_atom`): roles replace them. +6. Rows (`primitive.rs`) and rustymath objects (`facts.rs`, `recipe.rs`, `structure.rs`, `derivative.rs`). Row: its object, plan (kind rule; shape rule over known lengths), batching rule (`cells`), Under selection, linear map and its transpose, fold identity, cost, row facts (`passes`, `alternates`, `first_axis`). Object: 1 row holding its formula (once), partials, ranges, fold properties; kernels instantiate the formula over their `Ops` (recipe); an instantiation slower than a hand-written kernel is fixed in the formula or its `Ops`, never by a 2nd writing. Defined forms (monadic `≤ ≥ ⊼ ⊽`) need no rules: their code calls other primitives, which build nodes. Not: rules kept by kind across files; an object's facts in parallel tables (`rules!`, `Partials`, recipe bodies); a name check standing in for a fact; a 2nd writing of an object's math (`lowered_op`, kernels' scalar code beside a formula). +7. Rewrite engine (`program/rules.rs`; tinygrad `PatternMatcher`). 1 table: BPL pattern rows and structural rules (fold of a copy, contraction, sliding fold, copy commute). A rule: a root key (data: step kind, root primitive) that rejects; 1 match giving bindings; 1 builder from them. Runs in the scheduler's pipeline on `Program`s after `∂` and batching, a read of a lone node included (part 8). Every pending value has its origin, so a rule matches in every spelling: `⍟1+x` → `log1p` from 1 row, across statements, bindings, dfns and trains; a read replaces a node with its result (part 3), so a rule doesn't match across a read of its intermediate. Speed rules run after `∂` and batching (XLA's simplifier after JAX's transforms), so the hidden specials they introduce (partial sorts, ranks, `log1p`, `expm1`) need only plans and kernels; a pattern also matches its primitives' batched forms, which carry an axis. A rule may change last bits, never leave the documented accuracy bounds (kmeans' cancelling rule is out: decision 14); each rule has a reason that depends on no model, and a reference case. The table: selections of a grade or sort (`k↑⍋⍵`, `↑⍋⍵`, `i⌷⍋⍵`, `i⊃⍋⍵`, for `⍋ ⍒ < >`) as partial sorts; `⍋⍋⍵`, `⍋⍒⍵` as ranks; `⍟1+⍵` as `log1p` and `¯1+*⍵` as `expm1` (accuracy); mark rows, each a conversion node over its call's node, matched on nodes like any row (`fₐ` builds `(f args)ₐ`, part 21): `(√⍵)ₐ (⍟⍵)ₐ (⍺*⍵)ₐ (⍺○⍵)ₐ (⍺○⁻¹⍵)ₐ` as real-form specials, `(⌊⍵)ₓ (⌈⍵)ₓ (⍺÷⍵)ₓ` as exact forms; a `+` fold of `×` whose operands read through affine maps as 1 product (index notation's level 3); a fold of a copy that repeats items as a fold of its source; a dfn fold whose body is 1 associative primitive (`{⍺+⍵}/`) as that primitive's fold; `J+.×v` as a JVP and `u+.×J` as a VJP, where `J` is a Jacobian `∂` built and nothing has computed. Not rules: a combination of 1 call matched by its function's structure is its operator's own algorithm (monadic `-.×`, `+.×`: `primitive_expansion`); Key's counts and folds by group come from batching over segments (part 12). Not: a test written twice (a pre-check beside the match); a rule tried from 2 places; 1 rule in 2 directions written as 2 rules; marks and their machinery (`marks`, `UNMARKED`, `FunctionData.needs`, `call_marked`, `rules::pairs`, `rules::at_once`); mark rows bound to function values by spelling (`FunctionData.rows`, `Function::new`'s row filling, `operators::marked`, `spelled`, `marker`). +8. Scheduler (forcing: `graph.rs`, `program.rs`, `program/run.rs`). 1 pipeline, `schedule(Program) -> Plan` then run: gather, rewrite, order, stage ends (tinygrad `indexing.py`), outputs, cheap chains recomputed, folds sharing stages, select lowering (masked | compacting, from descriptions), compile decision, run. The same pipeline serves forcing, a trace's end and replay planning. Forcing records data errors in the session's error slot and never fails. A read runs its graph's plan from the 1 cache (part 10), keyed by the graph's structure, literals by value (tinygrad's `CONST`) and other leaves as descriptions (a length class and bucket per axis) and each length the data decides inside it by its class and bucket (part 10), with the plan's speculation boxes checked on each hit (part 2), and refilled where lengths changed; only a miss schedules, so a read of a graph seen before costs a key, a probe and the run (cost model). A lone node is a graph of 1 (JAX's 1 compiled program per primitive and signature; tinygrad's kernels cached by structure). Stage ends (tinygrad): a node whose length the data decides ends its stage, its count read before the steps after it are laid out; a value with 1 consumer fuses with it; consumers that index a value alike fuse with it; consumers that index it differently store it, unless it's recomputed. Recomputed in each consumer's stage: an elementwise chain over inputs and recomputed values, each node at most 1 copy in its row's `cost` (XLA's producer fusion, AOTAutograd's recomputation); reverse mode's residuals take the same rule. A value's life past the graph: reference counts, exact by releasing each name and temporary at its last use. An uncompiled stage stores outputs in its own pass. A fold whose rows hold more than 1 item adds each row into column totals (NumPy `sum(axis=0)`), a compiled kernel into its piece's totals; `inner == 1` keeps totals in registers. Compile decision: a kernel already compiled, or items × passes saved × saving per item × expected runs (loop counts, a recording's reuse) above the compile cost. Forcing reads, the 1 list: display, conversion to Python or JavaScript, writers (`•json⁻¹`); an observing read (a guard's predicate, a count or length, an item read one at a time, `⍎`'s text); a read a call makes of its own operands (part 5's roles: a parameter, values deciding a length; selective assignment's positions, part 16; a call on host values, part 5; an ordinary-integer call with no proved width, part 2); an effect, which outside a recording runs at its call (what it reads, after the effects before it); the memory limit, in bytes: a pending graph whose leaves kept alive (leaf buffers that only the pending graph still references) pass `MAX_KEPT` (8) times its largest array, or whose own nodes pass 1 MiB (about 7,000 nodes; one core's L2), never a count of calls. A top-level statement forces nothing at its end; destructuring forces nothing. Running a graph and reading its result are separate steps (tinygrad's `realize()`, `numpy()`): on a GPU a run submits work and a read waits for its readback. A fold whose totals feed later steps runs with them cell by cell where a cell fits the stage buffer (`fusion::BUFFERED`): each cell folds, then the later steps read its totals and its items from cache; the plan chooses this schedule, never the compile decision. Not: eager executors beside it (`fusion::fold`); a step routed around it to its kernel by kind; lone-call paths beside the pipeline (`Node::compute`'s, `copy_of`, `own_passes`, `fold_pass`); a plan cache beside the 1 cache (`PLANS`); a session registry forcing pending work at effects (`graph::barrier`); executor arms calling own code (`pervasive_apply`, `Operation::run`, `folded_by`). +9. Block stages, JIT (`program/blocks.rs`, `jit.rs`, rustymath `gather.rs`, `agreement.rs`, `reduce.rs`). A stage runs its steps a block of items at a time; it compiles where items × passes saved × saving per item exceeds the compile cost (1 formula, the plan's). Reads go through `Mapping`s by 1 rustymath read plan (periods, tiling, runs), whose consumer passes its run length, read by rustymath's kernels, folds and block stages alike. Lowering reads the object's formula. Compiled code: `jit::KERNELS` owns it (identical kernels compile once); a stage holds a shared handle (`Arc`) fetched once at plan time, a reference, not a 2nd copy, so no lookup per run. Block stages read Booleans, fixed integers, ordinary integers at a proved width, f32, f64, and characters as `i32` code points (`characters::as_codes`, no copy). A step giving characters, f16 steps and complex steps run on rustymath's whole-array kernels outside block stages (main's decision 13); a chain doesn't fuse across them. Every step rounds to its type in a stage or a compiled kernel, as when it runs alone (f16 computes in f32: rustymath's DEV.md); compiled code may reorder a fold's or a product's additions and fuse `a×b+c`, never change integer results, kinds, rounding points, NaN or infinity behaviour, or a dfn fold's order. The JIT generates code with Cranelift, natively only: the browser runs block stages uncompiled until the GPU stage; LLVM is never a target, and the built-in JIT works without an external backend (part 23). Not: a stage's own cache of compiled code (`Compiling.code`); kernel sightings as a compile rule (`Kernel::seen`); basedpl copies of rustymath's read planning; thresholds per consumer; a primitive → machine-op table (`lowered_op`); fold admission re-derived beside row facts. +10. Traces, recordings, cache (`program/trace.rs`, `program.rs` `Programs` and `tacit`, `program/run.rs`). Trace: no mode of dispatch: the graph every call builds anyway, recorded at its end, with capture of free values and opaque inputs (enter unforced; no producer facts). Recording: the scheduled program, `Key`, captured free values, speculation boxes (kinds; leaf range facts, part 2). 1 cache: 1 store keyed by function structure: a dfn by its `Definition`, which every closure of it shares (where its `Programs` live today), never by a closure instance, so a dfn made again on each evaluation (`{⍵+1}¨x` inside a function) reuses its recordings, where a cache keyed by the function object (JAX's `jit`) traces again; its captured free values are recording facts, checked at replay; its entries go when its `Definition` goes (a weak reference); entries no function value owns (a forced graph's plan) are evicted least recently used past 256 entries (a plan is a few kilobytes; a miss costs a schedule); 1 entry per function value, holding recordings, stops, select plans and loop bodies; a forced graph's plan sits in the same store under its graph's structure, leaves as descriptions (part 8). A transformed call is its own function value (part 19: `f∂`, framed, JVP step, VJP step), so its recordings sit in its own entry, never `f`'s, and `Key` needs no transform flags. A call under a level is that level's rule, recorded as the transformed function's run; values of older levels enter as opaque inputs (JAX's lift). Planning run: a case built on a stand-in for its argument (a leaf holding only a description, no items: a read of it ends the run), never forced, ending at its 1st stop; 1 route, inside and outside traces. Key: per argument axis, a length class (0, 1, more: length 1 changes broadcasting) and a power-of-two bucket B (lengths above B/2), a cache-key policy only; a trace records at concrete lengths. Replay checks key, free values and boxes; at entry it refills descriptions and maps from each step's origin (part 4) at the run's concrete lengths, through the row-plan builders the plan used (1 home for map derivation), and reuses the bucket's plan; engine code sees only concrete lengths. A free value read as structure is captured, and a mismatch retraces; a count from the arguments (`n↑`, `n⍴`, a window size) refills partway through the replay; a guard on a traced value stops. Refill runs only where lengths differ from the plan's last refill, and is a subset of planning (descriptions and maps only: no gather, rewrite, order, stage ends or compile decision; "Cost model": 130 against 270 instructions a node). Buckets serve plan reuse only: a new shape costs at most a plan, never a compile, since kernels take counts at run time and compiled code is cached by structure (JAX buckets and pads because XLA recompiles for each shape). Buckets, not a plan per exact shape: a plan's cost-based choices (the compile decision, the cheaper of 2 reads, recomputing a cheap chain) stay within 2× across a bucket, and a growing carry refills each step (265·N) where a plan per shape would schedule each step (405·N). A replay failing its check inside forcing runs the body as ordinary code, never as an error. Recorded bodies: a loop's body, a map's operand on framed values, `∂`'s operand; other code builds its graph unrecorded, its reads running plans from the same store (parts 5, 8). A trace runs on real values (decision 6.4). The key also records which axes' lengths were equal, and the axis names the trace drew, numbered by first appearance across constants, arguments and free values (`•index` draws a fresh name per call; a replay substitutes the current names, keys included). A trace builds a nested argument from its leaves' input values: each leaf is traced. Speculation (decision 3), 1 mechanism inside and outside traces: a node whose kind depends on values takes the likely kind (its range's where that decides it; else, in a recording, the 1st run's; else the real or exact kind: outside recordings nothing remembers a run, since the 1 cache holds outcomes only for recordings) and its kernel checks it (`√` flags a negative); a read whose check fails gives that node the other kind (a host call where that kind is a host kind), and every pending node reading it, in that read's graph or another, goes back through dispatch from its origin when a read reaches it (forcing re-enters dispatch for these nodes alone); the nodes after it are then planned again; in a recording a different kind retraces, counting toward the retrace limit; a stop that a failed speculation causes keeps no outcome for its signature; a replay failing its check runs that call as ordinary code and keeps its entry, a 2nd failure in a row drops the entry and traces again, a replay resets the count (`FAILS` = 2); once a trace has recorded an effect it no longer speculates. Retrace limit: a definition recording past its limit (more signatures in a row than it keeps, no replay between) keeps no new recording; kept ones serve the calls they fit, others run as ordinary code (Dynamo's 8). A recording stops at: a host read of a value that depends on the arguments and decides control flow or a structure other than a length (a guard's predicate, a rank, an axis or a permutation taken from items, an item read one at a time); `⍎`; an error guard (`::`); recursion; a write to a name outside the body (under `∂` a differentiated value written to an outer name is an error, as JAX raises when a tracer escapes); a host-kind value from the arguments; an effect that changes bindings or settings or gives a value read as structure (part 17); a drawn name that becomes characters. No stop: shapes and kinds (in the key); a length the data decides (`⍸`, `#` by a mask, `∪`, monadic `⊤`, Key's groups, a compacting select's counts, a count taken from items: `n↑`, `n⍴`, a window size, `•index n`): a replay computes up to its node and reads the count, which then keys the steps after it as an argument's length does: the plan records its length class, bucket and equalities with other lengths, and the replay compares them as at entry; a match refills the steps after it from their origins (`Plan::refill` from that step), and a mismatch schedules the rest from that node and keeps that plan under the key extended by the count's class and bucket; GPU memory bounds for such lengths wait for the GPU stage, given by users as JAX's `size=` is; a guard on a framed value (a select, part 14); output effects and draws (nodes, part 17). A plan choice that depends on an exact length (an unrolled short fold, a one-item read chosen by a repeat's length, grouping by equal lengths) is never made on a bucketed axis: refill derives descriptions and maps only. A buffer that a growing carry writes is allocated at its bucket's bound, so an append stays in place. Not: 2 recording stores (a dfn's `Programs`, `program::tacit`'s map); a trace as a dispatch mode (`in_trace`, `fusion::builds` branches); trace state in `transform.rs`; a 2nd cache; a cache keyed by a closure instance; a 2nd route for kinds that depend on values (read at the call outside traces, speculated inside); plans rebuilt per call; recordings assuming producer facts; transform flags in `Key` or `serve` (`frame`, `step`, `held`); a lookup declined under a level; bounded extents (extent nodes, map templates, pins, rows' `bounded` flags, `fixed()` guards, rustymath's generic `Traced` maps); a bounded rule per primitive; re-planning per shape; shape rules generic over a lengths type (`Lengths`, `Known`, `Bounded`) where every caller passes known lengths. +11. Levels (`transform.rs`; JAX traces). 2 kinds: `Derivative` (duals, tags), `Batched` (framed values). A call goes to the newest level among its arguments; a level's rule builds through dispatch. Levels nest, any kind inside any, as JAX's traces do: a map inside a batched run or inside a `∂` begins its own `Batched` level, as does a framed body (part 13). The running batched level is the innermost; a batch's level is found by its id. A level's rule takes a value of an older level as an unbatched operand, and its own calls pass that value on to its level (JAX's lift). Guard cases split the innermost run's cells. A nested level's extra rule pass costs only a map's 1st run, which the cache records. Not: `Trace`, `Record`, `Label`; engine code calling into a level; carriers beyond dual and batch; widening a running run (`Entry::Widen`, a `Case` with no definition, `batch::widened`, `Levels::widened`), a 2nd route for a map inside a map that nesting serves and correctness doesn't need; a refusal to begin a batched run inside one (`Kind::admits_batch`), which sends a map inside a `∂` inside a map to a call per cell. +12. Maps (`batch.rs`, 1 map entry; JAX `vmap`). Each, Rank, outer product, stencil, `f.g`, Key, Partition, ragged Each: 1 entry runs the operand once on framed values under `Batched`; rows' batching rules apply; ragged cells and Key run over segmented arrays; draws take their place in a map from the levels (part 17). Under `Batched` a framed value keeps only the frame axes it depends on (JAX's `not_mapped`): its whole keeps each frame axis in order, length 1 where it doesn't vary; a value whose cells must stay apart (a draw, a compacting select's mask) is laid over the whole frame first. Segmented maps: elementwise steps run on the leaf buffer (DuckDB's `list_transform`); folds and scans run by segment; a primitive with no segmented rule runs once per segment and joins the results; Key keeps each row's class number, so folds and counts by group reach `fold_groups` and `class_counts` with no gather. A map whose body returns an axis name it drew runs cell by cell, with a nested result. Not: evaluators per operator (Rank's shortcuts, Key by spelling); own-code arms by name in `batch::call`; per-cell fallback other than a stopped run. +13. Loop node (`Step::Loop`; Dex `for`, JAX `scan`, `while_loop`). `⍣`, dfn folds, scans. Body: a function value, replayed from its recording in the cache. Runs as an in-item kernel, a one-item kernel, or the host runner replaying the plan. Rules beside the node: batching, JVP, transpose (residuals: a host stack of per-iteration values, its length known when the forward loop ends; the reverse runner replays the VJP step once per iteration; recomputing, `Until::Rerun` today, only as checkpointing past the memory budget), substitution. Each builds a loop node whose body is the transformed function value, recorded on its 1st run, as JAX's rules bind a new `scan`/`while` on `batch_jaxpr`/`jvp_jaxpr`/the transposed jaxpr: batching → `f` framed (a nested `Batched` run over the leading frame axes); JVP → `f`'s JVP step; transpose → its VJP step. BPL re-runs functions where JAX transforms jaxprs (decision 6.4), so a transformed body is a function value (an operator applied to `f`, part 19: 3 internal operator kinds, framed, JVP step and VJP step; a kind's parameters are structure only: frame axes, packing, direction; values such as a fixed argument and its tangent enter as arguments or captured free values). The next level's rule then takes a transformed body like any body: `∂` of a fold over batched lanes (core 3526), `∂` under a map, nested `∂`. A loop under levels takes the newest level's rule, nothing else. A framed body begins a nested `Batched` level (part 11). Masked exit (batched predicate, `⍣∞`; JAX batches every carry): every lane steps until all exit; a finished lane keeps its carry; a masked loop that sets the error slot clears it and reruns the whole read, the loop starting again from its initial carry dropping finished lanes each step (refill), which raises only for live lanes; admitted wherever the body is effect-free. Inputs: the initial carry, values constant across iterations (`⍣`'s left argument), a fold's or scan's items; outputs: the final carry, the stacked states of a history or a scan. Exits: a count; `∞` once |new−old| ≤ t×max(|new|,|old|) at `Numeric::convergence()`'s tolerance; a predicate graph on the new and old states. A count array runs 1 loop per sign up to its largest count, storing the state at each requested count; negative counts and `¯∞` loop over `f⁻¹`'s body. A history is regular when the carry's shape is fixed, segmented when it changes. The in-item kernel takes an elementwise body (pervasive in the carry, other arrays read item by item, no folds, structural steps or effects) with a count or `∞` exit; under `∞` each item stops when it converges. A loop compiles by the scheduler's rule on its total work; with a count the data decides, once its iterations so far cost more than compiling (LuaJIT, V8). A failed kind check flags its iteration: a one-item kernel exits with the iteration and its carry, a host loop stops there, and the host reruns that iteration on real data or retraces for the new kind; an elementwise kernel's whole loop reruns from its initial carry. A body with effects runs only on the host runner, its effects in order on every iteration. A body that can't record (recursion; an inverse that reads values on the host: `⍸⁻¹ ↕⁻¹`, `,⁻¹` along an axis, `⊥⁻¹`) runs as ordinary code on every iteration on the host runner. An exact integer carry under dynamic kinds stays in the loop where its widened range proves a width; otherwise its arithmetic runs on the host every iteration. Checkpointing: past the memory budget, keep every k-th carry, drop residuals, and recompute each segment's residuals from its 1st carry (Futhark's stripmining: about √N carries stored, 1 extra forward run); a count fixed at the start sets k; with a count the data decides, drop residuals when the stack 1st fills the budget, then keep every other stored carry each time it fills. Not: several steppers; a Rust step (`Stepper`, `items_loop`'s `frame` branch) as a body; a frame or linearization point threaded through the runner and its helpers as data; host loops beside the runner; per-caller variants of the loop's JVP or transpose. +14. Selects (`Step::Select`). `⍚`, `f@[p]`, guards on framed values. Case plans from descriptions by 1 planning route (part 10), kept in the case's cache entry, inside and outside traces alike. Masked where every case reads only its own item, has no effect and gives 1 layout and kind (dynamic kinds: no value-dependent kind); a masked case that sets the error slot (which names the node) clears it and reruns the whole read with that select compacting, which raises only for items that picked the case and records any error the masked case hid; compacting otherwise (effects, reads across items, differing layouts). Under `∂`, a masked case reads each differentiated input it reads, closure included, through the input gate `select(mask, x, stop_gradient(x))` (JAX's double-`where` fix: no zero cotangent times an infinite slope). A mask with fewer axes than its argument (1 Boolean per row) selects by gathering whole rows. Compaction gathers each case's cells, runs the case and scatters the results; it transposes through the gather from several sources and its scatter-add. On a GPU a select is a per-lane branch. Not: admission of cases by spelling; planning on every call; a 2nd planning route outside traces; range narrowing for admission (`Desc.narrows`); compaction as its own executor beside `Step::Select`'s lowering. +15. `∂` (`derivative.rs`, `linear.rs`; JAX `jvp`, `linearize`, `transpose`). `Derivative` level and tags; tangents are linear nodes, arrays of their primal's kind and width under the usual promotion (no dual storage kind; exact primals differentiate exactly, `{⍵×⍵}∂3ₓ` → `6ₓ`; a seed is 1 of its argument's kind); JVPs from rustymath partials through rows; transpose = 1 pass over linear nodes, 1 arm per step kind, keyed on row facts; a Jacobian = the linearized function (part 19's JVP or VJP step, a function value) mapped over unit seeds by the 1 map entry; loops through the loop node's rules. Tangents: linear nodes over a seed leaf; forward mode binds the seed and the scheduler computes tangents with the primal; reverse mode leaves it unbound and transposes. A linear node holds its `∂` call's level (on the node, never in a `Desc` or a key); nothing forces it; the chokepoint refuses reading it; it may hold an older level's value as a constant operand. Kept: tags, derivatives stripped from escaping values, `f∂←g` as a JVP rule that reverse mode transposes, `•detach`, rustymath's partials recorded as primitive calls. A call whose result has a zero tangent (a grade, ranks, positions, a comparison) runs on its arguments' primals: 1 home, in `∂`'s dispatch. A map needs no rules: its calls are batched primitives; a shared input reaches each call spread over the frame (`agreement::spread`), whose transpose sums over the frame. A copy's transpose is `At` with `+` into zeros through the copy's map (JAX's 1 scatter-add); its plan picks the kernel from the run's map (a fold along step 0, a copy through an inverted table, a scatter-add). A read's cotangent: 1 home (`linear::read_back`: a gather where each source item is read at most once, else a scatter-add). A selection transposes by name through the same selection plan as its forward gather (JAX transposes a gather with its dimension numbers). Joins transpose through the gather from several sources. A loop's JVP is a loop over its body's JVP step, whose exit tests read primal values only. Reverse: partial evaluation splits the body by the linear mark; the primal part runs forward and stacks each iteration's residuals beside its carry (only nonlinear steps save inputs); the reverse loop runs the transposed linear body from the last iteration to the 1st, carrying the cotangents of values constant across iterations; a fold's or scan's items get cotangents on each iteration, a history's add in at their iteration; `⍣∞` and `⍣` with a condition stack every iteration (decision 6.3). Choices that depend on lengths (forward mode for 1 number; a Jacobian by rows or by columns, whichever count is smaller) are made at the trace, for its bucket. Complex numbers stay UNSUPPORTED under `∂` until a gradient convention is chosen. Not: an evaluator of its own; reverse loops per item; Jacobians built entry by entry; closures as map operands. +16. Writes (`selection.rs`, `eval/assign.rs`, compiled targets in `eval/binder.rs`, `eval/tree.rs`). 1 walk (`selection::selecting`) composes `g`'s nodes back to the root, through a leaf per item type for nested values; every write is 1 `At` node, inside and outside traces, written in place where the scheduler chooses. Direct paths (`⌷ .[] ⊃` chains) are compositions inside the walk. Key extension (`m.k←v`, a missing key): the walk reaches the container; the `At` writes the container joined with the new item. Positions (`f@[p]`, a mask): a selection by `p`, composed into the walk. A `g` that computes rather than selects writes through `g⁻¹`. A `g` that adds items (`{⍵,0}`) raises "cannot assign to an item that isn't from the original"; a catenation of selections works (`10@{(1↑⍵),¯1↑⍵}`, `((1↑a),¯1↑a)←8 9`). A selection into a segmented array takes 2 kinds of position: an item's entry and a leaf item's offset. At a repeated position, which value a replacing write keeps isn't promised, keyed or not (decisions 6.2, 15); an accumulating write (`y.[dst]+←v`) adds every repeat. Selective assignment checks every position before it changes anything: an error leaves the name unchanged; positions force, values may stay pending; in a trace the `At` node checks its positions when it runs, before it writes. An item whose kind stays on the host is a target taken whole only, found by identity. Not: a direct route beside the walk; labels; write loops per path or per target kind; write executors beside `At`'s (`write_targets`, `pervasive::combine_at`); appends chosen by spelling; host writes beside the `At` node (today `At` only when `fusion::builds`). +17. Effects, draws. Effects and draws are always nodes, inside and outside traces. Every effect is an ordered node (output, `⍕` of a carried value, system effects, time, input `⎕`); effects run in order among themselves, each taking the token of the effect before it, and an effect forces only what it reads (JAX's ordered effects; when other pending work runs isn't promised). Outside a recording every effect runs at its call, after the effects before it; a call whose body holds an effect is itself an ordered effect. An effect that changes bindings or settings, or gives a value read as structure (input `⎕`, `•load`, `⍎`, settings changes), ends a trace's recording; pending nodes keep the settings they were built with (part 4). A draw is a `Draw` node, computed when a read reaches it. Generators' state is a graph input, 1 home; draws use place-based counter streams (rustymath `random.rs`); map nesting comes from the levels. Effect kinds: `Acts` (output, input `⎕`, `⍕` of a carried value, system functions that read or write files, the network, the clock or the terminal, or make something new on each call: `•delay •nput •fetch •nget •time •load •js •uuid`), `Catches` (an error guard, which forces the work built inside it at its exit; a call on prototypes), `Writes` (a write to a binding outside a level's frames). An effect node takes the previous effect's token; a stage ends at each effect node; a training loop that prints its loss stays 1 cached loop that prints on every iteration. A `•rand` generator's state and the session generator's that `¿` draws from are graph inputs: a generator's key joins the recording's key and its count is an input when a replay starts, moved on by the body's number of draws, so every replay draws new numbers; `•prefs` with a seed changes the key. A `Draw` step draws at stream number c + k (c the count, k its place among the body's draws). A batched draw is 1 roll over the whole frame, each cell reading its own stream: a cell's 1st item is word p×w of 1 shared stream (p the cell's place, w the words a cell draws). Not: host draws or effects beside their nodes (`distribution::drawing`, `trace::acting`, `transform::acts`' host run); a session registry forcing every pending value at an effect; draws stopping traces; replays repeating draws; a 2nd home for a generator's count (`Recording::counts`); a draw context beside the levels (`MAPS`); a 2nd seeded generator (`SplitMix64`). +18. Binder, executor (`eval/binder.rs`, `eval/tree.rs`). The binder compiles each statement before it runs, emitting instructions only; `tree.rs` is the 1 statement evaluator and holds the value constructors. The compiler may build values from constants through those constructors (constant folding: `⍤1` derived once): construction, not evaluation. A role change the compiler can't see is an error (decision 10), never a restart. Binding rules: (1) a call's result is a value to the binder, and so is a field read from it; a subscript's selection counts as a call: `+/÷(f 0)` is an expression; `g←f 0 ⋄ +/÷g` names a returned function; `1⍚fs x` calls a selected one. (2) A role change the compiler can't see is an error: a write that changes the role of a name outside the writing function's frame (raised at the write), and a read, later in a statement, of a name whose role that statement's write changed (raised at the read); an array binds as a function when any item is a function; `Session::assign`, the 1 home of assignment, compares old and new categories. (3) `⍎` changing a name's role while a statement that reads it runs is an error (decision 9); an unbound name binds as an array (`x+⍎"x←3"` works). An earlier assignment in the statement (`x+(x←3)`) gives the name the assigned expression's role for the rest of it. Roles are read again at each statement's start. The compiler binds ahead: dot access on a name (the field's role from `T`'s value, a recorded fact); a scope `T.(expr)` (from `T`'s keys and field roles; a computed `T` compiles its body on entry, as `⍎`'s text does); superscripts; numeric subscripts by their operand's role (`(⊃fs)₁` selects); an absent `⍺` (compiled per valence, or checked at the statement's start). A recording checks the roles of the names it reads at the start of the statement and of each group or run. A top-level statement compiles, then runs once. A call that returns no result fails when it runs. Not: evaluation inside the binder; resume machinery (`Again`); a 2nd node evaluator (`Session::resolve`); a 2nd reader of the grammar (`Pattern::read`, `dot_path`). +19. Function values (`eval.rs` `FunctionNode`; tinygrad `UOp(op, src, arg)`, JAX `JaxprEqn(primitive, params, invars)`). Leaves: primitive, system function, dfn, late-bound name, function array. Every operator application is 1 variant: its operator (an `OperatorKind`, or a dfn operator's closure) and 1 or 2 operands; `/ ⌿ \ ⍀`, `⍠`, `⁻¹`, `ᵇ` and marks are operator kinds in it. Internal kinds, for part 13's loop rules: framed (a nested `Batched` run over leading frame axes), JVP step, VJP step; their parameters are structure only (frame axes, packing, direction); values (a fixed argument, its tangent) enter as arguments or captured free values, never as key parts. A train: 1 variant, its tines. Attached rules (`f⁻¹←g`, `f∂←g`): a field of the function's data beside `environment`, as JAX's `custom_jvp` holds its rule beside its function; display, parts and calls see `f`. Operand traversal (`visit_operands`, `rebuilt`, `equal_by`, owns, environment) written once over operands. Per-kind behaviour (call, spelling, inverse, `discards`, Python name): the operator kind's entry, keyed on the kind as `Primitive::info()` keys rows. A new operator or wrapper kind = 1 kind entry, 0 node arms. Attached rules' readers: `⁻¹`, Under (`discards`, inverse), `∂`; rules aren't operands (no traversal) and count for `environment`. No needs mask: rules match on nodes (part 7). Train tines: functions or arrays (an array tine is a constant, a data input in programs). A dot read on a function (`f.name`, `f.[I]`): an operator kind, its field or index the parameter. Operator values: a primitive operator (`OperatorKind`), a dfn operator (closure), a bound operator (its right operand bound); `f₁` is a bound `⍠`, no variant of its own. Not: a variant per wrapper kind (today `Fold`, `Axis`, `Inverse`, promoted by `Function::new` out of `Modified`/`Composed`, which already hold the kind); `Modified`, `Composed` and `Derived` as 3 application variants; attached rules as a node (`Ruled`); host closures as function values; a 2nd call route for trains (tines called in place). +20. Index notation and positions (`⍳`, `•index`, rustymath selection planning). Level 2: regular selectors as strides, in rustymath's selection planning; level 3: contractions as 1 rule (part 7). Positions count from 0, negative positions from the end; no selector fills outside its axis: a position past an axis's length or a missing key raises INDEX ERROR, and only a NaN position reads the fill (keyed agreement's fill: part 2). `.[]` takes no `⍠`; a fill comes through `⌷⍠["fill":v]`. Drawn names: part 10's key; a map whose body returns one: part 12. Axis order of `g×z`: where every axis of the left operand pairs and the right has more axes, the right's order; otherwise the left's axes first. Not: range inference (`•index ∞`, decision 11); strides decided from 1 run's index values inside a recording. +21. Scripts and marks (`number.rs` `MARKS`, `syntax.rs`). Marks, 1 table (`MARKS`): `ₓ` exact, `ₓ₁ ₓ₂ ₓ₄ ₓ₈` fixed widths, `ₕ ₛ ₚ` floats of 16, 32 and 64 bits, `ₐ` real; after a function each is an operator kind (part 19), after a value a conversion node (`Mark::conversion`). A mark after a function converts each result as it's stored, in its producer's stage: `⌊ₓ₁`, `-ₓ₁`, `÷ₛ` and `⍳ₓ₁` take no 2nd pass. A marked literal group, and a superscript after a literal number (`3²`), is constant folding through the same conversion or call (part 18), never computed by the lexer or parser (today `syntax.rs`'s `mark_literals` and `strands`, under `Numeric::current()`). `ₐ`: real floats at the arguments' float width, never chosen from values (a fixed integer takes its type's float promotion: `ₓ₁ ₓ₂` float32, `ₓ₄ ₓ₈` float64; a weak literal takes its partner's; arguments with no compact join take the default width); outside the real domain the real part of the principal value (`√ₐ¯4` → `0`, `⍟ₐ¯1` → `0`, `¯8*ₐ÷3` → `1`); `fₐ` ≡ `(f args)ₐ`, a static kind under both kinds. `fₐ` builds `(f args)ₐ`, a conversion node over `f`'s node; mark rows (part 7) rewrite such a pair on nodes: `√ * ⍟ ○` on reals to specials backed by rustymath objects (`RealSqrt`, `RealLn`, `RealPow`, `RealCircle`'s codes, whose table of codes is a rustymath object, not basedpl's `with_circle!` and `with_real_circle!`), each with a real form, a complex form giving the real part, a formula (`Facts::formula`), a range rule and the real part's slopes (`RealLn` 1/x; `RealPow` below 0 those of |x|^y cos πy; `RealSqrt` and codes whose real part is constant there, 0 outside the domain), with JAX's double `where` where a case's slope is infinite; `⌊ ⌈ ÷` with `ₓ` to exact forms; any other `fₐ` converts `f`'s result. Speculation stays (decision 3); `fₐ` is the spelling guaranteed fast. Superscript digits after a function: Rank, in a separate build (`open.md`, "Agreed, left"); this release keeps today's meanings (a digit after a function repeats it, `⁻¹` inverts). Letter superscripts: `ᵀ` after an array transposes it, `ᵘ` after a function makes a scalar holding it; either on the other role is an error. +22. Reports (`explain.rs`, rustymath `events`). `•explain` reports every graph, with no size filter; users filter by `items`. Records: `fused` (stages run); `stored` (nodes stored partway, with a reason); `forced` (pending work that a read forces partway, with a reason: an observing read, a host kind, an effect's operand, a selective assignment's checks); `programs` (every recording: loop bodies, maps' operands, `∂`'s operands; events include "retraced" and "rerun", as JAX's cache-miss explanations and Dynamo's recompile logs report); `stopped` (a row per reason with calls, items and `site`, as Dynamo's `break_reasons`); `fast` (each rule applied); `fallbacks` (every run per item, cell or iteration on the host, with reasons; today's `itemwise` and `loops` records fold into it); `declined`, `accumulations`, `mixed`, `compiles`, `routes`. A reader that takes its argument whole (sorts, grades, searches, `⌹`, products) is classified by `Facts::reads`, 1 home. Route checks and the coverage matrix keep 3 requirements: a mapping stage compiles (`compiled`), a fold reads its argument inside its stage (`fused fold`), a gradient replays (`trace`). `⍝⍝⍝ route` names only these 3; every other route is checked by the absence of slow events (`fallbacks`, `stored`, `forced`, `stopped`), which fail unless allowed. Routes are rustymath's `events`, their 1 home, which basedpl's `•explain` and rustymath's Python `rm.explain()` both capture; the route list holds only routes the target keeps; `stored` reports every value a stage end stores. Reports and diagnostic tools change without agreement. +23. Later stages' requirements on the core (not built in this release). GPU: running a graph separate from reading its result; nodes as plain data; transposes that read rather than scatter where they can; readbacks for results whose shape depends on values; bf16 as a kind (spelling open); WGSL has `bool i32 u32 f32` and `f16`, no f64, i64, complex or bf16 (those stay on the CPU or are emulated: the GPU stage's choice); data errors gathered into 1 flag read once at the end (the session slot's device form; XLA's error bits); allocation at a bucket's bound, so a captured graph's buffers stay fixed; 1 captured graph per key; user bounds for lengths the data decides (JAX's `size=`); kernels that read counts from device memory. Array files: leaves backed by buffers the allocator doesn't own: rustymath `Buffer`'s owner `Owned(Vec)` or `External(Arc, offset, len)`, read through `&[T]`, copied on its 1st write; a region says whether the host can read it; built with array files. External backend (mo): the graph as plain data that a backend writes out. + +## Interfaces + +Pinned before the build phase; a unit changing one tells main first. Names are targets, not the tree's. + +- `Desc`: kind, atom flag, layout, range. `Step::describe(&self, operands: &[&Desc]) -> Result`: its only producer. +- `Step`: part 4's variants, each with `describe`, `run::step`'s arm, `glyph`, `operands!`, and its origin (primitive, parameters). `run::step(&Step, operands) -> Value`; a data error sets the session's error slot. +- Row plan: `CallPlan { roles: per operand Data | Values (read at the call: a parameter, values deciding a length; a kind that depends on values is speculated, part 2), step: Step (Call where the row has none), len, bytes }`. 1 builder for every call, `fusion::built(step, operands) -> Value`, its `Desc` from `describe`; the node waits. No force policy: a read forces (part 8). +- Dispatch: `Operation::call(op, args) -> Result`, the only caller of level rules and of the builder. A level rule: `(level, op, args) -> Result`, calling dispatch for its parts. +- Levels: `Level { id, kind: Derivative | Batched }`; `Value::Carried { level: id, .. }`; the innermost running batched level; a level found by its id. +- Graph to runs: `Program::of(roots) -> Program` (the 1 conversion); `schedule(&Program) -> Plan`; `Plan::run(inputs) -> outputs`. Forcing, a trace's end and replay call these alone. A read first looks up `Program::key()` (structure, literals by value, other leaves as descriptions: a length class and bucket per axis, and each length the data decides inside the graph by its class and bucket) in the 1 store, checks the plan's speculation boxes, and schedules only on a miss. +- Rules: `Rule { key: RootKey { kind: StepKind, root: Option }, bind: fn(&[Node], &Step) -> Option, build: fn(&mut Vec, Bound, &Desc, &Shared) -> Option }`; the table indexed by `RootKey` once, run by the pipeline alone (a read of a lone node included). +- Function values: `FunctionNode`: `Primitive | System | Defined | LateBound | Applied { op: Operator, operands } | Train(tines) | Array`; `Operator`: `Kind(OperatorKind)` | a dfn operator's closure; `FunctionData { node, environment, rules: Option }`; `OperatorKind::info()`: call, spelling, inverse, `discards`, Python name. Internal kinds: `Framed { axes }`, `JvpStep`, `VjpStep`. +- Cache: `Programs::serve(&Function, Key) -> Outcome` (a recording, a stop, a select plan) on 1 store keyed by function structure (a dfn by its `Definition` through a weak reference, never a closure instance; entries no function owns evicted least recently used past 256), 1 entry per function value; a forced graph's plan sits in the same store under `Program::key()`. `Key`: argument descriptions (a length class and bucket per axis, part 10), numeric settings, free values read as structure, speculation boxes (kinds; leaf range facts); a length the data decides extends the key from its node (part 10). +- Refill: `Plan::refill(from: usize, &[Desc])` gives a plan, from step `from` on (0 at entry; the step after a length the data decides, partway through a run), its descriptions and maps at a run's concrete lengths, from each step's origin through the row-plan builders; a replay and a read of a cached graph both call it, only where lengths differ from its last refill. +- Loop: `Step::Loop { body: Function, until, carries, data }`; rules `batched`, `jvp`, `transposed`, `substituted`, each giving a `Step::Loop` whose body is `Applied(kind, f)`. +- Errors: 1 slot per session (`Session::error`), holding the 1st error and the node that set it, set by kernels, executors and forcing; `Session::raise() -> Result<()>` at the checkpoints; a run that sets it replaces no node downstream of the failing one; a failed kernel writes in-range values; clearing a masked case's error reruns the whole read. +- Effects: `Step::Effect` takes the token of the effect before it and its own operands, and outside a recording runs at its call; no session registry; a step that reads a numeric setting carries it as a parameter. +- Writes: `selection::selecting(target, root) -> Walk`; a walk writes through 1 `At` node. + +## Cost model + +What each stage of the pipeline ought to cost, counted from the work it must do and set beside the role models. Never from today's tree: a stage over its budget is a gap in that stage's code. Units: instructions retired (`instructions`, Apple silicon) per call, node, step or run; work per item in copies (DEV.md). + +### Primitive operations + +Assumed, Rust on Apple silicon, small sizes: +- small allocation (size-classed allocator) 40; free 30; `Arc` clone or drop 5 +- hash of a small key (≤32 B) 15; map probe, hit 40; insert 60 +- indirect call 10 +- a `Desc` (kind, atom flag, layout of rank ≤4, range) 30; a kind, shape or range rule 40-100 (2 operands' agreement at the top) +- a `Mapping` built or composed 30-60 +- work per item: elementwise f64 0.5-1 (2-lane NEON, unrolled); copy 0.5; fold 1 + +### Stage budgets + +| Stage | Work it must do | Budget | +|---|---|---| +| dispatch, 1 call | innermost level among ≤2 args (10); row by index (5); roles (10); kind, shape, range rules (100); length, bytes (10); node allocated and filled (70); interned: hash, probe (60) | 300 | +| pending node | memory only: about 150 B plus operand references; its leaves stay alive, no intermediate is stored | 0 | +| read of a pending graph of N nodes, plan cached | structure key: each node's step and operand descriptions hashed (30 a node); probe (40); per step: executor call (15), output allocation (40), kernel setup (50), work; each intermediate freed at its last use (30); `Program::of`'s walk (20 a node) and allocation (50); leaves bound to the plan's inputs (20 a leaf); the intern table updated as each node is replaced by its result or dies (30 a node) | 150 + 235·N + work | +| plan, a cache miss | gather (30 a node); rewrite: root-key probe (20 a node), match and build (200 a rule applied); order (20 a node); stage ends (20 a node); descriptions and maps (130 a node); compile decision (20 a stage); plan allocated (50 a node) | 270·N + 200·rules | +| refill | descriptions and maps at the run's lengths through the plan's own builders (130 a node); no gather, rewrite, order, stage ends or compile decision | 130·N, only where lengths differ from the plan's last refill | +| trace | the run's own dispatch and reads, plus free values captured (20 each) and the key (30 an argument) | dispatch + 50 a call | +| replay | key: argument descriptions hashed (30 each), free values compared (20 each), boxes checked; probe (40); refill where lengths changed; run as a read (135 a step + work) | 150 + 135·N + refill + work | +| compile (JIT) | only where its formula repays (part 9); code owned by `jit::KERNELS`, keyed by kernel structure, never by length or bucket | per formula | +| tree op, dfn call | op 30; name load 10; dfn frame 60 + 20 an argument | — | + +Role models for the shape of these costs: JAX's eager mode runs each primitive as a 1-op compiled program cached by primitive and abstract signature (`apply_primitive`); `jit` checks a cache keyed by the function and its arguments' signatures, so a hit costs a key check plus the program. tinygrad builds every op as a lazy `UOp`, caches compiled kernels by their AST, and TinyJit replays the kernels a real run captured. BPL's budgets sit below both: no Python, no device launch before the GPU stage. + +### What follows + +1. Small calls: a call then a read of its result ≈ 300 + 150 + 235 = 685 + work. Every pending value keeps its node; a call matching no rule pays 1 root-key probe (20). +2. Ordinary code against replay, a body of N calls run again at the same shapes: ordinary ≈ 300·N (dispatch) + 150 + 235·N (read, plan cached by structure) = 535·N + 150; replay ≈ 150 + 135·N. About 4× at tiny sizes; at 10⁶ floats a step's work (about 10⁶) dwarfs both. +3. Lengths: power-of-two buckets, refill on each run whose lengths changed (part 10). + - A plan's cost-based choices depend on item counts: the compile decision (part 9), the cheaper of 2 reads (contraction), recomputing a cheap chain. Across a bucket each is off by at most 2×; a choice that a length makes exactly (an unrolled short fold) is never made on a bucketed axis (part 10); a plan per exact shape would make them exactly, for little gain. + - A growing carry (1 item more each step): buckets ≈ (130 + 135)·N = 265·N a step, a plan at each power of two; a plan per exact shape ≈ (270 + 135)·N = 405·N a step; exact lengths only (a retrace per shape up to the limit, then ordinary code whose reads miss the cache) ≈ (300 + 30 + 270 + 135)·N = 735·N a step. + - Refill is a subset of planning by construction (descriptions and maps only), so it can't reach a plan's cost: no branch waits on a measurement. +4. 1 cache, keyed by structure (part 10): a function value's entry holds its recordings; a forced graph's plan sits under its graph's structure, leaves as descriptions (a length class and bucket per axis); a lone node is a graph of 1. Ordinary code refills as a replay does: a growing value read outside a trace ≈ 300·N + 150 + (235 + 130)·N ≈ 665·N a step. +5. Memory between reads: a pending graph keeps its leaves alive, never an intermediate; a node ≈ 150 B; the memory limit (part 8) forces first. +6. Scalar runs: a `⍣` loop of scalar ops is a loop node replaying its body (150 + 135·N an iteration); a scalar op outside a loop ≈ 685 (accepted: "The bar", "No cost when unused"). +7. A new length or bucket never compiles: kernels are keyed by structure; counts are run-time parameters. + +### Checking + +The closing pass counts each stage's functions (`call_profile`; `profile` with `defer=`) against these budgets. Over budget → a gap in that stage, fixed toward its budget, never by a 2nd route. + +## The bar + +Futhark, Dex and Julia set the standard. A design at least as good would meet the criteria below. Each one can be checked with `basedpl.bpltests.time_cases` or a reference test. + +| Criterion | Meaning | Reference point | +|---|---|---| +| Gradient cost | The gradient of a scalar function of n variables costs a small constant multiple of one evaluation, whatever n is. | On ADBench, Futhark's full Jacobian costs 3.2 to 13.0 times the objective, and hand-written code 4.4 to 8.6 times (`links/arXiv-2202.10297.md`, section 7.1). | +| Primal cost | Under `∂`, the function's own computation runs on the same whole-array kernels as without `∂`, and keeps the fusion it would get without `∂`. The derivative adds whole-array work beside it. Nothing sends the original computation down a per-item path. | Futhark's reverse mode re-executes the original statements unchanged (`links/arXiv-2202.10297.md`, section 4). | +| Bulk preserved | The derivative of a whole-array operation is a whole-array operation. A map's derivative is a map. The transpose of indexing is an accumulation, not a one-hot array or a sequential loop. | Dex paper, sections 5.3 and 7.3. Futhark's `vjpMap`, `diffReduce` and `diffScan`. | +| Coverage | Primitives in a documented subset, trains, operators with any operand, branches through Agenda (`⍚`), and records of parameters. Recursion is out of scope (part 10). | JAX branches on traced values only through `lax.cond`, `lax.switch` and `select`. | +| One rule set | Each scalar glyph's partial derivatives are written once and serve both modes. Each linear operation has one transpose. A user rule attaches once and works in both modes. | Futhark's `Derivatives.hs`, Dex's `linearizeOp`, JAX's `custom_jvp`. | +| Higher orders | `∂∂` is `∂` applied to the function `f∂`, with no confusion between nesting levels. | ForwardDiff's tags. JAX nests transformations. | +| No cost when unused | Code that isn't differentiated runs as fast as now, except agreed exceptions. Agreed: always-lazy evaluation gives every call a node and a read, so a lone primitive call costs about 1.5 to 2 times today's eager call (about 685 instructions plus its work: "Cost model"), while dfn calls, loops and scalar recursion cost less. | | +| Errors | When and how an error from deferred work surfaces isn't promised. Leaving an error guard forces the work made inside it, so the guard catches its errors. Effects keep their order among themselves; an effect forces only what it reads. | `meta/ideas.md`, "Block fusion", names late errors as a cost of lazy arrays. | +| Composes with optimisation | The rewrite rules and the scheduler apply to tangent and transpose nodes as to any other nodes. Derivative code fuses, windows and stencils included. The scheduler recomputes a residual that is cheap to compute again, rather than storing it. | XLA simplifies and fuses the programs that JAX's transforms produce. | +| Evaluator | The change doesn't repeat evaluator semantics. | | +| Build time | After a small edit to one function, the dev build rebuilds about as fast as now. Functions that every call goes through, such as `Primitive::call_axis`, show growth first. | The baseline below this table. | + +Exact derivatives for exact inputs, as `⌻` gives now, would be a BPL extra. + +Gradients over arrays of thousands of values or more, as in neural-net training and image diffusion, are in scope. `∂` therefore needs reverse mode, and the gradient-cost criterion is a requirement. + +Build-time baseline: the total time of `cargo build --lib --bins --profile test --timings`, the build that DEV.md documents for inspecting compilation. Each edit wrapped one value in `std::hint::black_box`, which changes the generated code but not the behaviour. Each function was timed after the edit and again after undoing it: + +| Edited function | Rebuild | basedpl's code generation | Rebuild, 20 October | Code generation, 20 October | +|---|---|---|---|---| +| `Chain::run`, now `Graph::run` (`src/fusion.rs`) | 1.9 to 2.8 s | 0.2 s | 2.4 s | 0.4 s | +| `fused` (`src/eval/binder.rs`), removed in step 4. `Binder::reduce` stands in for it on 20 October | 2.0 to 2.1 s | 0.3 s | 2.3 s | 0.3 s | +| `Primitive::call_axis` (`src/primitive.rs`) | 7.4 s | 5.7 s | 8.1 s | 6.2 s | +| `ArrayData::storage` (`src/array.rs`) | 2.2 s | 0.5 s | 2.5 s | 0.5 s | + +## Acceptance + +Every model in `tests/speed/models.bpl` and in each held-out set (written by fresh agents from the docs alone; implementation forks don't read them) passes under static kinds: (1) it traces whole: its forward pass and `∂` of every scalar loss each become 1 cached graph (`•explain`: "built" once, then "replayed", no "not kept"); (2) its training loop `step⍣N params` is 1 node, traced on the 1st iteration and replayed after; (3) its forward and gradient costs meet first-principles expected costs with no `held` lines, each gradient at most about 3× its forward pass; (4) it passes in at least 2 spellings (tacit and dfn, or index notation and plain) at costs within a small margin. Every rule has a reason that depends on no model, and a reference case outside the models. Under dynamic kinds every model gives correct results, costs recorded; 2 speed cases check the 4× aim: a fused chain through a value-dependent kind (`+/⍟v` or `+/√v` on a float column) and integer date arithmetic (`7|3+d` on day numbers). Also: every deletion in the parts' "Not" lists, and DEV.md's full checks. + +## Use + +- A fork's step names the part it moves toward and the departure it removes. +- A review measures a diff against its part, never against neighbouring code. A departure left → a gap row; a departure added → rejected. +- The gap review lists each departure in the tree, by part, with its home in the target. +- After the target is fixed, a change goes in an errata doc: each entry names the part number it replaces and why. From 96ea793f3592bea56643478a21fc3508c301a835 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Mon, 2 Nov 2026 12:33:50 +1000 Subject: [PATCH 02/47] nbs/foundations.qmd --- nbs/foundations.qmd | 102 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 nbs/foundations.qmd diff --git a/nbs/foundations.qmd b/nbs/foundations.qmd new file mode 100644 index 00000000..df9357b6 --- /dev/null +++ b/nbs/foundations.qmd @@ -0,0 +1,102 @@ +--- +title: "Computational foundations" +description: "What BPL borrows from tinygrad, JAX, Dex and Futhark, and what owning the language makes possible" +--- + +BPL can control the whole path from an array expression to the machine code that computes it. Its functions, numerical rules, transformations and execution belong to one runtime. That gives us an opportunity to make small interactive calls cheap while compiling larger compositions into bulk array work, without maintaining a separate execution system for each. + +This page explains the new core under construction and the reasons for its design. These are opportunities and design commitments, not measured speedups or a description of everything in the released implementation. [The target](target.qmd) specifies the required behaviour and costs. + +## Why owning the language helps + +Python array libraries already run numerical kernels in native code. BPL's opportunity is in the work around those kernels. A BPL call needs no Python operator overload, array-object protocol or Python-to-native crossing in its core. We can give dispatch one entry, make its inputs small and measure the cost of every step. This matters most for short expressions, loops and the many small calls used to construct a larger calculation. + +BPL also knows how a function was made. A primitive, a train and an operator applied to its operands have structural representations. We can reuse a recorded calculation for closures of the same definition while passing their different captured arrays as inputs. The cache need not depend on the identity of a newly allocated callable. Captured data are snapshots from the current call, never stale data kept inside compiled code. JAX documents the recompilation cost of repeatedly constructing temporary functions. [JAX's function caching](https://github.com/jax-ml/jax/blob/main/docs/jit-compilation.md#jit-and-caching). + +Code written for static kinds uses explicit widths and guaranteed fast forms such as `√ₐ`, `*ₐ`, `⍟ₐ`, `○ₐ` and `÷ₓ` where needed. Their result kinds follow from the arguments without inspecting array items. Dynamic kinds preserve interactive behaviour when values decide a result's kind. A necessary read of those values is an explicit boundary. Differentiation uses real floats, promoting exact differentiated inputs through BPL's normal float-width rules. + +Lazy arrays preserve the calculation that produces them across function calls. A transpose, a selection or a window can remain a description of reads until its consumer needs items. Both the BPL frontend and rustymath's numerical kernels can use that description. A single runtime can then coordinate graph construction, transformations, planning and code reuse instead of translating between independently maintained execution routes. + +## Calls build a graph; reads compute + +A graph records operations and their dependencies. A pending array holds its producer node instead of an intermediate buffer. For example, suppose `x` is a real float vector: + +```bpl +loss←+/⊼ +y←loss x +``` + +[`⊼`](glyphs/wedge-bar.qmd) squares each item. [`+/`](glyphs/slash.qmd) sums the squares. Calling `loss` builds those operations without first storing a vector of squares. Displaying `y` reads its value. The runtime then plans the reachable graph and computes the total, with the square inside the reduction where the execution rules support it. + +We borrow lazy graph construction and node interning from tinygrad. Interning gives structurally identical live nodes one representation. tinygrad separates Tensor construction, scheduling and `realize`, and replaces realized producers with storage. Its compiler caches generated programs by a structural key. BPL uses the same separation to retain producer information before a read and release it after storing the result. [tinygrad's `UOpMetaClass`](https://github.com/tinygrad/tinygrad/blob/master/tinygrad/uop/ops.py), [Tensor realization](https://github.com/tinygrad/tinygrad/blob/master/tinygrad/tensor.py), [compilation and execution](https://github.com/tinygrad/tinygrad/blob/master/tinygrad/engine/realize.py). + +A plan decides operation order, which intermediate results need storage and which operations run together. Fusion means computing a producer's items inside its consumer rather than storing and rereading an array between them. Futhark performs producer-consumer and shared-input fusion over a dependency graph, even when the source expressions are separated by names or functions. BPL's graph gives its rewrite rules that same view. [Futhark's fusion and views](https://github.com/diku-dk/futhark/blob/master/docs/performance.rst). + +A trace records the graph built by an actual run. A replay binds new inputs to the recorded calculation and runs its plan. tinygrad's TinyJit demonstrates recording executed work and replaying it without rerunning the Python function. BPL records the graph that ordinary calls already build. A trace is therefore a way to reuse construction work, not another interpretation of numerical operations. Ordinary graph reads and recorded calls use the same planning pipeline. [TinyJit capture and replay](https://github.com/tinygrad/tinygrad/blob/master/tinygrad/engine/jit.py). + +## Transformations keep whole-array work + +Batching applies one function to many cells by transforming its operations into array operations. A cell is the part of an array that the function receives at each position. BPL's Each, Rank and related operators use one batching mechanism instead of separate evaluators for each operator. + +JAX's Autodidax tutorial supplies the model for composing these transformations. In that presentation every primitive call goes through `bind`, which selects the relevant transformation interpreter from its arguments. A transformation can call primitives through the same entry. Older transformed values can be lifted into a newer transformation without losing their earlier meaning. BPL calls these nested interpreters levels. Its derivative and batching levels follow those data dependencies. [JAX's interpreter stack, lifting and batching](https://github.com/jax-ml/jax/blob/main/docs/autodidax.md#part-1-transformations-as-interpreters-standard-evaluation-jvp-and-vmap). + +For the example above: + +```bpl +grad←loss∂ +grad x +``` + +[`∂`](glyphs/partial.qmd) gives the gradient of a function with one number as its result. Here the gradient is twice each item of `x`. The original calculation is the primal computation. Forward differentiation propagates a tangent, the change in a result along a chosen input direction. Linearization separates the original calculation from a calculation linear in that tangent. Reverse differentiation transposes the linear calculation to propagate result sensitivities back to the inputs. We borrow this construction from JAX rather than writing independent forward and reverse derivative formulas. [JAX's linearization and transposition](https://github.com/jax-ml/jax/blob/main/docs/autodidax.md#part-4-linearize-and-vjp-and-grad). + +Dex and Futhark show why preserving array structure through differentiation matters. Dex linearizes `for` bodies and transposes their structured loops. Its transpose uses indexed accumulations to send sensitivities back to selected array positions. Futhark differentiates maps, reductions and scans as bulk array operations, then allows fusion to run again. BPL follows that principle. The reverse of a repeated read accumulates at the source positions, without constructing a one-hot array or invoking an interpreter for every item. [Dex linearization](https://github.com/google-research/dex-lang/blob/main/src/lib/Linearize.hs), [Dex transposition](https://github.com/google-research/dex-lang/blob/main/src/lib/Transpose.hs), [Futhark's array differentiation](https://github.com/diku-dk/futhark/blob/master/src/Futhark/AD/Rev/SOAC.hs). + +Dex also shows the value of writing a formula for one item while retaining its array structure. BPL's [index notation](index-notation.qmd) follows that idea through named axes. Index formulas and whole-array spellings build operations that the same contraction and read-mapping rules can recognize. BPL uses arrays of named indices rather than Dex's statically typed index sets. [Dex's table comprehensions and typed indexing](https://github.com/google-research/dex-lang/blob/main/examples/tutorial.dx). + +The reverse derivative of a structural operation can often read from the sensitivity array instead of scattering into a new one. tinygrad's gradient rules reverse padding with a shrink and a shrink with padding. BPL similarly prefers read-based transposes when the mapping permits them, retaining accumulation where positions repeat. [tinygrad's movement gradients](https://github.com/tinygrad/tinygrad/blob/master/tinygrad/mixin/gradient.py). + +The transformed operations still enter the ordinary graph. Rewrite rules and fusion therefore apply to the gradient as well as the original function. Scalar mathematical facts have one home, including formulas, partial derivatives and range information. A rule can recognize a contraction or a stable mathematical form after batching and differentiation have exposed it. + +## Loops stay loops + +BPL's iteration operators already identify a loop's body and the values carried between iterations. Expanding that body for every iteration would make graph size grow with the iteration count. BPL keeps a loop node and records its body once. Batching and differentiation transform the body, then construct another loop through normal dispatch. JAX's `scan` rules use this pattern with transformed body programs. BPL reruns transformed function values to build their graphs. [JAX's loop transformations](https://github.com/jax-ml/jax/blob/main/jax/_src/lax/control_flow/loops.py). + +Reverse differentiation needs some intermediate values from the forward run. These saved values are residuals. BPL splits a reverse-mode loop into a primal loop that produces the result and residuals, and a linear loop that uses them. Both use the normal runner. Dex's linearization explicitly separates primal computation, residuals and a linear function. [Dex's residual handling](https://github.com/google-research/dex-lang/blob/main/src/lib/Linearize.hs). + +Saving every intermediate is often unnecessary. Cheap expressions can be recomputed instead of stored. When loop residuals exceed the memory budget, checkpointing saves selected loop states and reruns sections to recover the intervening residuals. Futhark's reverse-loop implementation and stripmining provide the model for this memory/work tradeoff. BPL supports a host residual stack for a loop whose final iteration count is not known in advance. It does not inherit JAX's restriction against reverse differentiation of a dynamic `while_loop`. [Futhark's reverse loops](https://github.com/diku-dk/futhark/blob/master/src/Futhark/AD/Rev/Loop.hs), [JAX's dynamic-loop restriction](https://github.com/jax-ml/jax/blob/main/jax/_src/lax/control_flow/loops.py). + +## Reuse structure as lengths change + +An array's exact length often changes the amount of work without changing the computation. A kernel can take that length as a runtime argument. Futhark's compiled libraries accept array dimensions at runtime. tinygrad also represents bounded symbolic sizes separately from their current values. BPL's kernels use runtime counts without requiring a symbolic shape language throughout the runtime. [Futhark's array API](https://github.com/diku-dk/futhark/blob/master/docs/c-api.rst), [tinygrad's variables](https://github.com/tinygrad/tinygrad/blob/master/tinygrad/uop/ops.py). + +BPL keys reusable plans by structure and the facts their validity needs. The length component distinguishes empty, unit and longer axes. Unit axes change broadcasting. Ranks, kinds, axis equalities, structural parameters and assumed numerical ranges also matter. Two longer vectors of different lengths can reuse a plan when those other facts agree. + +Refill updates concrete result descriptions and read mappings for the new lengths. It preserves the plan's rewrites, operation order and stage structure. Preparation uses actual counts for the few choices where size changes the preferred execution, including whether compiling is worthwhile, which direction to read a contraction and whether to recompute an intermediate. + +Futhark demonstrates choosing implementations at runtime from size checks. Its multicore scheduler selects between parallel and sequential nested work, and its GPU compiler generates versions for different amounts of parallelism. BPL borrows size-sensitive choice without adopting an ahead-of-time catalogue of versions. A just-in-time compiler emits a worthwhile structural version when execution first needs it and keeps that code in one cache. A new length does not automatically require another compilation. BPL uses no size buckets or padding to a bucket. [Futhark's runtime selection](https://github.com/diku-dk/futhark/blob/master/docs/performance.rst). + +Filtering and finding unique items determine a result's length from data. BPL reads that count, then refills the downstream plan when its assumptions still hold. A changed structural fact uses the normal cache-miss path. The read is an execution boundary, not a reason to reject the expression. JAX's jitted `unique` instead requires a static output size, with truncation or padding to that size. This distinction matters for interactive data analysis. [JAX's data-dependent output sizes](https://github.com/jax-ml/jax/blob/main/jax/_src/numpy/setops.py). + +## Effects and errors belong in the model + +Lazy execution must preserve visible actions. BPL represents output and other effects as ordered nodes. A token is a dependency with no numerical meaning that keeps one effect after the previous effect. This follows JAX's ordered-effects design. An effect reads only its own operands, rather than forcing every pending array in the session. [JAX's effect sequencing](https://github.com/jax-ml/jax/blob/main/docs/jep/10657-sequencing-effects.md). + +Random draws also have graph inputs for their generator state. Replaying a program advances that state instead of repeating capture-time samples. JAX's counter-based random design explains how independent draws can be assigned stable positions without making array execution sequential. BPL manages the streams inside its runtime rather than requiring JAX's explicit user-level key splitting. [JAX's PRNG design](https://github.com/jax-ml/jax/blob/main/docs/jep/263-prng.md). + +Static graph-numeric execution has a non-raising policy for data errors. An invalid numeric index reads the fill, including on an empty axis, and an invalid numeric write skips that position. Dynamic execution retains checked behaviour. A potentially fallible dynamic branch or batched loop gathers only its live cells before running them, a process called compaction. There is no speculative whole-read retry. Shape, kind, name and host errors remain checked. These choices avoid maintaining error-replay machinery alongside the graph executor. + +## Share numerical algorithms at the right size + +An elementary operation normally has one width-specific formula over a small set of arithmetic operations, called `Ops`. Native kernels and generated code instantiate that same formula. Partial derivatives and range rules belong beside the mathematical object. A substantial algorithm, such as incomplete gamma's convergence calculation, can instead run as one whole-array `Call` through the common graph runner. One algorithm does not require every function to have the same granularity. + +The recommended native backend implements actual SIMD lanes and masks through rustymath's existing abstractions, with explicit tails, widths and bounded mixed-width operations. Scalar per-item callbacks are not a substitute. The preferred mixed-width support preserves f32 trig's existing f64 reduction on non-FMA targets. A table read is added only where a retained algorithm needs one after Pow simplification; a machine without gather instructions can require one scalar load per lane. + +The recommended numerical design composes general positive floating Pow/Root from shared Exp/Ln and keeps short ordinary trig formulas with rare object-owned scalar repair. Native SIMD repairs flagged items after its first pass. Generated code calls the same repair only for flagged lanes, before fused consumers use them. Compact Cbrt and genuine iterative specials keep their existing algorithmic roles. Gamma and compensated-constant-power simplification are later opportunities, not blockers for the basic build. + +Shared formulas and one home per mathematical fact are agreed principles. The numerical changes above are concrete recommendations awaiting approval, not an undecided choice between tables and a larger compiler. We still need to validate and agree any function-specific float-error scope, verify native/wasm repair providers and implement the SIMD/repair interfaces. GPU repair is a later device choice. No new accuracy exception is approved or published. [The target](target.qmd#numerical-implementation-choices) records the designs and their status. + +## One home for each decision + +BPL supplies language rules and function structure. rustymath supplies numerical operations and shared array-reading machinery. Transforms rebuild operations through the same dispatch entry. A mathematical fact, read mapping, loop rule or cache policy should not acquire another implementation because it is used from a different frontend or under a derivative. + +The same graph should remain usable by a later GPU or external backend without adding a second account of what a BPL expression means. From f0cca85dacdd4b7fe67c2d033ab324aece6644b4 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Mon, 2 Nov 2026 19:25:28 +1000 Subject: [PATCH 03/47] Graph rewrite checkpoint --- DEV.md | 550 ++-- cli/src/cli.rs | 8 +- cli/src/kernel.rs | 18 +- cli/src/worker.rs | 18 +- .../BPL.tmbundle/Syntaxes/bpl.tmLanguage.json | 14 +- .../BPL.tmbundle/Syntaxes/hpl.tmLanguage.json | 6 +- editors/vim/syntax/bpl.vim | 6 +- lib/regex.bpl | 2 +- nbs/arrays.ipynb | 5 +- nbs/assignment.ipynb | 65 + nbs/autodiff.qmd | 30 +- nbs/bpl.xml | 14 +- nbs/distributions.ipynb | 4 +- nbs/evaluation.qmd | 16 +- nbs/foundations.qmd | 8 +- nbs/getting-started.ipynb | 8 +- nbs/glyphs/at.qmd | 3 +- nbs/glyphs/diamond-underbar.qmd | 2 +- nbs/glyphs/dot.qmd | 3 +- nbs/glyphs/hydrant.qmd | 6 +- nbs/glyphs/inverted-question.qmd | 2 +- nbs/glyphs/quad-equal.qmd | 14 +- nbs/hpl.xml | 6 +- nbs/index-notation.qmd | 47 +- nbs/keyed.ipynb | 4 +- nbs/llms.txt | 10 +- nbs/numbers.qmd | 39 +- nbs/performance.qmd | 118 +- nbs/python.ipynb | 6 +- nbs/regex.ipynb | 27 +- nbs/scripts.ipynb | 38 + nbs/sidebar.yml | 3 +- nbs/study/05_searching-indexing.ipynb | 6 +- nbs/syntax.qmd | 2 + nbs/system-functions.qmd | 3 +- nbs/system/distribution.qmd | 2 +- nbs/system/explain.qmd | 27 +- nbs/system/memory.qmd | 2 +- nbs/system/prefs.qmd | 2 +- nbs/system/r.qmd | 8 +- nbs/system/rand.qmd | 10 +- nbs/system/replace.qmd | 10 + nbs/system/storage.qmd | 6 +- nbs/target.qmd | 185 +- nbs/web.ipynb | 5 +- py/src/lib.rs | 168 +- pyproject.toml | 5 +- python/basedpl/__init__.py | 2 +- python/basedpl/bpl.tmLanguage.json | 14 +- python/basedpl/bpltests.py | 662 ++++- python/basedpl/browser/language.js | 2 +- python/basedpl/hpl.tmLanguage.json | 6 +- python/basedpl/notebook_runner.py | 34 +- python/basedpl/worker.py | 137 +- src/agreement.rs | 50 +- src/array.rs | 1387 ++++------ src/array/storage.rs | 830 ++---- src/array/text.rs | 67 +- src/batch.rs | 784 +----- src/canvas.rs | 6 +- src/csv.rs | 26 +- src/data.rs | 40 +- src/date.rs | 7 +- src/derivative.rs | 1368 +++------ src/display.rs | 62 +- src/distribution.rs | 262 +- src/element.rs | 170 -- src/error.rs | 42 +- src/eval.rs | 1842 +++++++------ src/eval/assign.rs | 600 ++-- src/eval/binder.rs | 1382 +++++----- src/eval/bindings.rs | 642 +++-- src/eval/dissect.rs | 30 +- src/eval/operators.rs | 2446 ++++------------- src/eval/rewrite.rs | 531 ---- src/eval/session.rs | 525 ++-- src/eval/tree.rs | 1384 +++++----- src/execution.rs | 23 +- src/explain.rs | 345 +-- src/files.rs | 19 +- src/fusion.rs | 828 +----- src/graph.rs | 248 ++ src/host.rs | 1 - src/host_web.rs | 3 +- src/image.rs | 30 +- src/jit.rs | 331 ++- src/js.rs | 52 +- src/json.rs | 101 +- src/keyed.rs | 90 +- src/lib.rs | 5 +- src/linear.rs | 931 +------ src/name.rs | 4 + src/number.rs | 807 ++---- src/number_theory.rs | 15 +- src/pervasive.rs | 2120 -------------- src/plan.rs | 687 ++--- src/plot.rs | 5 +- src/polynomial.rs | 4 +- src/primitive.rs | 2280 +++++++-------- src/primitive/indexing.rs | 206 +- src/primitive/numeric.rs | 486 ++-- src/primitive/search.rs | 208 +- src/primitive/structural.rs | 295 +- src/program.rs | 1901 +++++++++---- src/program/blocks.rs | 1900 +++---------- src/program/contraction.rs | 207 +- src/program/control.rs | 300 ++ src/program/layout.rs | 406 +-- src/program/rules.rs | 269 ++ src/program/run.rs | 562 +++- src/program/trace.rs | 602 ++-- src/program/verify.rs | 100 +- src/protocol.rs | 52 +- src/reference.rs | 210 +- src/regex.rs | 55 +- src/search.rs | 159 +- src/selection.rs | 436 +-- src/settings.rs | 151 +- src/syntax.rs | 347 +-- src/system.rs | 323 ++- src/timing.rs | 161 +- src/transform.rs | 676 +---- src/xml.rs | 14 +- tests/acceptance/held_out.bpl | 237 ++ tests/coverage.txt | 60 +- tests/explain.rs | 41 +- tests/instructions.txt | 36 +- tests/jit.rs | 5 +- tests/reference/README.md | 2 +- tests/reference/aplcart.bpl | 50 +- tests/reference/april.bpl | 6 +- tests/reference/core.bpl | 752 ++++- tests/reference/distributions.bpl | 70 +- tests/reference/graphics.bpl | 6 +- tests/reference/ngn.bpl | 7 +- tests/reference/regex.bpl | 46 +- tests/reference/static.bpl | 43 +- tests/settings.rs | 3 +- tests/speed/arrays.bpl | 15 +- tests/speed/maps.bpl | 84 +- tests/speed/models.bpl | 51 +- tests/speed/structure.bpl | 7 +- tests/speed/tables.bpl | 23 +- tests/test_api.py | 27 +- tests/test_notebooks.py | 5 + tests/test_orchestration.py | 36 + tests/test_worker.py | 4 +- tests/wasm.mjs | 2 +- 148 files changed, 15490 insertions(+), 21964 deletions(-) create mode 100644 nbs/system/replace.qmd delete mode 100644 src/element.rs delete mode 100644 src/eval/rewrite.rs create mode 100644 src/graph.rs delete mode 100644 src/pervasive.rs create mode 100644 src/program/control.rs create mode 100644 src/program/rules.rs create mode 100644 tests/acceptance/held_out.bpl create mode 100644 tests/test_orchestration.py diff --git a/DEV.md b/DEV.md index 981c48e2..68450155 100644 --- a/DEV.md +++ b/DEV.md @@ -1,179 +1,216 @@ # Development -Keep the language concise to write, simple to read and consistent to understand. A design rule is a plan on trial. Discuss it when applying it makes a useful expression harder to read, removes something expressible or requires another special case. APL familiarity and compatibility are not costs in that decision. Change callers with an API change instead of adding compatibility layers. Give each kind of thing the general path rather than a separate one of its own. A system name, for example, resolves to a function and compiles as a glyph does. +Language: concise to write, simple to read, consistent. A design rule is a plan on trial: discuss it when applying it makes a useful expression harder to read, removes something expressible or adds a special case; APL familiarity and compatibility aren't costs there. API change: change callers; no compatibility layers. Each kind of thing takes the general path: a system name resolves to a function, compiles as a glyph does. -Speed and simplicity come before edge cases no real user meets. Follow IEEE, Rust and CPU behaviour where it gives a reasonable result. Precision follows the rules in the "Overview" of rustymath's `DEV.md`. +Speed and simplicity before edge cases no real user meets. Follow IEEE, Rust and CPU behaviour where reasonable. Precision: rustymath `DEV.md`'s "Overview". -A form with a reasonable meaning gives a result, not an error. A result that would be a bug in some programs, with no error, is acceptable when it buys useful behaviour or speed. Negative positions count from the end in every selection, as in Python, even though `x.[i-1]` at `i=0` then reads the last item. `⌈` beside NaN gives NaN or the other argument, whichever its kernel gives. Jeremy settled negative positions on 18 October 2026, after the question had come up repeatedly. +A form with a reasonable meaning gives a result, not an error; a silent result that is a bug in some programs is fine if it buys useful behaviour or speed. Settled: negative positions count from the end in every selection (Python), though `x.[i-1]` at `i=0` then reads the last item. `⌈` beside NaN: NaN or the other argument, whichever its kernel gives. -BPL serves two kinds of work. Default, dynamic kinds give the APL REPL experience that most users want most of the time, with exact integers that never wrap, exact rationals and tolerant comparison. Static kinds serve speed-sensitive work. Supporting both kinds costs complexity, not static speed. Large work runs in notebook cells and scripts, not in the REPL. +Dynamic kinds (default): the APL REPL experience most users want most of the time: exact integers that never wrap, exact rationals, tolerant comparison. Static kinds: speed-sensitive work. Supporting both costs complexity, not static speed. Large work runs in notebook cells and scripts, not the REPL. -Typed programs and the JIT have to end in code that is both fairly simple and very fast. If they can't, the approach itself is in question. Under static kinds, programs on integers should fuse, compile and run array-wise, with the simplest implementation that allows. That calls for one integer regime under static kinds, in which an ordinary integer is a fixed integer of the `int` width. Integer overflow should wrap on every route, because a check on each item and an error path out of a fused or compiled loop cost speed and code. Every integer operation should compute at its type's width. Each width's kernels cost compile time, binary size and code. A kernel family is added only where its benefit outweighs that cost. +Typed programs and the JIT must end in fairly simple, very fast code; otherwise the approach is in question. Static kinds: integer programs should fuse, compile and run array-wise, by the simplest implementation that allows → one integer regime: ordinary integer = fixed integer of `int` width; overflow should wrap on every route (per-item checks and error paths out of fused or compiled loops cost speed and code); every integer operation should compute at its type's width. Each width's kernels cost compile time, binary size, code: add a kernel family only where its benefit outweighs that. -Big-data analysis runs keyed arrays of functions over keyed arrays of data. Keyed and named arrays should therefore fuse and compile as plain ones do. The semantics of missing items and fill can change if that makes this fast and reasonably simple. +Big-data analysis runs keyed arrays of functions over keyed arrays of data → keyed and named arrays should fuse and compile as plain ones do; missing-item and fill semantics can change to make that fast and reasonably simple. -Laziness between cells isn't worth optimizing, nor are chains that rebind a name so that earlier lines compute nothing. Pending arrays fuse large calls in code that no trace holds. A view, such as a window or a transpose, stays a view until a call needs its items in memory. +Not worth optimizing: laziness between cells; chains that rebind a name so earlier lines compute nothing. Pending arrays fuse large calls in code no trace holds. A view (window, transpose) stays a view until a call needs its items in memory. -In the language, a bare number is a float: `3` is a float, and an integer is written `3ₓ`. That rule is about the language, not the implementation. An internal route may run a whole float as an integer where the result can't change. +Bare number = float (`3`; integer `3ₓ`): a language rule, not an implementation one; an internal route may run a whole float as an integer where the result can't change. ## You MUST read this ENTIRE section in FULL first -This file is internal guidance for BPL's developers. It makes no commitments. +Internal guidance; no commitments. Founding principles: `AGENTS.md`; details: "Accuracy" and "Dynamic kinds" below. -BPL and rustymath care about two things: speed and simplicity. Before designing a mechanism, study how great software such as tinygrad and JAX solved the same problem. Their authors worked hard to get the foundations right. Let those designs guide ours. Duplicate code, special-case hacks and several paths for similar work are complexity. Find the one principled mechanism. Give each fact one home. The aim is something pure, correct and principled (Jeremy, 30 October 2026). `AGENTS.md` states the founding principles. The paragraphs on accuracy and on dynamic kinds below give their details. +First read `nbs/llms.txt` in full. BPL differs substantially from Dyalog APL in grouping, names, numbers and operators: write BPL, not assumed APL idioms. llms.txt links point at the published site, which lacks unreleased changes: read sources under `nbs/` (`nbs/glyphs/at.qmd` for `glyphs/at.html.md`, `nbs/indexing.ipynb` for `indexing.html.md`). Before writing BPL for tests or examples, read `nbs/composition.qmd`. -LLMS: Read `nbs/llms.txt` in full before doing any work. BPL differs substantially from Dyalog APL in grouping, names, numbers and operators. Use BPL syntax, not APL idioms assumed to carry over. The links in `nbs/llms.txt` are relative to the published site. In this repository, read each linked page's source under `nbs/`, as in `nbs/glyphs/at.qmd` for `glyphs/at.html.md` and `nbs/indexing.ipynb` for `indexing.html.md`. The published site lacks unreleased changes. +Rustymath and basedpl have no outside users: treat neither as published. Machinery serving both basedpl and rustymath's Python API is shared; changing rustymath to share it is encouraged. Separate basedpl and rustymath machinery for one task is an error unless Jeremy asked for them to differ there. -Before writing BPL for tests or examples, read `nbs/composition.qmd` on composing functions. +The design and `meta/` notes for work ahead: -Rustymath and basedpl have no outside users yet, so treat neither as published. Machinery is shared when it has two users: basedpl and rustymath's Python API. Changing rustymath to give them shared machinery is encouraged. Separate machinery in basedpl and rustymath for the same task is an error, unless Jeremy has asked for them to differ there. +- `nbs/target.qmd`: the 1 home of the design: goals, non-goals, measures, flow, role models, parts (job, homes, what must not exist), interfaces, cost model (each stage's budget), the bar, acceptance. The measure for every step, diff and review in the release. Its “Numerical algorithms” records the approved numerical direction and the validation needed before documenting changed accuracy bounds. +- `open.md`: every open item, and the checks for the closing pass. +- `crate-split-2026-10-31.md`: whether to split basedpl into crates: no split now; rebuild time by build step; engine-to-language references, recounted at the closing pass. -These design notes in `meta/` guide work still ahead: +`meta/archive/`: notes the target replaces: the one-graph plan (`one-graph-2026-10-31.md`: reasons, decisions with their history, evidence) and its build record (`one-graph-build-record.md`), `programs.md`, `jit-plan.md`, `graphreview.md`, `stage0-design.md`, `program-layer-2026-10-30.md`, `graph-ir.md`, `recorded.md`, `linear.md`, `binder.md`, the stage, index-notation and design-audit notes; the stopped build's re-plan (`gaps-2026-11-01.md`) and design notes (`design/`). Other `meta/` notes (build notes of old features, e.g. `PRD.md`): history only. `closed.md`: archived decisions and finished work, for revisiting an old decision. -- `jit-plan.md`: the staged plan for the typed program, the JIT, batching and AD, and the GPU, with "Settled with Jeremy". -- `graphreview.md`: the review whose path the plan stages, with the program's requirements in section 5 and the milestones that the stages follow. -- `stage0-design.md`: the numeric rules, operation plans and the typed program, with Jeremy's answers in section 7. Section 6 names the parts of `graph-ir.md`, `gpu.md` and `recorded.md` that still apply. -- `programs.md`: AD's design basis, in "The bar", "Role models: JAX, tinygrad and Dex", "Semantics of `∂`", "Rules" and "Language changes under study". -- `index-compare.md`'s "Decisions" and `binder.md`'s levels: index notation. `index-levels-2026-10-26.md` designs levels 2 and 3, which aren't built. -- `linear.md`: the analysis that carries over to deferred Jacobians. Jeremy replaced its notation on 17 October 2026, as "Language changes under study" in `programs.md` describes. -- `stage3-build.md`'s "Step 4: shared values as outputs": the plan for bringing outputs back. -- `stage4-build.md`'s "Step 5 in outline", its choice 8 and "Not in stage 4": the stage's last step and what it leaves out. -- `design-audit-summary-2026-10-26.md`: the combined view of four audits of stages 1 to 4, whose order of work the indexing release's plan replaced. The four `design-audit-*` notes beside it give the evidence. -- `learned-2026-10-26.md`: what 26 October 2026 established, where each record is, and what's planned. -- `open.md`: every open item. +Accuracy: `nbs/numbers.qmd`'s "Accuracy": floats within 4 ulps of correctly rounded, except those rustymath's accuracy page lists; some results exact. Unpromised: last bits, behaviour of infinities and NaNs, zero's sign, and which value a write keeps at a repeated position. Last bits can differ by thread count, array size, platform, build, run, route and spelling (each can reorder a fold such as `+/`). FMA and reassociation can overflow differently near the range limits. A few ulps traded for real speed or simpler code become an exception documented on rustymath's accuracy page. Agents chasing identical bits or tighter-than-documented bounds have cost many rewrites. Never spend speed, memory or code on agreement between results, reproducibility, or accuracy past documented bounds, unless Jeremy agrees a real, common use needs it: -For the other notes, Jeremy's rule applies (26 October 2026): "note that meta/ docs used to build old features, like PRD.md, are of historical interest only." `closed.md` archives decisions and finished work, for study when an old decision matters. +- Don't make a fused, compiled or batched route copy another's fold order, block structure or kernel to match its bits; each route may regroup folds for speed. +- Don't add reference cases, harness runs or tests comparing floats bit for bit across routes, thread counts or array sizes, or checking behaviour of infinities and NaNs or zero's sign; compare within the Numbers page's accuracy. +- Don't count identical bits as a design benefit, or differing last bits as a regression. -BPL and rustymath give up minor reproducibility and tiny accuracy promises for faster, simpler code (Jeremy, 26 and 30 October 2026). The "Accuracy" section of `nbs/numbers.qmd` states how accurate float results are: within 4 units in the last place of the correctly rounded result, apart from the exceptions that rustymath's accuracy page lists in one place, with some results exact. Last bits, NaN behaviour and the sign of zero aren't promised. The last bits of a float result can differ between thread counts, array sizes, platforms, builds, runs, routes and spellings. Each of these can change the order in which a fold such as `+/` adds. A few ulps lost for real speed or a simpler implementation become a documented exception on rustymath's accuracy page. Undoing agents' habit of treating identical bits, or a tighter bound than the documented one, as a goal has cost this project many rewrites. Never spend speed, memory or code on agreement between results, on reproducibility, or on accuracy past the documented bounds, unless Jeremy agrees that a real, common use needs it: +Non-commitments don't license breakage: keep the exact results the Numbers page lists. Remove code existing only for agreement between results or accuracy past documented bounds, where removal cuts lines or adds speed without substantive complexity and breaks nothing else. -- Don't make a fused, compiled or batched route follow another route's fold order, block structure or kernel to match its bits. Each route may regroup a fold for its own speed. -- Don't add a reference case, harness run or test that compares floats from two routes, thread counts or array sizes bit for bit, or that checks NaN behaviour or the sign of a zero. Compare floats within the accuracy that the Numbers page describes. -- Don't count identical bits as a benefit of a design, or different last bits as a regression. +Dynamic kinds: autodiff and fast code target static kinds, branching and updating with approaches like Agenda (`⍚`), Under (`f@g`, `f@[p]`) or selective assignment (`x.[p]←`), not with dfn guards on traced values (target, "Goals"). Dynamic kinds serve interactive analysis: tacit code over dataset-sized arrays, and simple dfns over values such as dates and strings. Their code runs correctly and aims to cost at most 4× the same code under static kinds at the same size (target, "Goals"), except where Jeremy agrees, as for a large gap that a reasonable rewrite avoids. Speed work for them that adds much complexity waits for his agreement. Dfn guards on traced values: correct, with no speed work. The coverage matrix and speed tests keep dynamic-kinds cases, so costs stay known and new slowdowns fail; the two dynamic-kinds speed cases in the target's "Acceptance" check the 4× aim. A slow event from a value-dependent type under dynamic kinds: `allows` (known cost). -These non-commitments don't license breaking anything. Keep the exact results that the Numbers page lists. Remove code that exists only for agreement between results, or for accuracy past the documented bounds, where its removal cuts lines or adds speed without substantive complexity and breaks nothing else. +Build and test: -Autodiff and fast code are written for static kinds, with branches as Agenda (`⍚`) or `f@[p]`, not dfn guards on traced values ("Role models: JAX, tinygrad and Dex" in `programs.md`). Dynamic kinds serve interactive analysis. Code under them, and dfn guards on traced values, must run correctly. Speed there is welcome when it costs no complexity, but don't add code or designs to trace, fuse or compile them. They come second, not nowhere. The coverage matrix and the speed tests keep dynamic-kinds cases, so that their costs stay known and a new slowdown fails. Annotate a slow event that a type depending on values causes under dynamic kinds as `allows`, never `gap`. An `allows` line records a known cost. A `gap` line marks work to do. +- `cargo develop`: rebuilds and installs the native extension and `bpl`. `fresh`, `check_file`, timings, instruction counts and `pytest` run the installed build, which a Rust change reaches at the next `cargo develop`. `cargo cs` checks every target in its workspace, tests included, without codegen. Rustymath's tests need `cargo cs` in rustymath. `test` profile, shared with `cargo test`, which compiles missing code but installs nothing. +- `cargo test`: plain while working; one test binary for focused checks. Kernel: `fastws.cargo.run_tests(root, *cargo_options, test_args=(), timeout=30)` builds native test executables once, with no build limit, then runs those executables directly within one shared execution budget. 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. +- `pytest -q`: at the end of a piece of work. Before reporting a `pytest` failure in untouched code, `cargo develop` and rerun. +- Builds: allow 3 minutes; 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`, `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 nothing else without discussion: no scratch crates, alternate target directories, `--release`, `-p` or other-target builds, except the documented build diagnostics, browser work and x86-64 inspection below. Don't use `maturin develop`: its configuration doesn't reuse the normal local build. +- When another agent's unfinished edits stop the shared tree compiling: `cargo wait` with the Bash tool's `run_in_background`, then build after it exits; use it rather than writing a wait loop. `cargo wait` (`fastws-cli`, like `cargo develop`) runs `cargo check` on every workspace target each time a local package's source file or manifest changes, rustymath's included; it exits once the tree compiles, or after 30 minutes with the remaining errors. `cargo wait --path ~/aai-ws/rustymath` waits for rustymath's own workspace, its Python extension included. +- 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. -```bash -cargo develop -cargo test -pytest -q -``` - -Use `cargo develop` to rebuild and install the native extension and `bpl` after Rust changes. It uses the `test` profile shared with `cargo test`. Cargo tests compile missing code but don't install it. Run plain `cargo test` while you work; use one test binary for a focused check. From a kernel, `fastws.cargo.run_tests(root, *args)` runs `cargo test` and returns the build's compiler messages and each test binary's results. Run `pytest -q` at the end of a piece of work. Before you report a `pytest` failure in code you didn't touch, run `cargo develop` and rerun it. Give builds three minutes and test runs one minute; Cargo serializes builds in the same target directory. +Main keeps independent work moving and connected decisions with one owner. The one-graph process has one home: `nbs/target.qmd`'s "The build, in order". Its file map tracks current ownership; it is not a permanent subsystem partition. On a start, stop or handoff, main publishes the active roster by fork prefix and files, never agent IDs. Forks do not message each other; main relays requests to the owner without becoming an approval gate for local API details. A handoff stops the previous owner's edits before the next owner writes. Outside the one-graph build, agree function/module ownership before sharing a file. Use verified edits to preserve others' changes. -Before a stage's closing review and before a release, run the full checks: what CI runs, and the builds for the targets that this Mac's daily builds leave out. Run `cargo develop`, `cargo test`, `pytest -q`, `bpl-nb`, `nbdev-test`, `node tests/input.mjs` and `node tests/highlighting.mjs`. Then run `bpl-tests target_builds /tmp/_wasm`, followed by `wasm_test(DIR)` and `wasm_test(DIR, speed=True)` on that directory. `target_builds` builds basedpl's and rustymath's libraries for Linux x86-64 and the browser package at once. "Codegen and rebuilds" says how to find the function that holds up a build that stays on one core for minutes. +Main reviews coherent milestones against the target: architectural departures, duplicated mechanisms, deletion of old routes, interface fan-out, net source lines and substantive costs. The owning fork decides routine names and implementation details. Delegate mechanical inspection when it resolves meaningful uncertainty; do not require another broad review of every note or API proposal. Main can also cut code and reassign connected work when repeated coordination shows a poor boundary. -Build nothing else without discussion: no scratch crates, alternate target directories, `--release`, `-p` or other-target builds. The documented build diagnostics, browser work and x86-64 inspection below are exceptions. Don't use `maturin develop`; its configuration does not reuse the normal local build. +One-graph steps 1–3 iterate declarations, planning and end-state writing in useful parallel chunks. Existing notes remain usable across ownership changes. No compiles, checks, tests or timings run before step 4. Forks may read code and run the installed build to learn behaviour. A handoff reports the target parts met, net `src/` lines and unresolved departures; record failures in `meta/open.md`'s current work section. Steps 4–5 integrate in dependency order, then check correctness before costs, with the target's full acceptance gates. Once builds are allowed, notify main before building; use `cargo wait` when another edit breaks the tree. Installs (`cargo develop`) belong to the closing pass or tooling work; note the installed baseline first. -When another agent's unfinished edits stop the shared tree compiling, run `cargo wait` with the Bash tool's `run_in_background`, then build after it exits. `cargo wait` comes from `fastws-cli`, as `cargo develop` does. It runs `cargo check` on every target of the workspace each time a source file or manifest of a local package changes, rustymath's included. It exits once the tree compiles, or after 30 minutes with the remaining errors. `cargo wait --path ~/aai-ws/rustymath` waits for rustymath's own workspace, its Python extension included. Use it rather than writing a wait loop. +Costs in copies: `BASEDPL_COSTS=1 cargo test --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. -An already-running Python process retains its loaded extension. In a kernel, use `from basedpl.worker import fresh; fresh(code)` to check the installed `bpl` in a fresh worker instead of restarting the kernel. A BPL error raises `BplError`. `fresh(code, build=BASE)` runs another build, such as the baseline described below. A list of expressions runs each in its own fresh worker and gives one row for each, holding its output or its error. `prefs=` runs before the code in each worker, as in `fresh(cases, prefs='•prefs ["kinds":"static"]')`. Check one expression and read its result before building on it. `fresh`, `time_cases` and the failures that `check_file` and `check_page` give show each input's linear form (`basedpl.syntax.linear`), which shows with brackets and parentheses how BPL groups it. `basedpl.bpltests.check_file` and `check_page` check reference files and documentation through the worker without building Rust. With `build=`, they check another build without installing it. +- `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. +- 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 by the manual `Speed` workflow. Linux counts one core per set of hardware threads sharing a core; a VM not reporting the sets counts each vCPU (the `Speed` workflow's `lscpu` step shows CI's). +- `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)`: CI 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. -Measure with `BASEDPL_COSTS=1 cargo test --test speed` or `basedpl.bpltests.time_cases(cases)`. Both count costs in copies. A copy is the time to write as many floats as the data has items, each negated, into a second buffer. `rustymath::timing::COPIES` stores the copy time of each machine that runs the speed tests, by its CPU, the item's size and the number of items, at the slower end of what that copy normally takes, for copies whose source and destination fit in cache, 64 MiB together (`CACHED_BYTES`). A copy of 65,536 items timed beside each case varied more than 3 times on CI's runner, because it read its source from the L3 cache that other VMs share, while the functions' own times varied at most 1.18 times. Stored times keep that noise out of every cost. A larger copy runs at the speed of memory, which a shared runner varies by up to 2 times between runs, and the cases of that size vary with it. The test measures such a copy once in each run. It also measures a cached size with no entry once, and prints a line to add to `COPIES`. The browser build has entries of its own, for its copy under the JavaScript engine. It can't read the CPU's name, and `tests/wasm-pkg.mjs` passes it the name that Node reads. Each case's line in a log ends with its time for each item, which `logged_costs` keeps in each row, and the log has a copy line for each size of data, which it reads into its result's `copies`. A case whose footprint, its data and the most bytes it holds at once (`rustymath::memory::peak_held`), passes 1 MiB (`rustymath::timing::CORE_CACHE_BYTES`, one core's L2 cache on CI's runner) also measures a copy of its footprint beside it. Its line then ends with the footprint and that copy's time for each item, which `logged_costs` keeps in `footprints`. Data past L2 reaches the L3 cache and memory that the runner shares with other VMs. In runs 56 to 62, cases from about 2 MB up swung by up to 4 times between runs, a different few in each run, while cases of up to 1.5 MB held their times. Costs still divide by the copy of the data. The footprint copy shows whether a case's time follows it. Budgets are single-thread measurements. `thread_scaling()` checks multithreaded speed. It times a large product, a 512×512 SVD and Hermitian eigendecomposition, a stack of QR decompositions, a product of a convolution's windows, a copy of those windows and a large elementwise function on one thread and on t threads, the smaller of 4 and the machine's physical cores. Each case's serial fraction s, the share of its one-thread time that doesn't split across threads, gives its expected speedup S = 1/(s + (1 − s)/t), by Amdahl's law. s comes from first principles for our own kernels. On macOS, Accelerate computes the product, the SVD, the eigendecomposition and the product of windows, and their s comes from measurement. The product of windows fails where its rows no longer split across threads, and the copy of windows where a copy's items no longer split. Its s comes from measurement too, because memory bandwidth, not serial code, limits a copy's gain. A case fails below half its expected gain, 1 + (S − 1)/2, which is 2.5 on 4 cores and 1.5 on 2 for a case that splits completely. A case expected not to split, with s of 1, fails below 0.9. Accelerate's SVD and eigendecomposition are expected not to split, because its GESDD and SYEVD run on one thread whatever its threading setting. The manual `Speed` workflow runs it. On Linux, `thread_scaling` counts one core for each set of hardware threads that share a core, so a VM that doesn't report those sets counts each vCPU as a core. The `Speed` workflow's `lscpu` step shows what CI's VM reports. `speed_costs()` reads the cases in `tests/speed/*.bpl`. `cases=` selects expressions, `wasm=DIR` selects a browser build, `build=` selects another build, such as a kept build's id, `native=True` times rustymath's own linear algebra, which dev builds compile in, and `prefs=` runs BPL before each section's header, such as `•prefs ["kinds":"static"]`. `logged_costs(log)` reads CI output from its text or a file. `cargo_costs(path)` runs either project's Rust speed test, or with `build=EXE` a saved copy of that test's executable, and `pytest_costs(path)` runs 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 the cases' `⍣N` wrapper 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). -Costs in copies can't show a per-call change of a few percent, because code layout alone moves a timing by more than that. `instructions(cases)` counts instructions, which load and layout don't change. Each case is an expression, such as `k(⌈/⊢)⌸a`, run once per step inside `{case ⋄ ⍵}⍣N 0`. That dfn's own call adds about 1,900 instructions a step. A case that names `N` makes its own calls instead, such as `{⍵}⍣N 0`, which makes one dfn call a step. Such a case must make one call per step: a map such as `f¨N⍴0` calls a dfn once on all its cells, so it gives no per-call cost. It runs fresh single-threaded `bpl` processes at four values of `N`, three at each value (`runs=`), and keeps each value's lowest count. For each pair of successive values, it divides the difference in instructions retired by the difference in `N`, which cancels startup and `setup=`, and it reports the median of those three slopes. The median drops a one-off cost that starts once `N` passes some size, because such a cost falls between only one pair. `≢•ucs N⍴"ab" "cde"` has one: it retires about 5×10⁶ more instructions from about 131,350 items on. Every value of `N` lies past the warm-up, from a rough count at 1,000 steps, because a case that allocates an array at each step retires extra instructions over its first steps while the allocator settles. Each process pays a different warm-up, up to about 5×10⁶ instructions, so successive values of `N` lie 5×10⁸ instructions apart, where that difference moves a slope by under 1%. `counts=` sets two or more values instead. It reads `/usr/bin/time -l` on macOS and `perf stat` on Linux. `build=` counts another build. A list of builds, as in `build=[base, None]`, counts each at the same values of `N` and shows them side by side, with the change from the first build to the last. A case whose result stays pending, such as large windows or a transpose, counts making the pending copy, not the copy: `3↕v` retires about 12,000 instructions a step. `force=True` computes each step's result in full, through an indexed write, for any result that is an array with a first item: `3↕v` then counts the copy itself, about 9 million instructions a step. The test that picks those results adds about 6,000 instructions a step. A join computes at once and needs no `force`. +- A case is an expression (`k(⌈/⊢)⌸a`) run once per step in `{case ⋄ ⍵}⍣N 0` (that dfn adds about 1,900 a step). 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 a rough count at 1,000 steps: 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. +- A pending result (large windows, a transpose) counts making the pending copy, not the copy (`3↕v`: about 12,000 a step). `force=True` computes each array result with a first item in full, by an indexed write (`3↕v`: about 9 million, the copy, plus about 6,000 for the test selecting those results). Joins compute at once: no `force`. +- `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 checks only speed budgets. +- `profile(case)` (macOS): instructions by function; takes `force=` like `instructions`. A fresh process repeats the case on one thread while `sample` counts each function at the top of the main thread's stack; share × the build's `instructions` count estimates instructions per step, assuming one retirement rate for all functions. `profile_diff(case, base)`: which functions moved a count, from both builds' estimates; a function one build inlines shows under the other only. `base`: a build, e.g. the one installed before a change; `build=`: the build to profile (default installed). 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. -`tests/instructions.txt` holds the instructions that each case retires for each step, as counted on macOS on Apple silicon. Most cases make one call per step, from `{⍵}⍣N 0` to `fib`. Three map a function over the same 64 cells each step, through Each, Rank's general path and a system function's own item map. `instruction_budgets()` counts each case through `instructions` and marks a case `over` when it retires more than 3% above its count, and `stale` when it retires more than 3% below it. `tests/test_instructions.py` fails on either mark, and skips on other platforms. The change that makes a call cheaper also lowers each count it moves. A new compiler version can inline differently and move the counts, and then they're set again from `bpl-tests instruction_budgets`. CI checks only the speed budgets. +`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`). -`profile(case)` shows where a case's instructions go, by function, on macOS. It takes a case, and `force=`, as `instructions` does. A fresh process repeats the case on one thread while `sample` counts how often each function is at the top of the main thread's stack. A function's share of those samples, times the build's count from `instructions`, estimates its instructions per step. The estimate assumes every function retires instructions at the same rate. When a count moves, `profile_diff(case, base)` shows which functions moved it, from those estimates for both builds. A function that one build inlines and the other calls out of line appears under one build alone. Run them before reading machine code. `base` is a build, such as the one installed before a change, and `build=` sets the build to profile, the installed one by default. +CI costs: every push, the `Test` workflow logs each native (`test` job) and wasm (`wasm` job) cost of the test-profile build the budgets limit. `bpl --costs PATH` times a `.bpl` file or directory as the speed test does, printing each cost with this machine's budget, then cases still over after retries, marked `over`; it never fails on a budget. The manual `Speed` workflow runs it on a `dist` (release-profile) build on CI's runner, beside wasm timing; the `Release` workflow on each binary it builds, on GitHub's runners: release costs, to compare with `Test`'s. `logged_costs` reads all three's logs; each log names its CPU in its copy lines. -`call_profile(case, function)` counts every instruction that one call of `function` retires, with the calls it makes, by function, on macOS on Apple silicon. `lldb` runs the case once per step in a fresh process, lets `skip=` calls of `function` run, then single-steps the next one to its return. `function` is a regex for a demangled name, such as `binder::derived`, and needs code of its own. `base=` names a build whose counts appear beside those of `build`. The allocator's functions, with the functions that they call, share one row, because what they retire depends on the heap's state. A second total leaves that row out. These counts are exact, where `profile`'s come from time. `profile_diff` once put about 50 instructions of a value mark's rise on a conversion loop whose machine code hadn't changed, and single-stepping found them in `derivative::converted`. Use `profile_diff` to find the function whose call changed, then `call_profile` on that function to settle the change. It counts calls of up to 100,000 instructions, at about 450 a second. +Reviewing a run: find costs over budget and costs changed more than 20% since the previous run. `compare_costs(before, after)` lists both, from two runs' logs or rows from `speed_costs` and the other timing functions, comparing times per item where both have them (so `COPIES` changes don't show). Study a slowdown only if it repeats across CI runs or across similar cases. -`bpl-tests NAME ARGUMENT...` runs one of these functions from the shell and prints 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`, `compare_costs`, `wasm_test` or `wasm_stack`. For example, `bpl-tests time_cases '+/v' --threads 4` times `+/v` on four threads. `time_cases`, `instructions` and `speed_costs` take cases as positional arguments. When a case starts with `-`, put the cases last, after `--`, as in `bpl-tests speed_costs -- '-.×m' '+/v'`. `bpl-tests NAME -h` lists a function's arguments. Called in a kernel, the same functions return Python objects, such as the rows that `compare_costs` reads. +Budgets: -On each push, the `Test` workflow logs every native cost in its `test` job and every wasm cost in its `wasm` job. The speed test that writes them times the test-profile build that the budgets limit. `bpl --costs PATH` times a `.bpl` file or a directory of them as the speed test does, and prints each cost with this machine's budget, then each case still over its budget after the retries, marked `over`, without failing on a budget. The manual `Speed` workflow runs it on a `dist` build, the profile releases use, on CI's runner, beside its wasm timing. The `Release` workflow runs it on each binary it builds, on GitHub's runners. Their logs show what releases cost, for comparison with the `Test` workflow's. `logged_costs` reads all three workflows' logs. When you review a run, look for costs over their budgets and for costs that changed by more than 20% since the run before. `compare_costs(before, after)` lists both, from two runs' logs or from the rows that `speed_costs` and the other timing functions give. It measures each change on times per item where both runs give them, so a change to `COPIES` doesn't show as one. Every log names its CPU in its copy lines. Study a slowdown until you know its cause only when it appears in more than one CI run, or when two or more similar cases slow by similar amounts. Treat any other slowdown as normal variation. +- 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, CI on rustymath's kernels). +- A BPL speed file's `⍝⍝:` footer: rule budgets (expected × multiplier) for macOS on Apple silicon, Linux on CI's x86 runner, then (basedpl only) wasm under Node on CI's x86 runner. +- 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 CI's 1 MB L2, 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% against noise, so CI isn't flaky: 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 CI 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. +- 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. +- Speed tests fail on budgets only on `rustymath::timing::BUDGET_CPU`, the CPU they were set for: the development Mac (macOS), CI's runner (Linux, wasm). Other CPUs print every cost, then cases still over after retries, marked `over`, failing none. Costs in copies depend on CPU caches as well as speed (rustymath's DEV.md: the 9950X in CI's pool). +- `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`. -A budget is a case's expected cost times a multiplier: 2 under 50 copies, 1.5 from 50 to 500, and 1.3 over 500. The multiplier is largest for cheap cases, which vary most between runs. The expected cost starts from a first-principles analysis of what the case should cost. A later expected cost comes from a fuller understanding of the case than that analysis gives, such as a platform difference or a cost the analysis missed, and the case's comment records it. Every footer holds the rule budget, the expected cost times its multiplier. Where a case costs more than that allows, a line among its directives holds the column: `⍝⍝⍝ held linux 15.6 wasm 18.9: REASON` for a gap in the code, naming each held column with the cost it is held at, and `⍝⍝⍝ busy linux 2.7: REASON` for a case whose data passes CI's 1 MB L2, which a busy host slows in some runs. The speed tests apply the multiplier to the held cost, and a case's line gives both budgets, as in `budget 32 held from 16`. A gap's hold is stale once the case costs at most 0.9 times its rule budget, inside the 10% margin under which a review holds a column. Each run prints such a case, marked `stale`, whose `held` line can go. A busy host's hold is never stale. Where a case runs within 10% of its budget on a platform that checks it, a `held` line raises that column's budget by 10% as a margin against noise, with the date (Jeremy, 30 October 2026: "so we don't have flaky CI"). Its held cost is the one whose budget, rounded up as `budget` rounds it, comes out at least 10% higher: `⍝⍝⍝ held wasm 17.6` raises a rule budget of 32 to 36. Such a hold goes stale under the rule above, once the case costs at most 0.9 times its rule budget. `rustymath::timing` holds the multiplier (`budget`), reads these lines (`hold`) and applies them (`Budget`) for every speed test. Rustymath's budget files write them as `# held ...` and `# busy ...` lines above a case's line. A platform's cost that no run has measured there, such as an estimate for Linux or wasm, keeps twice the estimate until CI measures it. A held line marks such a cost with a `~` before it, as in `⍝⍝⍝ held linux ~107: REASON`. Rustymath's tables still sit at twice their expected cost, the rule before 27 October 2026. A case over its budget needs study, which finds either a speed bug to fix or something the analysis missed. Avoid raising a budget to match a run without understanding why the case costs what it does. Each `⍝⍝:` footer in a BPL speed file holds a native budget for macOS on Apple silicon, then one for Linux on CI's x86 runner, then, in basedpl's file, a wasm budget for Node on CI's x86 runner. A speed test fails on a budget only on the CPU that the budgets were set for, `rustymath::timing::BUDGET_CPU`: the development Mac's for macOS, and CI's runner's for Linux and wasm. On any other CPU, it prints every cost, then each case still over its budget after the retries, marked `over`, and fails on none. A cost in copies depends on a CPU's caches as well as its speed, as rustymath's DEV.md shows for the 9950X that joined CI's runner pool in October 2026. One expected cost serves every platform at first, until a specific difference on one platform is found. macOS running matrix products and decompositions on Accelerate, where CI runs rustymath's own kernels, is such a difference. A comment above each case, or above its group, gives the expected cost with its reason, and any platform's own cost with the reason for it. `set_budgets(expected)` applies the multiplier and writes the rule budgets for the cases that `expected` names, a dict from each case's code to its expected cost, leaving `held` and `busy` lines as they are. `builds=` limits the change to one platform's column, for a cost that differs there. `estimate=True` keeps twice the cost, for a platform's estimates. A case can appear in more than one section, such as `copies` and `widths`. `sections=` limits the change to the sections it names. `path=` selects a BPL speed file or directory, or one of Rustymath's table-format files, `tests/speed.txt` or `tests/speed_python.txt`. +Routes: -`tests/explain.rs`'s `speed_cases_take_their_routes` captures every speed case after warm-up, with its normal setup and size, forcing the result outside timing (`timing::routes`). It also captures each reference case that has route directives, once under ordinary preferences. Within `cargo test`, only this test checks route directives. `check_file` checks a file's route directives too, as this test does, on one thread. Put `⍝⍝⍝ route NAME` above a case to require that fast route to execute. A permitted slow event never excuses a missing required route. +- `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`), and each reference case with route directives once under ordinary preferences. In `cargo test`, only it checks route directives; `check_file` checks a file's 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. +- 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. +- Coverage matrix (`reference::coverage`, in the route test): every pervasive form in the facts table against the executors, on floats, under dynamic and static kinds: between two other steps of one block stage (`2×*1+x`, `2×(1+x)|y`, `2×(1+x)|c` for c = 0, 1, 2, 0.5, ¯1, `2×c|1+x` for 1, 2); fold `|/2×x`; product `m+.|k`; each one's gradient. Fusion at any size, every stage compiled (`settings::Compiles::Always`). Required routes: mapping `compiled`, fold `fused fold`, gradient `trace`. A block stage running compiled parts takes `compiled`; if its compile setting asks for compiled parts, it declines `compiled` once per step that doesn't lower, named by facts-table form (`compiled: residue`). A call large enough to wait that no block stage computes declines `fusion` with its plan's reason (`fusion::defers`). Missing-route messages list the run's declines. +- `tests/coverage.txt`: accepted gaps, one per line: section, `dynamic`/`static`, tab, case code, tab, a `⍝⍝⍝ gap` (work to do) or `⍝⍝⍝ allows` (known cost) line. Listed cases needn't take their route. Dynamic kinds keep calls whose result type depends on values out of block stages and traces by design; such misses take `allows` lines citing the rule. Lines whose event no longer appears fail (a fix deletes its gap lines), as do lines naming no case. A change opening a gap fails with the case's code and its declines or slow events: add a line naming one, with what should happen. Cases whose evaluation fails (`~` on floats) are printed and skipped. +- 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. -The checker rejects unlisted slow events: pending items computed outside a fused group (`stored`), a call that computes pending items (`forced`), a batched fallback (`fallbacks`), a write that leaves mixed storage (`mixed`), itemwise work (`itemwise`), or a stopped trace (`stopped`). Name the event as `•explain` does: the function for `forced`, the code for `mixed`, the route for `itemwise`, and the reason for the others. `⍝⍝⍝ allows RECORD "EVENT": principle` permits work such as a matrix product reading its arguments whole. `⍝⍝⍝ gap RECORD "EVENT": what should happen` names a fast-path gap. A stopped trace takes an `allows` line only where the stop is by design, as dynamic kinds' rule for types that depend on values makes it. Any other stop takes a `gap` line. A considered route that was not taken (`declined`) is informational. You can name its route and reason, such as `fusion: an exact argument`, in a `gap` or `allows`. Every annotation must match an observed event. Remove it when the event disappears. `cargo test -- --nocapture` prints the remaining gaps. +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. -The route test also runs the coverage matrix (`reference::coverage`), which checks every pervasive form in the facts table against the executors. Each form gives cases on floats under dynamic kinds and under static kinds: the form between two other steps of one block stage, as in `2×*1+x`, `2×(1+x)|y`, `2×(1+x)|c` for the constants 0, 1, 2, 0.5 and ¯1, and `2×c|1+x` for 1 and 2; its fold, `|/2×x`; the product `m+.|k`; and the gradients of each. They run with fusion at any size and every stage compiled (`settings::Compiles::Always`). A mapping case must take the `compiled` route, a fold the `fused fold` route, and a gradient the `trace` route. A block stage that runs compiled parts takes `compiled`. Where its compile setting asks for compiled parts, it declines `compiled` once for each step that doesn't lower, named by its form in the facts table, as in `compiled: residue`. A call large enough to wait that no block stage computes declines `fusion` with its plan's reason (`fusion::defers`). The message for a missing required route lists the run's declines. `tests/coverage.txt` lists the gaps that the matrix accepts, one line each: the section, `dynamic` or `static`, a tab, the case's code, a tab, then a `⍝⍝⍝ gap` line for work to do or a `⍝⍝⍝ allows` line for a known cost. By design, dynamic kinds keep a call whose result's type its values decide out of block stages and traces. Cases that miss their route for that reason take `allows` lines that cite the rule. A case that it lists needn't take its route. As with an annotation on a reference case, a line whose event no longer appears fails, so a fix deletes its gap's lines. A line that names no case is an error too. A change that opens a gap fails with the case's code and its declines or slow events: add a line that names one of them, with what should happen. The test prints and skips the cases whose evaluation fails, such as `~` on floats. +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. -Rust code in both repositories reports routes taken, declines and stopped traces through `rustymath::events`. basedpl records itemwise routes in `•explain`'s own rows. With no capture, reporting costs one atomic load. The route test compares observed successes, declines and fixed itemwise routes with the IDs declared there, including cases for idioms and annotated references. The inventory includes warm-up observations. Route contracts use the warmed run alone. This checks event-name coverage, not every branch at an emission site. Study any unexpected event before adding an annotation, as you would a speed case over budget. +- `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. +- 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. +- 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. -The speed tests and `thread_scaling` time each failing case again after the rest, after each pause in `rustymath::timing::RETRY_PAUSES`, of 0.1, 1 and 10 seconds, and fail only for a case that fails every time. The 10-second pause waits out a longer burst of load from other VMs on CI's runner. Before each retry, the speed tests and `bpl --costs` keep the thread busy for 0.1 s (`rustymath::timing::warm`). On this Mac, small cases timed straight after the 10-second pause without it cost up to 3 times as much, because an idle core runs slower until it has been busy for a while. Each retry prints the case's earlier and new costs, or speedups in `thread_scaling`, even in a passing run. `logged_costs` keeps a retried case's lowest cost. Study a case that needs a retry in most runs, as you would a slowdown. +Edit journal: Claude Code sessions record every exhash edit in `/tmp/exhash-journal/`, by agent (kernel startup sets `EXHASH_JOURNAL` from the session id, `EXHASH_AUTHOR` from the `RUSTYGATE_AGENT` rustygate gives each subagent's kernel). A fork reports its lines with `from exhash import journal`, then `journal(os.environ['EXHASH_JOURNAL'], author=os.environ['EXHASH_AUTHOR']).stat()` (a kernel's `exhash` is a function without `journal`). Main reviews a fork's edits with `.diff()` on its agent id's journal: git can't separate forks' uncommitted edits. -For a change meant to make something faster, or a change to code that every call goes through, time the cases it affects before and after the change. `cargo develop` keeps each build it installs, read-only, under an id that hashes the build's inputs and files, and installs it by pointing one link at it (`fastws.store`). `fastws.store.builds(root)` lists the newest builds of the project at `root`: each one's id, when it was kept, who kept it, and the input files that changed since the build before, with `*` on the installed one. Before your change, note the installed build's id. Every timing tool takes `build=`: a prefix of a kept build's id, a path to a `bpl`, or `None` for the installed build. It resolves each build once, as it starts, so another agent's `cargo develop` never changes a run in progress. Time both builds in one call: `time_cases(cases, build=[base, None])` in a kernel, where `base` is the id you noted, or `bpl-tests time_cases CASE --build BASE NEW` in the shell, with both ids. Each build runs in fresh workers that take turns in alternating order, `rounds` times, and the result holds each case's fastest time per item on each build, in picoseconds. `compare_costs(*result)` lists the cases that moved. Timing sessions seed their random generators at 0, and so do the processes that `instructions`, `profile` and `call_profile` start. Every process and every build then draws the same data. A build from before a change to `rustymath::timing::COPIES` divides its costs by a different copy. On this Mac, a fused case over a large input can cost up to about 15% more in one process than in another, depending on whether `malloc` starts the input on a 32 KB boundary, where every one of a block stage's blocks then ends. Time again with more `rounds` before you read a lone difference of that size as a regression. The speed test fails a case only past its budget. It misses any smaller slowdown. Other changes need only the speed test. Don't restore old code in the shared tree to get a baseline. `pytest_costs` and `run_pytest` take a rustymath build's id as `build=`, for a change to rustymath's Python API. On macOS, native timings of matrix products and decompositions mostly measure Accelerate. Rustymath's DEV.md says how to time rustymath's own kernels. +Timing lock `/tmp/aai-ws-timing.lock`: -Compare static kinds with dynamic kinds, or programs with the interpreter, only on code written for static kinds, with explicit sizes on literals and data. Code written for dynamic kinds can give wrong results under static kinds, and its timings don't show what static code costs. +- 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. +- 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). -In a Claude Code session, every exhash edit is recorded in `/tmp/exhash-journal/` under the agent that made it. The kernel startup sets `EXHASH_JOURNAL` from the session id, and `EXHASH_AUTHOR` from the `RUSTYGATE_AGENT` that rustygate gives each subagent's kernel. A fork reports its own lines with `from exhash import journal`, then `journal(os.environ['EXHASH_JOURNAL'], author=os.environ['EXHASH_AUTHOR']).stat()`. In a kernel, `exhash` names a function that has no `journal`. Main reviews a fork's edits with `.diff()` on the journal for that fork's agent id. git can't separate one fork's uncommitted edits from another's. - -Cooperating tools share the machine's timing lock, `/tmp/aai-ws-timing.lock`. Timings take it exclusively. Every rustc call takes it shared, through the rustc wrapper `.cargo/rustc-lock` that `ws-sync` writes into the workspace's Cargo config, so a build waits behind a timing however it was started. The reference and explain tests, pytest, `check_file`, `check_page`, `bpl-nb`, `instructions`, `profile` and `wasm_test` without `speed` also take it shared. A timing holds a second file, `/tmp/aai-ws-timing.gate`, from the start of its wait, and a shared hold passes that file first. No new build or test run then starts while a timing waits, and the timing runs once the holds already taken end. Rust code takes the lock through `rustymath::timing`, and Python code through `fastws.timelock`. When a kernel calls `time_cases` or native `speed_costs`, the `bpl` worker that the call starts takes the lock, not the kernel. Node can't lock a file. `wasm_test` and `speed_costs(wasm=DIR)` hold the lock in Python while Node runs. After five minutes a timing fails. A build or test run then goes ahead without the lock. After 30 seconds, an exclusive wait prints the processes with the lock file open. The list can include processes that are only waiting for the lock. A process that holds the lock shared must not start a build or a timing, which would wait behind a timing that waits for that hold until the five minutes end. Never take the lock yourself in a kernel process. Never coordinate timings by hand: the lock schedules builds and timings. A hold that outlasts its call blocks the timings that later calls launch. Native timings run on one thread and request user-interactive macOS QoS, which favours the performance cores. QoS is not CPU affinity. - -Run `cargo wasm` during development only for browser work. Agents use `cargo wasm --out DIR`, then run the browser tests against that build with `basedpl.bpltests.wasm_test(DIR)` for `tests/wasm.mjs` and `wasm_test(DIR, speed=True)` for `tests/wasm-speed.mjs`. On this Mac, that speed test prints every cost and marks each case still over its wasm budget without failing, because those budgets were set for CI's runner. This avoids replacing a module another agent is using. For other edits under `cfg(web)` or `host_web.rs`, check compilation with `cargo cs -p basedpl-wasm --target wasm32-unknown-unknown`, which checks the browser wrapper crate together with basedpl's browser code. `cargo cs` alone 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 `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: budgets are set for CI's runner). 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` shows the machine code of the function whose demangled name matches the regex `PATTERN`, read from the installed `bpl` with `objdump`. `--build BUILD` reads another build instead. Where several functions match, it lists their names. A function that is always inlined has no code of its own. Read its callers instead. +`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. -The JIT's kernels exist only while a process runs. With `BASEDPL_JIT_DISASM` set, `bpl` prints each kernel it compiles to stderr: a line describing the kernel, then its machine code. `bpl-tests jit_disasm CASE --setup SETUP --prefs PREFS` runs a case that way, as in `bpl-tests jit_disasm '+/ux>127ₓ₁' --setup 'ux←(¿(2*22)⍴256)ₓ₁' --prefs '•prefs ["kinds":"static"]'`. A stage compiles on its first run only from `COMPILES_AT` items, about 3 million. +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` shows the x86-64 code that CI and Linux run. It first builds basedpl's library and the rustymath library it links for Linux x86-64, into `/tmp/aai-ws-x86`. Where an unfinished edit stops basedpl compiling, it reads rustymath's library alone, and its output says so first. After a change, that build takes minutes. Use it only to answer an x86-code question. The target is Linux because macOS builds leave out rustymath's native linear algebra kernels. Only the libraries build, because the Python extension crates can't be built for Linux on this Mac. Their `release` profile matches CI's `test` profile in its x86-64 flags and optimization settings. The build sets `CC_x86_64_unknown_linux_gnu=clang` for itself. Don't move that setting into `.cargo/config.toml`. CI's Linux release builds also read that file, in a manylinux container that has no `clang`. Don't use `--emit asm`. A build with it splits code into other codegen units and inlines differently. +`--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. ## Workspace, builds and CI | Crate | Responsibility | |---|---| -| `basedpl` | Python-free interpreter and public Rust API. | -| `cli/` (`basedpl-cli`) | `bpl`, REPL, Jupyter kernel and JSON-lines worker. | -| `py/` (`basedpl-py`) | Unpublished PyO3 extension, `basedpl._core`. | -| `wasm/` (`basedpl-wasm`) | Browser bindings. | -| `xtask/` | `cargo wasm`, which builds the browser package. | +| `basedpl` | Python-free interpreter; public Rust API | +| `cli/` (`basedpl-cli`) | `bpl`, REPL, Jupyter kernel, JSON-lines worker | +| `py/` (`basedpl-py`) | unpublished PyO3 extension `basedpl._core` | +| `wasm/` (`basedpl-wasm`) | browser bindings | +| `xtask/` | `cargo wasm`: builds the browser package | -The default workspace members are core, CLI and Python; wasm/xtask build only when requested. Frontends use the core's public API. Core/CLI Rust tests live in `tests/` and `cli/tests/`. Libraries set `test = false` to avoid separate `cfg(test)` builds. Because no build runs `#[cfg(test)]` code, libraries hold no inline tests or test-only counters. Any internal behaviour worth a test, such as whether an expression fuses, is worth knowing for every user too. Tests in `tests/` check it through the public API, such as `•explain`, `•storage` or `•memory`. PyO3 is in its own crate, not an optional core feature. The core uses `serde_json` for the process protocol and data functions. +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. -Rust 1.98 or later supplies algebraic float operations. `test` inherits `release`: opt-level 2, incremental compilation, thin LTO across each crate's codegen units, no debug information, overflow checks or debug assertions. A `debug_assert!` therefore never runs here. Write a check that matters as an always-on test. `.cargo/config.toml` adds LLVM's loop and SLP vectorizers as extra passes, in that order, and targets x86-64-v3 on x86. Target-specific rustflags repeat the passes because they replace build-wide flags. `dist` disables incremental compilation and strips symbols. Keep these settings aligned with rustymath's. +Profiles: Rust 1.98 or later (algebraic float operations). `test` inherits `release`: opt-level 2, 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`: LLVM's loop and SLP vectorizers as extra passes, in that order; x86-64-v3 on x86; target-specific rustflags repeat the passes (they replace build-wide flags). `dist`: no incremental compilation; strips symbols. Keep these aligned with rustymath's. -`fastws`'s `cargo develop` builds profile `test`, installs the extension and stages `bpl` into the venv. `py/` enables PyO3's `extension-module` and supplies its link arguments without libpython. Editable installs use `fastws.build_backend`; they install metadata and a path to `python/` without compiling Rust. After a standalone `uv sync` or editable pip install, run `cargo develop`. `ws-sync` performs that rebuild after syncing. Wheels use maturin for the extension and `cargo stage` for the Python-free CLI. Linux staging runs inside the manylinux container. +Packaging: `fastws`'s `cargo develop` builds profile `test`, 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. -Basedpl's crates pass on rustymath's default `native` feature, which rustymath's DEV.md describes. Each depends on the crate below it with `default-features = false` and makes `native` its own default. Release staging and wheel builds both pass `--no-default-features`. The released Mac builds therefore leave out rustymath's own linear algebra. The Python binding inherits the CLI's pure-Rust frontend dependencies to keep shared dependency features identical for release builds. On Apple targets, `flate2` uses the system zlib. Other targets keep its Rust backend, `miniz_oxide`. +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`. -Use published dependency ranges with the lower bounds required by the APIs in use. `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 relying on them in basedpl CI. Don't edit fastws-generated Cargo patches, `.git/fastws-cargo-key` or workspace configuration, and don't commit the workspace-generated `Cargo.lock`. `meta/` is ignored planning material, not contributor documentation. `[tool.uv] cache-keys` tracks packaging manifests, not Rust sources. +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. -`Test` runs native and wasm jobs on pushes to main and pull requests. Native CI runs `cargo test`, editable installation, `cargo develop`, pytest, `bpl-nb` and `nbdev-test`. Python tests use the same test-profile build. The wasm job builds with `cargo wasm --strip` and runs both Node suites. Both jobs run on `ubicloud-standard-4-ubuntu-2604`, which is Ubuntu 26.04 on the `ubicloud-standard-4` VM size that speed budgets are set for. They generate a lockfile before restoring `Swatinem/rust-cache`, making dependency changes part of the cache key. `Speed` is manually triggered. Its `wasm` job restores the `Test` workflow's wasm cache. Its native job builds the `dist` profile without one. +CI: `Test` runs native and wasm jobs on pushes to main and pull requests. Native: `cargo test`, editable installation, `cargo develop`, pytest, `bpl-nb`, `nbdev-test`; Python tests use the same test-profile build. Wasm: `cargo wasm --strip`, both Node suites. Both on `ubicloud-standard-4-ubuntu-2604`: Ubuntu 26.04 on `ubicloud-standard-4`, the VM size speed budgets are set for. A lockfile is generated before restoring `Swatinem/rust-cache` → dependency changes are part of the cache key. `Speed`: manual; its `wasm` job restores `Test`'s wasm cache; its native job builds `dist` without one. -`Release` runs separately on `v*` tags on GitHub's runners. It builds dist wheels, sdist, standalone CLI artifacts and the Windows keyboard archive, then publishes Python artifacts and the GitHub release. Its npm job publishes the stripped browser package through trusted publishing and purges jsDelivr's package files to avoid stale unversioned URLs. Release neither runs tests nor waits for the Test workflow. +`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`. ### Codegen and rebuilds -Local builds of the `test` profile are incremental, with thin LTO across each crate's codegen units (Cargo's default, `lto = false`). Incremental builds put `#[inline]` helpers and generic instances, such as iterator adapters, `array::map` and `copy_from_slice`, in codegen units of their own, where only LTO can inline them. Measured on 22 October 2026, thin LTO cut per-call instruction counts by 4 to 18%, to within about 3% of `dist`. A one-line edit in basedpl then rebuilt `bpl` in about 9 to 12 s, against 6 to 8 s without LTO, and an edit in rustymath took about 30 s either way. Thin LTO imports a function into another unit, where it can inline it, only below LLVM's import limit of 100 instructions. A long fused iterator chain can exceed that limit. rustymath's DEV.md lists what else stays out of line, under "Loops and codegen". Under LTO, rustc runs the extra `passes` from `.cargo/config.toml` before the link step, and the loop vectorizer only at that step. The configs list `loop-vectorize` before `slp-vectorizer` for that reason. Listed alone, SLP packed the lanes of short inner loops before the loop vectorizer could vectorize those loops across items. `∪c` then ran 71% slower, in `dist` builds too. After an interrupted build, a link error on undefined `.llvm.` symbols means incremental compilation reused stale codegen units. `cargo clean -p --profile test` clears them. +Local `test` builds: incremental, thin LTO across each crate's codegen units (Cargo's default, `lto = false`). Incremental builds put `#[inline]` helpers and generic instances (iterator adapters, `array::map`, `copy_from_slice`) in codegen units of their own, where only LTO can inline them. Thin LTO: per-call instructions 4-18% lower, within about 3% of `dist`; a one-line basedpl edit rebuilds `bpl` in about 9-12 s (6-8 s without LTO); a rustymath edit about 30 s either way. Thin LTO imports a function into another unit, to inline it, only below LLVM's import limit of 100 instructions; a long fused iterator chain can exceed it. What else stays out of line: rustymath's DEV.md "Loops and codegen". Under LTO, rustc runs `.cargo/config.toml`'s extra `passes` before the link step, the loop vectorizer only at that step → configs list `loop-vectorize` before `slp-vectorizer`: listed alone, SLP packed short inner loops' lanes before the loop vectorizer could vectorize them across items (`∪c` 71% slower, `dist` too). After an interrupted build, a link error on undefined `.llvm.` symbols = stale incremental codegen units: `cargo clean -p --profile test`. + +Moving lines changes the code below them (each panic site, e.g. a bounds check, holds its line) → every unit holding that code recompiles. Split related source into child modules to limit the shifts: e.g. the generic block runner in its own file, `program/blocks.rs` (a one-line edit above a runner in its own file recompiled one 140 KB unit in about 2.4 s; with the runner below it in the same file, about 9 s). Children: `use super::*`; `pub(super)` for parents and siblings. Parents own public re-exports. -Moving lines changes the code below them, because each panic site, such as a bounds check, holds its line. Every unit that holds that code then recompiles. Related source is split into child modules to limit those shifts. For example, the generic block runner has a file of its own, `program/blocks.rs`. When fusion's old runner sat in `fusion/blocks.rs`, a one-line edit to `Graph::run` recompiled one unit of about 140 KB in about 2.4 s, against about 9 s when the runner sat below it in the same file. Children use `use super::*` and expose items to parents and siblings with `pub(super)`. Parents own public re-exports. +Generic loop instances can concentrate in the defining module's codegen unit. A closure passed to a generic adapter such as `map` makes an instance that can compile in another unit → every edit to the closure's function recompiles that unit (once 1.9 MB, about 8 s; a plain loop avoided it). Mark loop helpers, nested ones included, `#[inline]` when their instances must compile with the caller's kernel table. Kernel tables: separate modules `booleans`, `wholes`, `float_kernels`, `complex_kernels`; `at_widths!` gives the concrete widths their own modules. Keep new tables separate; add kernel specializations only for measured gains. -Generic loop instances can concentrate in the defining module's codegen unit. A closure passed to a generic adapter such as `map` makes an instance that can compile in another codegen unit. Every edit to the closure's function then recompiles that unit. When fusion's old `Graph::run` built a vector with `map`, an edit to it recompiled a 1.9 MB unit in about 8 s, and a plain loop avoided that. Mark loop helpers `#[inline]` when their instances need to compile with the caller's kernel table, including nested helpers. Kernel tables have separate modules: `booleans`, `wholes`, `float_kernels` and `complex_kernels`. `at_widths!` gives the concrete widths their own modules. Keep new tables separate and add kernel specializations only for measured gains. +Diagnostics of the current compilation arrangement; don't create scratch builds to approximate it: -Use `cargo build --lib --bins --profile test --timings` to inspect the normal build. Its report under `target/cargo-timings/` separates frontend and codegen time. `BASEDPL_UNITS=1 cargo test --test units -- --nocapture` lists large units and their functions. `BASEDPL_SIZES=pattern cargo test --test units -- --nocapture` measures machine code matching a demangled-name regex. `BASEDPL_SIZES` sees only basedpl's own codegen units. `BASEDPL_SIZES_DIFF=old:new cargo test --test units machine_code_growth -- --nocapture` compares two binaries, such as copies of `bpl` from before and after a change, by generic function across every crate. `BASEDPL_CALLS=pattern cargo test --test units -- --nocapture` lists each call from a function whose demangled name matches to a function in another codegen unit, in basedpl's build and in the rustymath rlib it links. Those calls stayed out of line through thin LTO. rustymath's DEV.md says which calls do, under "Loops and codegen". After an edit, `BASEDPL_RECOMPILED=pattern cargo test --test units -- --nocapture` lists the units that the rebuild compiled again, each with its biggest functions whose demangled names match. These commands diagnose the current compilation arrangement; don't create scratch builds to approximate it. +- `cargo build --lib --bins --profile test --timings`: the normal build; report under `target/cargo-timings/` separates frontend and codegen time. +- `BASEDPL_UNITS=1 cargo test --test units -- --nocapture`: large units and their functions. +- `BASEDPL_SIZES=pattern cargo test --test units -- --nocapture`: machine code matching a demangled-name regex; basedpl's own codegen units only. +- `BASEDPL_SIZES_DIFF=old:new cargo test --test units machine_code_growth -- --nocapture`: two binaries (e.g. `bpl` before and after a change) compared by generic function, across every crate. +- `BASEDPL_CALLS=pattern cargo test --test units -- --nocapture`: each call from a matching function to a function in another codegen unit, in basedpl's build and its linked rustymath rlib: calls that stayed out of line through thin LTO (which do: rustymath's DEV.md "Loops and codegen"). +- `BASEDPL_RECOMPILED=pattern cargo test --test units -- --nocapture`, after an edit: units the rebuild compiled again, each with its biggest matching functions. -A build that stays on one core for minutes is usually waiting on one function that an LLVM pass takes minutes over. LLVM's loop vectorizer chooses each loop's vector width and interleave count from the target's cost model. A loop that it vectorizes in seconds for arm64 can take it minutes for x86-64. On macOS, `sample 5 -file /tmp/_sample.txt` on the busy `rustc` shows the pass, such as `LoopVectorizePass` unrolling a loop by its interleave factor (`VPlanTransforms::unrollByUF`). The nightly toolchain names the function. `cargo +nightly rustc -p CRATE --lib --release --target x86_64-unknown-linux-gnu --target-dir /tmp/_x86 --config 'build.rustc-wrapper=""' -- -Z llvm-time-trace` writes LLVM's time for each pass on each function to `CRATE-.llvm_timings.json` under the target directory. With sccache on, a cached crate writes no trace. A crate with C code also needs `CC_x86_64_unknown_linux_gnu=clang`. `rg -o '"dur":[0-9]{8,},"name":"[^"]*"(,"args":\{"detail":"[^"]*"\})?' FILE` lists the passes that took 100 s or more, with each function's mangled name. One crate's trace can be gigabytes. `cargo llvm-lines -p rustymath --lib --release --target x86_64-unknown-linux-gnu` lists each function's lines of unoptimized LLVM IR, with its copies. It shows where generic instances multiply, but not what LLVM does after inlining. `cargo wasm` builds `xtask` for the host, rustymath included. On CI's x86-64 runner, the wasm job therefore waits on any stall in rustymath's x86-64 build. After a change to inlining or to loops, time an x86-64 build of the crate. rustymath's DEV.md, under "Build time and code size", gives the command and describes the stall found in October 2026. +Build on one core for minutes: usually one function an LLVM pass takes minutes over. The loop vectorizer picks each loop's vector width and interleave count from the target's cost model: seconds for arm64 can be minutes for x86-64. + +- macOS: `sample 5 -file /tmp/_sample.txt` on the busy `rustc` shows the pass (e.g. `LoopVectorizePass` unrolling a loop by its interleave factor, `VPlanTransforms::unrollByUF`). +- The nightly toolchain names the function: `cargo +nightly rustc -p CRATE --lib --release --target x86_64-unknown-linux-gnu --target-dir /tmp/_x86 --config 'build.rustc-wrapper=""' -- -Z llvm-time-trace` writes each pass's time per function to `CRATE-.llvm_timings.json` under the target directory. sccache on → a cached crate writes no trace. A crate with C code also needs `CC_x86_64_unknown_linux_gnu=clang`. `rg -o '"dur":[0-9]{8,},"name":"[^"]*"(,"args":\{"detail":"[^"]*"\})?' FILE`: passes of 100 s or more, with each function's mangled name. One crate's trace can be gigabytes. +- `cargo llvm-lines -p rustymath --lib --release --target x86_64-unknown-linux-gnu`: each function's lines of unoptimized LLVM IR, with its copies: where generic instances multiply, not what LLVM does after inlining. +- `cargo wasm` builds `xtask` for the host, rustymath included → CI's wasm job waits on any stall in rustymath's x86-64 build. After a change to inlining or loops, time an x86-64 build of the crate (command and the October 2026 stall: rustymath's DEV.md "Build time and code size"). ## Code map | Files | Responsibility | |---|---| -| `syntax.rs`, `name.rs`, `eval/binder.rs`, `eval/tree.rs` | Structural parse, interned names, runtime category binding, trains, dot access and subscripts, and dfn statements' compiled recordings. | -| `eval.rs`, `eval/session.rs`, `eval/bindings.rs`, `eval/operators.rs`, `eval/rewrite.rs`, `eval/assign.rs` | Shared calls, sessions/modules, binding storage, operators, fast combinations, scopes and assignments. | -| `execution.rs`, `error.rs` | Cancellation, deadlines, input/output and retained diagnostics. | -| `array.rs`, `array/storage.rs`, `array/text.rs` | Values, layouts, compact/mixed storage, gathering and source formatting. | -| `agreement.rs`, `keyed.rs`, `selection.rs` | Rustymath's broadcasting `Agreement` with basedpl's layouts and their renderer, reading paired items, named/keyed alignment, and selection/writeback. | -| `primitive.rs`, `primitive/` | Primitive metadata, ranks/axes, inverses and array operations. Structural glyphs read their arguments, take each result's layout and `Steps` from a plan in rustymath's `structure`, and copy by it. | -| `number.rs`, `element.rs`, `pervasive.rs`, `fusion.rs`, `explain.rs` | Numeric domains, item operations, compact kernels, pending arrays with the programs that compute them, and the events that `•explain` records and the live nodes that `•memory` counts. | -| `derivative.rs`, `linear.rs` | `∂`: duals, tags, seeding, the recording of rustymath's partial derivatives and the rules for operators, and the record of linear calls that reverse mode transposes. | -| `transform.rs` | The core that running transformations share: one stack of levels, routing to the newest level among a call's arguments, and the read point for code that isn't a call. | -| `batch.rs` | Batched maps: the batch carrier, the try in `each_pair` that calls the operand of an Each, a Rank or an outer product once on batches of its cells while the `batch` pref is on, and the batch rules that routing hands calls to. The pref is on by default. `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` | Typed programs, which pending work and `∂` run: plans that give a call's result type and layout, keys and axis names included, from its arguments' types and layouts, the trace level that turns `∂` of a function into a program of typed steps that holds the call and its derivative (`program/trace.rs`), kept on a dfn's definition or, for any other function, by its structure (`program::Tacit`), replay of the program on later calls whose arguments and free values it still fits (`program/run.rs`), and the units that replay its runs of elementwise steps: block stages (`program/blocks.rs`), which run a block of items at a time, on compiled kernels (`jit.rs`) for runs of steps that lower and on the interpreter's kernels for the other steps. `blocks::plan` finds the runs. `program/contraction.rs` reads the sources of folds of products and of copies, and `program/layout.rs` sets the maps' storage orders, for fusion and traces alike. `program/verify.rs` checks a program's contracts in reference runs. | -| `search.rs`, `number_theory.rs`, `polynomial.rs`, `distribution.rs` | Array-level adapters to rustymath. | -| `system.rs`, `data.rs` | System-function metadata, native functions, options and import rules. | -| `csv.rs`, `json.rs`, `xml.rs`, `image.rs`, `plot.rs`, `canvas.rs`, `display.rs` | Formats, rich values and rendering. | -| `protocol.rs`, `reference.rs`, `timing.rs` | Process encoding, semantic checks and measurements. | -| `host.rs`, `host_web.rs`, `js.rs` | Platform services and JavaScript values/functions. | -| `symbols.rs`, `inspection.rs` | Shared names, shortcuts, syntax/help metadata and inspection. | -| `cli/src/`, `py/src/lib.rs`, `wasm/src/lib.rs` | Frontend boundaries. | - -User-facing language rules belong in `nbs/syntax.qmd`, `evaluation.qmd`, `numbers.qmd`, `arrays.qmd`, `terms.qmd` and the glyph/system pages. Define terms in `terms.qmd` before using them. The sections below record contributor contracts and ownership rather than duplicating the language reference. +| `syntax.rs`, `name.rs`, `eval/binder.rs`, `eval/tree.rs` | structural parse; interned names; runtime category binding; trains; dot access, subscripts; dfn statements' compiled recordings | +| `eval.rs`, `eval/session.rs`, `eval/bindings.rs`, `eval/operators.rs`, `eval/assign.rs`, `eval/dissect.rs` | shared calls, function values; sessions, modules; binding storage; operators; scopes; assignments; expression trees of bound values | +| `execution.rs`, `error.rs` | cancellation, deadlines, input/output, retained diagnostics | +| `array.rs`, `array/storage.rs`, `array/text.rs` | values, layouts, compact and mixed storage, gathering, source formatting | +| `agreement.rs`, `keyed.rs`, `selection.rs` | rustymath's broadcasting `Agreement` with basedpl's layouts and their renderer; reading paired items; named and keyed alignment; selection, writeback | +| `primitive.rs`, `primitive/` | primitive metadata, ranks, axes, inverses, array operations. Structural glyphs read their arguments, take each result's layout and `Steps` from a plan in rustymath's `structure`, copy by it | +| `number.rs`, `element.rs`, `pervasive.rs`, `fusion.rs`, `explain.rs` | numeric domains; item operations; compact kernels; dispatch's gate (`fusion::gate`, the cost rule `waits`), nodes for copies, joins, selects and conversions, forcing (`resolved`); events `•explain` records; live nodes `•memory` counts | +| `graph.rs` | the one lazy graph: interned nodes (`graph::Node`: a step, operands read through rustymath `Mapping`s, a `Desc`), their work, forcing a node through a `program::Program` and keeping its result | +| `derivative.rs`, `linear.rs`, `optimize.rs` | `∂`: duals, tags, seeding, rustymath's partials through rows, operator rules, loops' JVP and transpose; the transpose and substitution passes over the graph's linear nodes; `⇄` through `rustymath::optimize` | +| `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`) | +| `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 | +| `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 | + +User-facing language rules: `nbs/syntax.qmd`, `evaluation.qmd`, `numbers.qmd`, `arrays.qmd`, `terms.qmd`, glyph and system pages; define terms in `terms.qmd` before use. Sections below: contributor contracts and ownership, not the language reference. ## Parser, evaluator and scopes @@ -181,7 +218,7 @@ The lexer/parser records byte spans and structural syntax without evaluating exp `symbols.rs` owns APL, simplified and traditional Hanzi spellings. Its rows also hold each glyph's names: a glyph name for the character, which is also its page's file name in `nbs/glyphs/`, and a name for each valence, or one name where both valences do the same thing. The glyph reference, named entry and help read them there. Python's exported names read them, with each system function's name, through `symbols::builtins`. `hanzi_aliases` enables Hanzi glyph spellings, canonicalizing token decisions while retaining original source and spans. Other Hanzi remain name characters. `_` quotes the next non-whitespace character into a name, including a glyph alias. The `notation` preference affects structured function/operator display, not stored source, serialization or structural APIs; help and editor metadata use the same spelling table. -The binder resolves runtime categories right to left. It owns unfinished strands, trains and bound arguments; shared function nodes are immutable. Grammatical reduction produces application requests instead of calling functions. An array that holds a function as an item has its own binder category, `Callable` (`Category::bound`), while `Category::of`, which assignment's grammar reads, keeps it `Value`. `Binder::push` works out each entity's role once, as it pushes it: `Category::before` resolves `Callable` against the role of the entity below, which is to its right, to `Function` where that entity is a subject or an absent `⍺`, and to `Value` otherwise. `Rule::get` and the strengths don't change, and recordings check `Callable` as a category of its own. A function-only operand slot converts such an array to `FunctionNode::Array` in `constant_operand`, and `Function::from_value` makes the same conversion where a call by text needs a function. Primitive/operator rows own names, operand kinds, ranks, pervasion, axis behaviour and reduction identities. Change those rows instead of duplicating metadata in consumers. A non-extending primitive above its natural ranks runs through Rank; pervasive Each/Outer/Rank paths reuse whole-array kernels. +The compiler binds a statement right to left by categories (`binder::Compiler`, `Rule::get`, `shifts`). Reductions emit instructions instead of calling functions; shared function nodes are immutable. An array that holds a function as an item has its own category, `Callable` (`Category::bound`). `push` works out each item's role once, as it pushes it: `Category::before` resolves `Callable` against the role of the item below, which is to its right, to `Function` where that item is a subject or an absent `⍺`, and to `Value` otherwise. `Rule::get` and the strengths don't change, and recordings check `Callable` as a category of its own. A function-only operand slot converts such an array to `FunctionNode::Array` in `constant_operand`, and `Function::from_value` makes the same conversion where a call by text needs a function. Primitive/operator rows own names, operand kinds, ranks, pervasion, axis behaviour and reduction identities. Change those rows instead of duplicating metadata in consumers. A non-extending primitive above its natural ranks runs through Rank; pervasive Each/Outer/Rank paths reuse whole-array kernels. Dot names rewrite to Pick and dot brackets to selection. Numeric subscripts derive selection on arrays and `⍠` on functions from the bound operand. Writable subscripts normalize to selection calls at assignment. `ⱼ`, `ₑ` or `ᵣ` between two arrays then rewrites to the primitives it stands for, as `xⱼy` to `x+y×0ⱼ1` (`syntax::joined_term`). Dot parentheses evaluate a namespace: `T.(expr)` and `T⍎text` bind each key on the first axis of `T` to its item or major cell as a local, resolve other names in the caller and keep assignments local. They share the scoped execution route and lexical lifetime checks. Between functions, dot remains inner product. @@ -191,173 +228,300 @@ Module 0 owns session names. Each `•load` runs in a new module, without the ca Dfns run statements in order and return the last statement's value. Final assignments are silent. An absent left argument propagates through the binder's absent-binding rules; default-left assignment runs only when needed. Guard handlers see bindings as they stood at failure; nothing rolls back. Wildcard `∞::` excludes interrupt, timeout and unsupported errors. Explicit named guards can catch them; catching an interrupt clears its flag. -The binder compiles a dfn statement on the statement's first evaluation (`eval/tree.rs`). Its recorder follows the binder's two stacks over registers and constants, and emits an instruction for each node that needs evaluating and for each reduction. A move between the stacks and the drop of an absent `⍺` compile to nothing, and a derive or a bind on constants compiles to a constant, so an operator bound to a constant operand, as in `⍤1`, derives once. Later evaluations run the instructions: they make the same calls in the same order, checking the category of each name, group and call result against the recorded one. Each checked instruction keeps a template of the binder's state at that point: what its two stacks hold, its cursor, the leaf it had just resolved or the result it had just pushed, and the assignment it was making. Where a category differs, the binder carries on from that state (`Binder::resumed`). Registers hold the binder's own entities (`binder::Entity`), in an inline array of 8 (`INLINE`) or on the heap for a statement that needs more. The recording keeps its constants as entities, and an instruction borrows them, so a call reads a constant's array or function without copying it (`array_of`, `function_of`). The binder evaluates a statement every time when the statement selects. The parse tree depends on the evaluator, because each `syntax::Statement` holds its own recording. A change to a reduction that the binder records (a drop, a strand, an attach, a call, a derive, a bound right operand or a train) must change the recorder and the instructions too. `strand`, `attached`, `split_call`, `derived`, `bound_right`, `train` and `tails` in `eval/binder.rs` serve both, and the binder tells the recorder its cursor and whether a call is its last reduction. The `_ => rc.fail()` arm in `Binder::reduce` stops recording any statement with a reduction that the instructions don't cover. The binder then evaluates that statement correctly every time. +Every statement compiles before it runs (`eval/tree.rs`). The compiler binds it from its text and the roles of the names it reads, and emits an instruction for each node that needs evaluating and each reduction (`Op`); `Recording::execute` runs them in one loop. `strand`, `attached`, `split_call`, `derived`, `bound_right`, `train` and `tails` in `eval/binder.rs` give the reductions' values, while compiling for constants and while running otherwise. The drop of an absent `⍺` compiles to nothing, and a derive or a bind on constants compiles to a constant, so an operator bound to a constant operand, as in `⍤1`, derives once. A call's result, a field read from it, a subscript's selection and a scope's result bind as values whatever they hold, so no call changes how its statement binds. A value of another category than its instruction compiled with takes the compiled one (`Entity::fit`, `refit`, `returned`). A call or derive result carries `call`: assignments store, and operators other than subscripts and superscripts take, the function its atom holds, and so does a statement's result (`called_as`). Registers hold the compiler's entities (`binder::Entity`), in an inline array of 8 (`INLINE`) or on the heap for a statement that needs more. The recording keeps its constants as entities, and an instruction borrows them, so a call reads a constant's array or function without copying it (`array_of`, `function_of`). Each statement holds two recordings (`Statement::recordings`, `tree::Slots`), so a dfn called with both valences keeps both, and a statement whose facts fit neither compiles and runs uncached. While capture is on, calls, strands, attaches and derived calls carry `Expression`s for dissection (`execute::`). + +A recording keeps each fact that its binding read: the role at a name's or a dot path's address (`tree::Fact`). A statement checks its facts as it starts (`Recording::holds`), except that a name's first read before anything in the statement acts checks its own role (`Mismatch::Again`, `binder::early`). A role that changed there puts back the bindings that the statement took (`Recording::again`) and raises `ErrorKind::Again`, which only `Session::statement_step` and `Session::bound` catch, to compile the statement again. A read of a name that an assignment earlier in the statement binds takes the assigned category (`Mismatch::Fit`). Any other read whose role changed while the statement ran fails (`role_changed`), after a selective write in the statement or a write inside `⍎`. A call's write that would change the role of a name outside its frame fails at the write (`Session::outer_role`). An unbound name binds as an array (`binder::UNBOUND`). An own-frame slot's role check reads the slot (`Bindings::own_role`), and module entries' facts are checked through an epoch that an entry's role change, any unslotted write, `swap` and `replace` renew (`Bindings::roles`). `∇` and `⍢` record no facts (`Name::fixed`). -Each group or run in the statement is a level, whose instructions sit inline between an `Enter` and an `Exit`, so one loop runs the statement. The recorder writes a level's instructions straight into the statement's as the binder evaluates the level, keeping a mirror of the level's binder (`tree::Walk`) while it runs. A failed check resumes the binder of the level that holds the instruction. Where the group's value keeps its recorded category, the instructions after its `Exit` carry on. Otherwise the level that holds it resumes, and so on out to the statement. Dot access, subscripts, joiners and runs that start with an operator rewrite a statement before the binder walks it (`Session::joined`, `Session::members`), and the recording keeps the rewritten nodes. Whether a rewrite applies, and whether a run starts with an operator or ends in a dyadic one, depend on what names hold, which one reader reads without evaluating anything (`Session::static_category`): a name by its binding, a dot path by its field, and any other group by the operand grammar of assignment targets (`assignment_operand`). While a statement records, the reader keeps a fact for each name or dot path it read, with the name's address and the category it found (`tree::Fact`). A recording checks a level's facts again when the level starts, and the binder evaluates the statement or group from its own nodes where one differs. A run that ends in a dyadic operator is a level whose binder starts with the run's right operand on its stack, read where the level that holds it keeps it. +Each group or run in the statement is a level, whose instructions sit inline between an `Enter` and an `Exit`. The compiler groups a level's dot reads, subscripts, joiners and runs that start with an operator as it starts the level (`Compiler::members`, `Compiler::joined`), reading their categories through itself, so the groups match how it compiles. Each instruction holds the nodes it reads, so no group outlives the compile. A run that ends in a dyadic operator is a level whose compile starts with the run's right operand on its stack. A group, run or bracket literal that `Session::resolve` evaluates compiles through `Compiler::compile_nested`, which checks that a run reduces to one value (`Compiler::one_value`). -A group or run whose leaves are all literals, primitives, operator glyphs or such groups, and which makes no call, records as the array or function it gave, so a train like `(+/÷≢)` builds once. The recorder drops such a group's instructions by truncating the statement's back to where the group started (`Recorder::truncate`). A run that ends in a dyadic operator, whose right operand is constant, records as a constant too. A group whose value is a train with a computed tine exits with the train's tines (`Term::Tines`). A call of that group calls the tines in place (`Op::CallTines`, `operators::train_call`). The call builds the train and calls it instead in three cases: while a transformation or `•explain` runs, where two array tines are side by side, and where the last tine orders its argument (`idioms::orders`). +A group or run whose leaves are all literals, primitives, operator glyphs or such groups, and which makes no call, compiles to the array or function it gives, so a train like `(+/÷≢)` builds once. The compiler drops such a group's instructions by truncating back to where the group started (`Compiler::truncate`). A run that ends in a dyadic operator, whose right operand is constant, compiles to a constant too. A group whose last step builds its value as a train exits with the train's tines (`Term::Tines`), and a call of that group calls the tines in place (`Op::CallTines`, `operators::train_call`). The call builds the train and calls it instead while a transformation or `•explain` runs, where two array tines are side by side, and where the rule table marks the train's functions (`operators::calls_tines`, `rules::marks`). -An assignment compiles to one instruction, which runs `Session::assigned`, the step after the target is found that the binder runs too: `Session::assign`, the one home of every target form, or a store of a plain name straight into the frame's slot for it, which `Bindings::here` found when the statement compiled. What a target's names hold decides where a target of more than one node starts (`Session::assignment_start`), so the instruction checks that start again, and the binder carries on with the assignment in progress where it moved. A default argument, an error guard's kinds and a dfn's last statement compile as other statements do. +An assignment compiles to one instruction (`Op::Assign`), which runs `Session::assigned`: a store of a plain name straight into the frame's slot that `Bindings::here` found when the statement compiled, or `Session::assign`, the one home of every target form. The compiler finds where a target of more than one node starts (`Compiler::target`), and compiles a modified assignment's function before the instruction, as any function; the instruction reads its register. The instruction holds the target compiled (`tree::Target`, `Compiler::assigned_target`), so running it compiles nothing and reads no name's role: a name, `⎕`, brackets of names, a function's rule, or a selection, whose dot reads and subscripts become selection calls first. A selection compiles into layers (`tree::Layer`). Each splits at a selecting primitive, with its index compiled, for the direct route and the extension of missing keys, and holds the code before its last node, compiled with a seed, for the walk. A run evaluates each index once (`Split::slot`). A default argument, an error guard's kinds and a dfn's last statement compile as other statements do. -Each item of a bracket literal is a level whose value goes into the literal at once, as `Session::item_result` converts it, and `Session::bracket` builds the literal for `resolve` and for the compiled `Array` instruction alike. A literal with a computed key that isn't constant stays with `resolve`. Each statement keeps the recording of its first evaluation (`Statement::recording`). A later call whose `⍺` is absent where that evaluation's held an argument, or the reverse, resumes the binder where the statement reads `⍺`. +Each item of a bracket literal, and each key that isn't constant, is a level whose value goes into the literal at once (`session::listed`). The level that finishes a keyed item makes its entries (`tree::Keyed`), and the `Array` instruction assembles the literal (`tree::assembled`). `resolve` compiles a bracket literal that a dot scope holds, as it compiles a group (`binder::nested`). Tail calls reuse frames while preserving lexical dependencies. Handlers disable tail reuse. Evaluation depth is capped at 20,000 natively and at 244 in the browser, whose stack runs out first. Each call of a function other than a primitive counts toward it, and so do each statement of a dfn other than its last, each group, each run that ends in a dyadic operator and each bracket item, compiled or not. A compiled statement or level counts with `Session::descend` alone, without the interrupt check and stack growth of `Session::nested`. Calls of primitives other than `⍎` don't count, because a primitive never calls back into BPL. They skip the depth, capture and stack bookkeeping, and on numbers a form that applies a kernel to each pair of items runs that kernel at once (`Primitive::scalar_call`). A separate 20,000-frame limit bounds retained lexical frames. Late-bound name resolution stops at 128 levels. These are execution limits, not a sandbox against arbitrarily deep source, arrays or function graphs. ## Values, layouts and assignment -`Value` holds a number, character, function, operator or shared array. Compact storage holds a rustymath `buffer::Buffer` of Booleans, narrow integers, width-specific floats or complex numbers, or characters, all of one dtype, and `Items` borrows it as a `buffer::View`. BPL's promotion rules for those dtypes stay in basedpl, as `Promotion` in `array/storage.rs`. A rustymath buffer keeps the dtype it's given, and basedpl picks an ordinary integer array's width (`array::ordinary_width`): the narrowest that holds its values under dynamic kinds, and the `int` width under static kinds, where an array's storage follows its kind. Mixed storage holds `Value`s and memoizes its prototype, its innermost lexical dependency and whether any item is a function. Other properties are computed when needed. An array holds its layout and its storage in one allocation. Its `Shape` holds ranks 0 to 2 inline, so a small array allocates no shape. Constructors take `impl Into`: pass a slice, or an array such as `[n]`, rather than building a `Vec`. A shape, key or name change on a shared array makes a view of the base array's storage and copies no items. A part of compact storage, such as one of Rank's cells or a partition (`Value::part`), is a view of a range of its base's items. A view keeps its whole base alive, as a NumPy view does. A part of mixed storage is a copy, because the memo answers for all of a mixed array's items. A large result of a fusible float call holds a pending call in place of its storage, and computes the storage on the first read (`fusion.rs`). `Value::view` and `imported_buffer` are the public width-preserving frontend boundary. They give and take rustymath's `View` and `Buffer`, which basedpl's API re-exports as `basedpl::buffer`. - -Atoms, rank-0 arrays and singleton vectors are distinct. Function nodes retain handles, not lexical frames or Python objects. Functions match by their parts; dfns and generators match by identity. Operators can be values and can cross the native Python boundary. Values and function nodes are `Send + Sync`. - -Numeric storage fills with zero and characters with blank. Mixed nonempty arrays derive their prototype from their first item on demand; empty arrays require one explicitly. Builders accept a value or a lazy prototype function and evaluate the function only for an empty result. Preserve function handles inside prototypes. - -`Layout` wraps rustymath's `layout::Layout`, which holds the dimensions, position keys and axis names, and adds the renderer. Methods that change only keys or names keep the renderer, as `select` does. Methods that build a layout from axes drop it, and pairing two layouts by broadcasting keeps the renderer they agree on (`Layout::paired`). Rustymath's `agreement` does the pairing: it unites keys and broadcasts through index maps. Missing positions read each source's prototype. Agreement pairs equal axis names first. It pairs remaining axes positionally unless both have different names. Result assembly drops colliding axis names; public name attachment rejects duplicates. Contracted axes require equal key sets and retain the first order. Nonempty key lists with no named positions become unkeyed; empty record keys survive. +`Value`: number, character, function, operator or shared array. -`Gather` owns copying and widening. `Steps`, from rustymath's `gather`, describes strides, ranges/fills, rotations and offset tables. `Gather::walk` copies compact storage through `rustymath::gather::walk`. Structural operations should use `remap`/gathering rather than reconstructing `Value`s per item. Equal-shape cells copy in blocks. `Items` reads counts, positions and axes from compact storage without boxing every item. Shape, tally, positions and predicates have dedicated integer/Boolean builders. +- Compact storage: a rustymath `buffer::Buffer` of one dtype (Booleans, narrow integers, width-specific floats or complex numbers, characters); `Items` borrows it as a `buffer::View`. Promotion rules for those dtypes stay in basedpl (`Promotion`, `array/storage.rs`); a rustymath buffer keeps the dtype it's given. Ordinary integer array width (`array::ordinary_width`): narrowest holding its values under dynamic kinds; the `int` width under static kinds, where storage follows kind. +- Mixed storage: `Value`s; memoizes prototype, innermost lexical dependency, whether any item is a function; other properties computed on demand. +- Segmented storage (`Segments`): two or more arrays of one rank and kind, with no keys or names on their axes, held flat as one leaf array, with each item's offset and shape, as DuckDB's list vectors and Arrow's ListView hold lists; an item reads as a view of its range of the leaf, and a view of nested items takes its own first item's prototype (`ArrayData::nested_prototype`). `Value::gathered` builds it with no copy where every item is a view of one stored array (`Segments::of`: Rank's `⊂` cells, a partition's parts, a selection of segmented items). A producer whose items are made together builds its leaf whole (`Segments::new`), as `keyed::text_column` does for `•csv` and `•json` texts, one range for each distinct text. Items computed apart stay mixed. Each's batched try reads items of one layout and kind as one array (`Items::stacked`: a view of a segmented leaf where they lie in order, else a copy). Search, classification and grade compare exact cells, a segmented array's as slices of its leaf, through rustymath's `search::Exact`. A write turns it mixed first (`Storage::values_mut`). +- Layout and storage share one allocation. `Shape` holds ranks 0-2 inline → small arrays allocate no shape. Constructors take `impl Into`: pass a slice or an array (`[n]`), not a built `Vec`. +- Shape, key or name change on a shared array → a view of the base's storage, no copy. Part of compact storage (a Rank cell, a partition: `Value::part`) → a view of a range of the base's items; a view keeps its whole base alive (as NumPy's). Part of mixed storage → a copy (the memo covers all items). +- A large result of a fusible float call holds a pending call in place of its storage, computed on first read (`fusion.rs`). +- `Value::view`, `imported_buffer`: the public width-preserving frontend boundary; they give and take rustymath's `View` and `Buffer`, re-exported as `basedpl::buffer`. -A call chooses its fill through `⍠`'s options record or a window specification's `fill` key (`WindowSpec`, in `primitive/structural.rs`). `Primitive::fill_for` validates one scalar item of the data's type (`Value::fill_of`); nested data takes any BPL value. Native fills travel with rustymath's mappings through pending copies, kernels and traces. `copied_fill` consumes fills of mixed data eagerly. Keyed pervasive calls attach native fills to their agreements without first copying the operands. Values retain the chosen fill; tangents and label ids read their own zero fill. Maps needing two different fills keep their copies separate. +Atoms, rank-0 arrays and singleton vectors are distinct. Function nodes hold handles, not lexical frames or Python objects. Functions match by parts; dfns and generators by identity. Operators can be values and cross the native Python boundary. Values and function nodes: `Send + Sync`. -Under with a function and selective assignment find their targets through labels. Each begins a label level (`transform::Kind::Label`) and runs the selector, or evaluates the target, on the root wrapped as a `selection::Labelled` value. A primitive whose row selects carries the labels through its own code. Planned forms and `⌷` compose a rustymath `Mapping` relative to the root, so a selection reads only the items it selects. Ordering forms select through `Primitive::taken`. Any other call strips the labels and runs on the values. `Labels::read` takes the labels of the selector's result. Labels that are still a mapping with no fill write back through that mapping, as a walk over the root (`Targets::Walk`). `Labels::replacements` turns any other labels into offsets, or into paths where the root has nested items. A strand of labelled items joins into one labelled array (`Carrier::joins_at_once`). An item that the selector makes rather than selects has the `FOREIGN` label. Writing to it is a DOMAIN ERROR. A result without labels at the level sends Under to the selector's inverse. A batched try refuses while a label level runs (`Levels::admits_batch`). Function-Under applies its selector to a left argument as a plain value. Positions-Under passes the left argument whole. Keyed values align to selected keys, and nested paths retain empty-cell structure. +Fill: numeric zero, character blank. Mixed nonempty arrays derive their prototype from the first item on demand; empty arrays need one explicitly. Builders take a value or a lazy prototype function, evaluated only for an empty result. Preserve function handles inside prototypes. -Selective assignment removes the binding before writing. `Arc::make_mut` copies only when another holder needs the old array. Repeated targets must read values written by earlier occurrences. `pervasive::scatter` combines each target of `+ - × ÷ ⌈ ⌊` into compact numbers in place, in turn, at the kind that the call's plan computes in (`plan::pervasive_on`), through rustymath's `gather::Writes`, whose `combine` and `try_combine` read a selection's walk a run of offsets at a time, without listing it. Other pervasive modifiers group repeated targets into rounds of distinct positions. A target at a fill reads the fill, and its write is dropped. Append updates use `Value::append` when layouts permit it; rank/key/name changes fall back to ordinary catenation. Modified destructuring calls modifiers left to right; ordinary destructuring assigns right to left. An assignment's RHS runs once. +`Layout`: rustymath's `layout::Layout` (dimensions, position keys, axis names) plus the renderer. Methods changing only keys or names keep the renderer (as `select` does); methods building a layout from axes drop it; broadcasting two layouts keeps the renderer they agree on (`Layout::paired`). Rustymath's `agreement` pairs: unites keys, broadcasts through index maps; missing positions read each source's prototype. Equal axis names pair first; remaining axes pair positionally unless both have different names. Result assembly drops colliding axis names; public name attachment rejects duplicates. Contracted axes need equal key sets, keep the first order. Nonempty key lists with no named positions become unkeyed; empty record keys survive. -Each, Rank and outer products call their operand through `each_pair` in `eval/operators.rs`. It first tries one batched call (`batch::tried`), then calls the operand once for each cell and assembles the results through shared layout and prototype rules. A 0-cell preserves an atom or encloses an array item. For custom operands, empty Each, Rank and outer product calls substitute prototypes for empty arguments and use prototype mode. Explicit effects remain observable when a call runs. Catchable prototype-call failures produce prototype zero, but interrupt, timeout and unsupported errors propagate. Restore prototype mode on success and error. A stencil runs as Rank over `↕`'s windows, and reads its specification through `↕`'s `WindowSpec` (`primitive/structural.rs`). +`Gather` owns copying and widening. `Steps` (rustymath's `gather`): strides, ranges and fills, rotations, offset tables. `Gather::walk` copies compact storage via `rustymath::gather::walk`. Structural operations should use `remap`/gathering, not per-item `Value` reconstruction. Equal-shape cells copy in blocks. `Items` reads counts, positions and axes from compact storage without boxing items. Shape, tally, positions, predicates: dedicated integer and Boolean builders. -Rank tries `whole` before `each_pair`. `whole` calls the operand once where neither argument has a frame. It then tries `decomposed`, for `•decompose⍤2` on a stack of matrices, which factors the whole stack through rustymath's `linalg::Stacked`, as `•decompose` factors one matrix, and `whole_cells` for a primitive or a product. `whole_cells` holds Rank's routes for whole arguments, which batched calls share (`batch::whole_call`). In order, they are `primitive::stacked`, which runs `⌹` and `-.×` on stacks of inexact matrices through rustymath's `linalg::Stacked`, as rustymath's Python API runs them, and every `f.g` that rustymath has a product kernel for, on compact stacks of any item kind, through `linalg::Product` (an exact product that doesn't fit 64 bits keeps the call for each cell, which computes exactly), one padded call of a pervasive primitive (`padded_call`), `primitive_cells` for a primitive with one argument or with a left argument that every cell shares, and `paired_cells` for a primitive whose arguments' frames match or whose right argument every cell shares. An argument with no axes besides its cells' is the same in every cell. After `whole_cells`, `whole` tries `pervasive_cells`, for a pervasive primitive under nested Rank, and then, for a monadic operand, `cell_rule`, which also takes folds, atops and a bound left argument. `primitive_cells` runs a primitive on the whole argument along the cells' axes. A form that `extends` runs whole at a cell rank at least its own. Other forms follow the `cells` fact (`CellRule`) in their row of `Primitive::info()`. With it, `⊖ , ↑ ↓ ⌷ #` and rotate-first call their axis forms, `⍉` sends each cell's axes after the frame's, `≢` and `⍴` give constants, a reshape that keeps a cell's items in order is a view, and `⊂` encloses each cell of rank 1 or more through its axis form, whose cells are views of the argument's items. Any other form with a left argument that every cell shares takes `paired_cells`. `paired_cells` joins each pair of cells for `,` and `⍪`, and calls a form that extends once where both cells are at least its ranks. `padded_call` and `paired_cells` leave an argument with a named frame axis to the path through cells, which pairs frames by name (`named_frame`). Names on cell axes pair in their whole calls as calls on cells pair them. It first repeats an argument that every cell shares over the frame (`agreement::spread`). A pervasive primitive folds or scans a simple array along the cell's axis (`folded_cells`). `f/∘,` folds all the cell axes at once. An atop applies its inner function's rule, then Rank for its outer function. Each rule applies only where Rank's mix of the cells' results gives what the whole-array call gives. Other nested results, and folds by other functions, keep the call for each cell. +Fill choice: `⍠`'s options record or a window specification's `fill` key (`WindowSpec`, `primitive/structural.rs`). `Primitive::fill_for` validates one scalar item of the data's type (`Value::fill_of`); nested data takes any BPL value. Native fills travel with rustymath's mappings through pending copies, kernels and traces. `copied_fill` consumes fills of mixed data eagerly. Keyed pervasive calls attach native fills to their agreements without copying operands first. Values keep the chosen fill; tangents and label ids read their own zero fill. Maps needing two different fills keep separate copies. -Axis nodes select/move axes and reuse Rank when the function has no direct axis meaning. Vector-axis reduction uses the general composed-ravel rule, preserving axis order and the whole seed. Single-axis reductions retain direct kernels. Layout-only changes share storage. Structural results retain renderers where their rank permits; scans and in-place changes copy complete layouts. Match ignores renderers. +Writes: Under with a function, and selective assignment, run the selector on the root while every call builds a node, then walk its result back to the root's positions (`selection::selecting`). A compact root enters as one input node. Any other root enters through leaves (`selection::Leafed`): each compact array or atom, at any depth, as a view of the leaf node for its type of item; each nested array rebuilt from its parts; each host item (exact number, function, operator) copied to an address of its own (`Value::readdressed`). -Inverse nodes propagate fixed-argument sides through composition. `f⁻¹←g` and `f∂←g` replace the binding with a `Ruled` node, which holds the function with its hand-written inverse and derivative. Calling it calls the function. `⁻¹` and `∂` use the rules, and 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/axis validation. Agenda calls its selector once, then passes the original arguments to the selected branch. Computed operands for Rank, Axis, Stencil and Agenda use shared operand evaluation. Python constructs these nodes directly, not by generating source. +- The walk follows copies, gathers and joins back to the root (`root_places`). A result that reads the root whole in order → a walk over it (`Targets::Walk`); otherwise offsets, or paths where the root has nested items. A part found by its address, or by its place and shape in its leaf, is taken whole: its write replaces it at any length. A selecting form whose result is its argument itself gives a copy node (`Primitive::call_axis`), which selects every position; a form with `Facts::passes` gives the argument itself, taken whole. +- A root with duals on it walks through each value's primal (`undualed`), and `transform::written` writes each part. A regular batch of the running batched run: the selector runs once, on a batch over its whole (`framed`). Any other batch stops its run. A result that no root node holds → Under uses the selector's inverse; in selective assignment it's a DOMAIN ERROR, as is an element from elsewhere. +- Function-Under applies its selector to a left argument as a plain value; positions-Under passes the left argument whole. Keyed values align to selected keys; nested paths keep empty-cell structure. -The form rewriter (`eval/rewrite.rs`) is the one home of fast combinations. Each rule is a row of BPL text, a pattern over `⍵` and `⍺`, beside the hidden `Special` function that computes it, as `⍟1+⍵` beside `Log1p`. `rules` adds the selections, one for each order and pick. The rewriter reads a function and an expression as one tree of calls (`Form`), and replaces each subtree that a rule matches with a call of the rule's function. `Function::new` rewrites a function's monadic and dyadic forms once and keeps the results (`rewritten`). `dispatch` calls them in the function's place, while 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. A rule's functions must be glyphs and its constants literals: a name that holds `1` or `⍟` isn't recognised. `combined` runs the structural replacements (selections, ranks, Key's counts and folds, the determinant and the permanent) before any running level. Where an algorithm doesn't suit the arguments, or a running level carries one, `combined` calls the functions that the combination replaces, which the level then sees. `calls_tines` builds a train whose tines `may_combine` accepts. `Function::new` then rewrites it. Each combination has a row under "Fast combinations" on its glyph pages. `tests/explain.rs` checks through `•explain` that every documented spelling of a structural combination takes it. Reference cases in `core.bpl` check that each gives what its functions give in turn. The coverage matrix runs each rule whose function maps its argument (`mapping_patterns`). It respells any form's own case that 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, such as Rank's for pervasive operands, belongs with the operator. +Selective assignment removes the binding before writing; `Arc::make_mut` copies only when another holder needs the old array. Repeated targets must read values written by earlier occurrences. `pervasive::scatter` combines each target of `+ - × ÷ ⌈ ⌊` into compact numbers in place, in turn, at the kind the call's plan computes in (`plan::pervasive_on`), via rustymath's `gather::Writes` (`combine`, `try_combine` read a selection's walk a run of offsets at a time, without listing it). Other pervasive modifiers group repeated targets into rounds of distinct positions. A target at a fill reads the fill; its write is dropped. Append updates: `Value::append` where layouts permit; rank, key or name changes → ordinary catenation. Modified destructuring calls modifiers left to right; ordinary destructuring assigns right to left. An assignment's RHS runs once. -## Numbers and backend ownership - -`number.rs` owns scalar kinds, promotion and exactness, and the suffix table that spells numbers (`e`/`ₑ`, `j`/`ⱼ`, `r`/`ᵣ`, `ₓ` and the width marks), which the lexer, `Number::parse` and display read. Integers and BigRational share an exact domain. Overflowing scalar integer arithmetic widens to exact rationals; whole rational results narrow back when possible. Booleans are a distinct kind. Arithmetic reads them as 0/1; logical operations preserve them. Match/search/grade compare their numeric value, while reference checks also distinguish representation and exactness. +Each, Rank and outer products call their operand via `each_pair` (`eval/operators.rs`): one batched call first (`batch::tried`), then once per cell, assembling results by shared layout and prototype rules. A 0-cell keeps an atom or encloses an array item. Custom operands: empty Each, Rank and outer product calls substitute prototypes for empty arguments, in prototype mode; explicit effects stay observable when a call runs; catchable prototype-call failures give prototype zero; interrupt, timeout and unsupported errors propagate; restore prototype mode on success and error. Stencil: Rank over `↕`'s windows, reading its specification through `↕`'s `WindowSpec` (`primitive/structural.rs`). -Joining compact arrays widens Boolean -> integer -> float -> complex, and float widths join at the wider width. Item-by-item construction preserves exact/approximate kinds and stays mixed when needed; only approximate real/complex items widen together. A write into an existing array follows the items rule (`Widening::Items`), as a literal of the same items would: exact and approximate numbers stay apart, and so do Booleans and other numbers. A rearrangement or combination of mixed storage finishes in `Gather::finish`, which holds the items compact where one dtype holds them under the items rule, as a literal of the same items would be. Its scan compares each item's variant with the first item's, so a copy that stays mixed pays little. An in-place write into mixed storage keeps it mixed, because a scan on every write would make a loop of writes quadratic. Fresh computations choose storage from their result path. Results that an operation computes one cell at a time, as Each, Rank, Key, power histories, and the polynomial and number-theory functions do, assemble as items, through `Value::assemble`. Mix and catenation join arrays. Keep this distinction between whole-array joining and element assembly. +Rank tries `whole` before `each_pair`. `whole`, in order: -`element::Converts` is the one rule that converts a compact item to another numeric type. Size marks, `•float`, and programs' `Convert` steps in replays and block stages all use it. A float target reads the item as rustymath's `Source` reads it. An integer target takes a Boolean as 0 or 1 and wraps an integer to its width. It takes a float's floor within the tolerance of the float's width, then saturates at the target's range. `Value::converted` converts a plain value. `derivative::converted` converts a carried value through its carrier (`Carrier::cast`). On a pending array it adds the conversion as one more pending call (`fusion::convert`). +1. one operand call where neither argument has a frame; +2. `decomposed`: `•decompose⍤2` on a stack of matrices factors the whole stack via rustymath's `linalg::Stacked`, as `•decompose` factors one; +3. `whole_cells`, Rank's whole-argument routes, shared with batched calls (`batch::whole_call`), in order: `primitive::stacked` (`⌹`, `-.×` on stacks of inexact matrices via `linalg::Stacked`, as rustymath's Python API runs them); every `f.g` with a rustymath product kernel, on compact stacks of any item kind, via `linalg::Product` (an exact product not fitting 64 bits keeps per-cell calls, which compute exactly); one padded call of a pervasive primitive (`padded_call`); `primitive_cells` (a primitive with one argument, or with a left argument every cell shares); `paired_cells` (matching frames, or a right argument every cell shares). An argument with no axes besides its cells' is the same in every cell; +4. `pervasive_cells`: a pervasive primitive under nested Rank; +5. monadic operand: `cell_rule`, which also takes folds, atops and a bound left argument. -Bare numbers, empty literals and NaN use the parsed float width. Top-level `evaluate_code` first validates the whole syntax, runs leading literal-record `•prefs` statements, then parses the remainder at the new width. A syntax error prevents header execution. The default width also applies to imported numbers, random data and float results from exact operands. Time/date/metadata values use f64. Formatting marks non-default widths; serialization marks every float for width-independent readback. +- `primitive_cells` runs a primitive on the whole argument along the cells' axes. A form that `extends` runs whole at a cell rank at least its own. Other forms follow the `cells` fact (`CellRule`) in their `Primitive::info()` row: `⊖ , ↑ ↓ ⌷ #` and rotate-first call their axis forms; `⍉` sends each cell's axes after the frame's; `≢`, `⍴` give constants; a reshape keeping a cell's items in order is a view; `⊂` encloses each cell of rank 1 or more via its axis form, whose cells are views of the argument's items. Any other form with a left argument every cell shares takes `paired_cells`. +- `paired_cells`: joins each pair of cells for `,` and `⍪`; calls a form that extends once where both cells are at least its ranks; first repeats an argument every cell shares over the frame (`agreement::spread`). +- `padded_call`, `paired_cells` leave an argument with a named frame axis to the per-cell path, which pairs frames by name (`named_frame`). Names on cell axes pair in whole calls as per-cell calls pair them. +- A pervasive primitive folds or scans a simple array along the cell's axis (`folded_cells`); `f/∘,` folds all cell axes at once. An atop applies its inner function's rule, then Rank for its outer function. +- Each rule applies only where Rank's mix of the cell results equals the whole-array call's result. Other nested results, and folds by other functions, keep per-cell calls. -Float operations compute at the wider operand width. Exact operands round to the width an operation computes at. f16 operations run on f32 copies. `Numeric::halving` decides when: for a call with an f16 operand, or any call while the default width is 16 bits. `pervasive::planned_halves` applies it to whole operations and `Number::halves` to scalar arithmetic and comparisons. `fusion::defer` declines those calls. Exact inputs convert once, to f32, and float outputs round back to f16. The scalar `Number` path rounds each f16 step. Complex parts have f32/f64 widths; f16 becoming complex uses f32 parts. Scalar Complex32 arithmetic computes at Complex64 and rounds, while kernels can compute at f32. These paths need not agree in their last bits. +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. -Floats use IEEE NaN, infinity and signed zero. Comparison treats NaN as unequal; match/search treat it as one value, and grade puts it last. Exact arithmetic involving division by zero can produce those float values. Floor/ceiling retain an exact operand beside infinity. Beside NaN the language leaves their result open: NaN or the other argument, varying by platform. Their folds, scans, windows, products and repeated writes give NaN or the maximum or minimum of the items that aren't NaN, never another number, by the rule in rustymath's `Folds` doc. A loop that folds by them either passes its running result as the right argument, which `rustymath::reduce::maximum` keeps beside NaN, or folds through rustymath's `Max` and `Min`, as `Dyad::extremum` does. Monadic floor/ceiling/sign turn whole finite results into exact integers, including big integers; nonfinite results remain floats. Use `Number::float(value, width)` and explicit-width array construction outside `number.rs` instead of implicit f64 conversion. +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. -Under `•prefs ["kinds":"static"]`, a result's kind follows from its arguments' kinds alone, as `meta/kinds.md` describes. `Numeric` carries `kinds` and `int`. Until the `float` pref sets a width, `Numeric::set` gives new floats the width the kinds give, 32 bits under static kinds and 64 under dynamic kinds (`Kinds::float_width`). Whether the pref has set one lives in `settings::float_set`, outside `Numeric`: every call builds and copies a `Numeric`, and a Boolean field there cost about 14 instructions on each primitive call and 34 on each dfn call. A pervasive call on arrays plans before it computes. `plan::pervasive` builds a `plan::Pervasive` from the operands' kinds (`plan::Kind`: type, fixed width and weakness) and the settings, without reading items. It holds the settings with the fixed width that the operands give (`Numeric::for_kinds`), whether 16-bit floats compute in 32 bits, the kind that the call computes in (`computes_in`, which applies rustymath's `DType::common` after `Numeric::kind`), its kernel table (`computes`) and, under static kinds, every result's kind (`Pervasive::result`). Eager dispatch, fusion's deferral (`Pervasive::fuses`) and typed programs read the same plan. Products and folds plan the same way. `plan::Product` holds `g`'s pervasive plan and the settings under which `f` folds its results (`Product::folds`). `plan::Fold` holds a fold's or scan's settings and its operands' kinds. Eager products, folds and scans read them, as typed programs do. Comparisons, `⌈ ⌊` and bitwise functions compute at their arguments' storage width instead, where their results are the same at every width (`pervasive::first_kind`). A comparison between integers and a whole float that stands for an integer computes on integers, through `Pervasive::computes`, as does a residue by such a float or a residue scan from one, whose results then convert to floats (`plan::whole_float`). Literals, values computed only from literals, and Python's scalar arguments are weak (`Number::is_weak`). Before a call, `Primitive::weakened` converts a weak number beside a strong one to the width that rustymath's `DType::common_weak` gives it, or to a fixed integer's width where it fits. The loops keep the failed results that `keeps` names: floats' NaNs, and the wrapped integers of objects that report `WRAPS`. The kernel tables choose float `⌊ ⌈ ×` and wrapping negation from `run.numeric()`. `Stored::build_at` keeps complex results complex. `Primitive::number` gives every pervasive result on numbers, as well as each step of the fold of numbers. Under static kinds, `statically` converts it to the kind that the call's plan gives. A static call on ordinary integers that an integer kernel takes runs that kernel at the `int` width instead, as a call on fixed integers runs it at theirs (`Primitive::integer_kernel`). Under dynamic kinds, a call on two numbers stays outside plans, because the type of its result depends on their values. `Number::wrapped` and `Value::wrapped` wrap an exact integer result to the integer width, the fixed width that `Numeric::for_kinds` finds among a call's operands, or the `int` width under static kinds. Under static kinds a strong ordinary integer is a fixed integer of the width it computes at, the `int` width joined with its storage width (`plan::Kind::stored`). Every static call on integer arrays then computes under a fixed width. Weak integers have no fixed width. NEP 50 decides theirs, as before. Display marks a fixed integer of the `int` width with plain `ₓ` (`Mark::integer`). A fixed kernel that fails where a result doesn't fit, as lcm and binomial coefficients can, gives the exact result wrapped (`Primitive::fixed_number`). `statically` uses them, and so do the functions that compute exactly, such as `⊥`, `⌻` and the determinant. Where integers wrap, a function whose results are approximate, such as `⌹` or polar parts, computes integers as floats of the width that their division gives (`plan::Kind::approximate`). `∂` converts the integers that wrap in its argument to those floats before it runs its function (`derivative::differentiate`). `Number::parse` keeps complex literals and checks integer literals against the `int` width. +Form rewriter (`eval/rewrite.rs`): the one home of fast combinations. -Settings live once for the whole process, in `settings.rs`, and are read from there, not passed through functions, except where a read is slow and on a hot path. Each setting that calls and scalar operations read is an atomic: the numeric settings, the hanzi aliases, the `batch` pref, the verifier flag, the fusion report flag, fusion's sizes, which `settings::set_sizes` sets, and the compile setting. The display settings sit behind one lock. `•prefs` writes them, the lexer writes a source's leading `•prefs` header as it reads it, and `settings::reset` restores the defaults, as `]clear` and the test harnesses do. A module that `•load` runs changes the settings for the whole process. Nothing restores them when it returns. Rustymath holds the thread count, the one setting that it reads itself, and `•prefs` writes it there. Each call reads the store once where it starts. Pervasive calls, folds and products read it as they plan (`Primitive::pervasive_mapped`). A dfn call reads it as it looks for its program (`program::call`). So do `Domain::of` and the lexer. Values derived from the settings within a call stay explicit arguments. A call's plan holds the settings it computes under. A pending array computes under the plans that its calls made, whatever settings are in force when it computes. A pending conversion holds the settings that give its floor's tolerance (`Step::Convert`'s `numeric`). A dfn's programs are found by the settings they were traced under (`program::Key`). Under other settings, the dfn traces a new program. The loop structs keep the settings because their closures run on rustymath's worker threads. `Number`'s tolerance methods and `Primitive::number` take them because they run once per item. The parser's leaves take a width marker's width, and `matches_within` takes `⍣∞`'s convergence tolerance. Imports build complex numbers with `Number::complex_in_session`: Python's `_Session.array` and the JSON protocol's `request`. JavaScript values hold no complex numbers. +- 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`). +- 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. -`Numeric::mathish_at(width)` owns Mathish eligibility: the preference must be on, kinds dynamic and the calculation ordinary exact, f64 or complex128. Fixed-integer operations are excluded even when computing at f64. Kernels select their numerical objects before the loop. Mathish comparison tolerance is `1E-14`. f32/f16 compare exactly. Fixed-point convergence uses four ulps at those widths and `1E-14` at f64, independently of Mathish. Index/count/Boolean/character-offset conversion uses `near_integer` with the fixed default tolerance. The numerical objects come from rustymath. Use independent inside/outside-tolerance tests with exact/mixed controls rather than language comparison as its own oracle. +## Numbers and backend ownership -`search.rs` selects compact exact or tolerant backends. Classification of 1,024 items or more first groups items that share one array, before it reads any (`search::shared_classes`). Imports make repeated texts share: `keyed::Texts` makes each distinct text once, for each `•csv` column, each `•json` document and each Python conversion, up to 65,536 distinct texts. Exact integer/Boolean/character storage uses keys; float searches use their width. Integer haystacks accept the whole-number shortcut only for exactly whole float needles. Tolerant indexing validates whole-key matches unless the haystack is exactly whole and falls back to buckets on mismatch. Classification intentionally chains tolerance through sorted runs, so it need not match every member to one representative. Other cells use exact hashes or first-number buckets with `Value::matches` confirmation, falling back to pairwise comparison where indexing is unsuitable. Grade is stable. Position buffers use u32 and become integer storage without another pass. `⍷` and monadic Key take each class's positions from `search::class_positions`, which keeps a class's own buffer for 32-bit positions and copies narrower ones once from the shared buffer. Dyadic Key selects rows through `Groups::each` slices. Key's group-fold idiom folds through `search::numbered`'s class numbers. Where `fold_groups` declines, Key builds the positions from those numbers. Key with the operand `≢⊢`, which `≢∘⊢` also builds, counts its classes through `search::class_counts` and builds no positions. Under Rank and in batched maps, `⍳`, `∊` and `⍸` whose haystack differs from cell to cell search each cell's own haystack in one call on the whole arguments (`primitive::searched_cells`). +`number.rs` owns scalar kinds, promotion, exactness, and the suffix table spelling numbers (`e`/`ₑ`, `j`/`ⱼ`, `r`/`ᵣ`, `ₓ`, width marks), read by the lexer, `Number::parse` and display. Integers and BigRational share an exact domain; overflowing scalar integer arithmetic widens to exact rationals; whole rational results narrow back when possible. Booleans: a distinct kind; arithmetic reads them as 0/1, logical operations preserve them. Match, search, grade compare numeric value; reference checks also distinguish representation and exactness. -`Domain` and `with_field!` select working reals, Complex64 or BigRational for generic numeric algorithms. Compact storage selects the domain without scanning its items. `number::read` converts once; `Field::array` builds compact float/complex results without a `Number` per item. Each caller lists the domains it supports. Compact complex input does not take a real path merely because all imaginary parts are zero; results built through `Value::complex` can normalize to float storage. +Joining compact arrays widens Boolean → integer → float → complex; float widths join at the wider. Item-by-item construction keeps exact and approximate kinds apart, staying mixed when needed; only approximate real and complex items widen together. A write into an existing array follows the items rule (`Widening::Items`), as a literal of the same items would: exact and approximate numbers stay apart, as do Booleans and other numbers. Rearranging or combining mixed storage finishes in `Gather::finish`: compact where one dtype holds the items under the items rule; its scan compares each item's variant with the first's → a copy that stays mixed pays little. An in-place write into mixed storage keeps it mixed (a scan per write makes a loop of writes quadratic). Fresh computations choose storage from their result path. Results computed one cell at a time (Each, Rank, Key, power histories, polynomial and number-theory functions) assemble as items via `Value::assemble`; mix and catenation join arrays. Keep whole-array joining and element assembly distinct. -Rustymath owns the mathematical algorithms. Basedpl owns pervasion, storage, numeric promotion, axes and language policy. Code whose inputs and outputs are only buffers, shapes and plain numbers belongs in rustymath, even when only basedpl calls it. Approximate matrix solve/pseudoinverse and decomposition use rustymath factorizations. On macOS, rustymath sends much of this work to Accelerate, as its DEV.md describes. No platform-specific adapter belongs here. The Cargo configuration, for Cargo's own builds, and `pyproject.toml`, for the wheels, both set the macOS deployment minimum to 15. Exact solve/pseudoinverse/determinant use rational field algorithms. Generic permanent and polynomial helpers also run at floating/complex field types. Singular/wide matrix division gives least-norm solutions. LU eligibility and SVD cutoffs are rustymath policy, not duplicated here. +`element::Converts`: the one rule converting a compact item to another numeric type; size marks, `•float`, and programs' `Convert` steps in replays and block stages use it. Float target: reads the item as rustymath's `Source` does. Integer target: Boolean → 0 or 1; integer wraps to the width; float → floor within the float width's tolerance, then saturates at the target's range. `Value::converted`: plain values. `derivative::converted`: carried values, through the carrier (`Carrier::cast`); on a pending array, one more pending call (`fusion::convert`). -Hash maps and sets come from `rustymath::hashing`, as rustymath's DEV.md describes. Use its `HashMap`, `HashSet` and `Hashing` rather than another hasher, so every lookup in both projects inlines its hash the same way. +Bare numbers, empty literals, NaN: the parsed float width. Top-level `evaluate_code` validates the whole syntax, runs leading literal-record `•prefs` statements, then parses the rest at the new width; a syntax error prevents header execution. The default width also covers imported numbers, random data, and float results from exact operands. Time, date and metadata values: f64. Formatting marks non-default widths; serialization marks every float (width-independent readback). -`•decompose`/`•decompose⁻¹` share domain reading and `Factor` dispatch, with options for kind, vectors and complete QR. Polynomial adapters choose compact evaluation or generic field operations; gradients differentiate coefficients before evaluating. Number-theory adapters supply cancellation checks and assemble result cells. Distribution constructors expose native density/CDF/quantile/sample functions. Compact continuous calls use slice objects. Discrete samples and finite discrete quantiles are exact integers. +Float operations compute at the wider operand width; exact operands round to the width computed at. f16 operations run on f32 copies when `Numeric::halving` says: a call with an f16 operand, or any call while the default width is 16 bits. `pervasive::planned_halves` applies it to whole operations, `Number::halves` to scalar arithmetic and comparisons; `fusion::defer` declines those calls. Exact inputs convert once, to f32; float outputs round back to f16. The scalar `Number` path rounds each f16 step. Complex parts: f32 or f64; f16 becoming complex uses f32 parts. Scalar Complex32 arithmetic computes at Complex64 and rounds; kernels can compute at f32. These paths needn't agree in last bits. -`•rand seed` makes a ChaCha8 stream behind a mutex (`distribution::Stream`), which every copy of its record shares. `¿` and a distribution's `sample` without a generator draw from the session's stream, which `•prefs ["seed":n]` reseeds. Draws take a stream's numbers in the order they run. The algorithm is fixed, but range/distribution sampler changes can change draws; the reference sequence detects that. Sampling from a generator record uses its roll function to recover the same stream. +Floats: IEEE NaN, infinity, signed zero. Comparison: NaN unequal; match and search: one value; grade: last. Exact arithmetic dividing by zero can give those floats. Floor/ceiling keep an exact operand beside infinity; beside NaN the result is open: NaN or the other argument, varying by platform. Their folds, scans, windows, products and repeated writes give NaN or the maximum or minimum of the non-NaN items, never another number (rustymath's `Folds` doc): a loop folding by them passes its running result as the right argument, which `rustymath::reduce::maximum` keeps beside NaN, or folds through rustymath's `Max`/`Min`, as `Dyad::extremum` does. Monadic floor/ceiling/sign: whole finite results → exact integers, big integers included; nonfinite results stay floats. Outside `number.rs`, use `Number::float(value, width)` and explicit-width array construction, not implicit f64 conversion. -## Kernels, fusion and threading +Static kinds (`•prefs ["kinds":"static"]`): a result's kind follows from its arguments' kinds alone (`meta/kinds.md`). -A dispatch selects a primitive before its loop, and a pervasive call's plan picks its kernel table (`plan::Pervasive::computes`). Concrete `Kernels` tables call rustymath objects: `many` for slices and `one`/`checked` for scalar Numbers. `at_widths!` writes f32/f64 tables once. Arithmetic, circle-code conventions and scalar policy remain in the numeric modules rather than a second evaluator. +- `Numeric` carries `kinds` and `int`. Until the `float` pref sets a width, `Numeric::set` gives new floats the kinds' width: 32 bits static, 64 dynamic (`Kinds::float_width`). Whether the pref set one: `settings::float_set`, outside `Numeric` (every call builds and copies a `Numeric`; a Boolean field there cost about 14 instructions per primitive call, 34 per dfn call). +- A pervasive call on arrays plans before computing: `plan::pervasive` builds a `plan::Pervasive` from operand kinds (`plan::Kind`: type, fixed width, weakness) and settings, reading no items. It holds the settings with the operands' fixed width (`Numeric::for_kinds`), whether 16-bit floats compute in 32 bits, the computing kind (`computes_in`: rustymath's `DType::common` after `Numeric::kind`), its kernel table (`computes`) and, under static kinds, every result's kind (`Pervasive::result`). Eager dispatch, fusion's deferral (`Pervasive::fuses`) and typed programs read the same plan. +- Products and folds plan likewise: `plan::Product` holds `g`'s pervasive plan and the settings `f` folds its results under (`Product::folds`); `plan::Fold` a fold's or scan's settings and operand kinds. Eager products, folds and scans read them, as typed programs do. +- Comparisons, `⌈ ⌊` and bitwise functions compute at their arguments' storage width, where results are the same at every width (`pervasive::first_kind`). A comparison of integers with a whole float standing for an integer computes on integers via `Pervasive::computes`; so do a residue by such a float and a residue scan from one, whose results then convert to floats (`plan::whole_float`). +- Weak (`Number::is_weak`): literals, values computed only from literals, Python's scalar arguments. Before a call, `Primitive::weakened` converts a weak number beside a strong one to the width rustymath's `DType::common_weak` gives, or to a fixed integer's width where it fits. Weak integers have no fixed width; NEP 50 decides theirs. +- Loops keep the failed results `keeps` names: floats' NaNs, wrapped integers of objects reporting `WRAPS`. Kernel tables choose float `⌊ ⌈ ×` and wrapping negation from `run.numeric()`. `Stored::build_at` keeps complex results complex. +- `Primitive::number` gives every pervasive result on numbers and each step of a fold of numbers; under static kinds, `statically` converts it to the plan's kind. A static call on ordinary integers that an integer kernel takes runs it at the `int` width, as fixed integers run at theirs (`Primitive::integer_kernel`). Under dynamic kinds a call on two numbers stays outside plans: its result type depends on their values. +- `Number::wrapped`, `Value::wrapped`: wrap an exact integer result to the integer width: the fixed width `Numeric::for_kinds` finds among the operands, or `int` under static kinds. Under static kinds a strong ordinary integer is a fixed integer of the width it computes at, `int` joined with its storage width (`plan::Kind::stored`) → every static call on integer arrays computes under a fixed width. Display marks a fixed integer of `int` width with plain `ₓ` (`Mark::integer`). +- A fixed kernel failing where a result doesn't fit (lcm, binomial coefficients) gives the exact result wrapped (`Primitive::fixed_number`), used by `statically` and by exactly computing functions (`⊥`, `⌻`, the determinant). Where integers wrap, a function with approximate results (`⌹`, polar parts) computes integers as floats of their division's width (`plan::Kind::approximate`); `∂` converts its argument's wrapping integers to those floats before running its function (`derivative::differentiate`). `Number::parse` keeps complex literals and checks integer literals against the `int` width. -Arguments of mixed item types run through `Converting` where the object runs item by item in rustymath's own loop (`Results::INLINE`), which converts each item as it reads it. Other objects use `Chunked`, which reads each argument through rustymath's `Side`, converting a run at a time before calling one homogeneous implementation. Sharing those kernels avoids a storage-type cross-product of machine code. Compile only operand pairings that can occur. Same-type arguments can reach rustymath in one call; other shapes expand reused chunks or use block mappings to avoid per-item division. +Settings: once per process in `settings.rs`, read from there, not passed through functions, except where a read is slow and on a hot path. Atomics: each setting calls and scalar operations read: numeric settings, hanzi aliases, the `batch` pref, the verifier flag, the fusion report flag, fusion's sizes (`settings::set_sizes`), the compile setting. Display settings: behind one lock. Writers: `•prefs`; the lexer, for a source's leading `•prefs` header as it reads it. `settings::reset` restores defaults (`]clear`, test harnesses). A module `•load` runs changes settings process-wide; nothing restores them on return. Thread count: held by rustymath, the one setting it reads itself; `•prefs` writes it there. -Custom elementwise closures use `rustymath::pairs` and its uninitialized append path. Function objects use their own slice forms. Keep per-item bodies inlinable across codegen units; passing a closure by reference can introduce an uninlined `Fn` shim. Pass it by value, or wrap an existing reference in a new closure. Hot loops use slices and fixed chunks rather than iterator helpers that introduce calls in the incremental build. Avoid captured mutable flags that live in memory and block vectorization. +- Each call reads the store once, at its start: pervasive calls, folds and products as they plan (`Primitive::pervasive_mapped`); a dfn call as it looks for its program (`program::call`); `Domain::of`; the lexer. Values derived from settings within a call stay explicit arguments. +- A call's plan holds the settings it computes under → a pending array computes under its calls' plans, whatever settings are in force then. A pending conversion holds the settings giving its floor's tolerance (`Step::Convert`'s `numeric`). A dfn's programs are found by the settings they were traced under (`program::Key`); under other settings the dfn traces a new program. +- Passed explicitly: loop structs keep the settings (their closures run on rustymath's worker threads); `Number`'s tolerance methods and `Primitive::number` take them (once per item); the parser's leaves take a width marker's width; `matches_within` takes `⍣∞`'s convergence tolerance. +- Imports build complex numbers with `Number::complex_in_session`: Python's `_Session.array`, the JSON protocol's `request`. JavaScript values hold no complex numbers. -A failed compact kernel leaves placeholder results and triggers a wider/exact/complex or scalar path for the whole call. Complex fallback is shared with `Number` policy. Overflow detection belongs to rustymath: the integer forms of its arithmetic objects, its checked reductions and its checked products. Cheap integer multiplication uses bounds to select an exact width without a per-item overflow check. Add no blanket NaN/signed-zero canonicalization pass; algorithm-specific failure checks and rustymath's SIMD kernels remain part of their algorithms. +`Numeric::mathish_at(width)` owns Mathish eligibility: preference on, kinds dynamic, calculation ordinary exact, f64 or complex128; fixed-integer operations excluded even at f64. Kernels select their numerical objects, from rustymath, before the loop. Mathish comparison tolerance `1E-14`; f32 and f16 compare exactly. Fixed-point convergence: four ulps at those widths, `1E-14` at f64, independent of Mathish. Index, count, Boolean and character-offset conversion: `near_integer` with the fixed default tolerance. Test with independent inside- and outside-tolerance cases and exact/mixed controls, not language comparison as its own oracle. -`fusion.rs` defers the calls that a block stage can run (`plan::Pervasive::fuses`): `Primitive::pervasive_mapped` gives a pending array (`Data::Node` holding a `fusion::Pending`) in place of the result. A pending call holds a typed `Program` of its one step, whose plan is fixed when the call is made, and holds the arrays and atoms that it reads, each pending array in a value of its own (`fusion::held`). Pending arrays survive assignment and link to the pending arrays they read. Reading one forces it through `ArrayData::storage`, which gathers into its program the programs of the pending arrays that it reads in order (`Pending::gathered`) and runs the result through the units of its plan. `program::planned` plans each structure of program once, from a copy with inputs in place of its held arrays, and keeps the plan for later forces of the same chain on new arrays. Both builders of programs, fusion's and a dfn's trace, add each node through `program::add`, which gives an earlier node that makes the same value instead: a held atom of the same identity, the same held array, or the same step on the same values. A pending array that the program reads in order and that a name or another array also holds joins as an output (`Program::outputs`): the program computes it in the same pass, and a block stage stores its items as it stores the result. Each other pending array that the program reads but doesn't compute as a step is computed first, on its own (`Pending::gathered`): one that it can't read in order, and a copy that something else holds. A fold of a pending call's result that another array also holds stores the result as an output of the fold's pass, once for all its readers (`fusion::fold`). A pending copy folds the same way whether or not another array holds it. A block stage runs the program's elementwise steps block by block, in parts (`blocks::Part`): each run of steps that lower to machine operations giving their kernels' results exactly (`blocks::lowered_op`) through one compiled kernel (`jit.rs`), and the other steps through the same pervasive kernels (`pervasive::node`). A part whose kernel can't take a run's items, such as an input that a broadcast reads as one item, runs its steps on those kernels for that run. A stage compiles all its parts or none. Its compile policy (`blocks::Blocks::compiles`) compiles them where the same kernel compiled or ran before, or where the stage has at least `COMPILES_AT` items. `settings::Compiles` can compile every stage or none instead, for the reference harness, and for timings through `BASEDPL_COMPILE`. Timings can also set the fewest items of a call that waits, `MIN_ITEMS`, through `BASEDPL_MIN_ITEMS`. Readers that need only the shape, kind or prototype, such as `Value::len` and `Value::environment`, answer without forcing. `Value::force` forces a result, as timing does. Each step writes a block to a buffer that later steps reuse once its readers have run. A comparison that only later steps of a stage read writes 1 and 0 as floats there (`Dyad::compared`, rustymath's `tolerant::Indicator`). `⍚` and `@[p]` run cases that can't fail on every item and select their results by a Boolean mask (`selected` in `eval/operators.rs`). `plan::select` gives the select's type and the maps through which its mask and cases read. `fusion::select` defers the select where the mask is such a comparison, and otherwise runs it at once through rustymath's `chosen`. A stage of one step runs only where it reads through a copy, and records no `fused` row. Node-making code stays out of line from dispatch, because inlined code grows the frame that stays on the stack at every recursion level. The module doc owns eligibility rules and limits; don't copy its thresholds here. A structural copy of one source through rustymath `Steps` (`remapped`) whose result has at least `MIN_ITEMS` items gives a pending copy (`fusion::copy`) for any compact kind. Its map is a `Mapping::Steps` with one entry for each result axis. Broadcasting's maps use the same variant. Forcing composes a read of a copy with the copy's map, and moves a read with no repeat or fill under the pervasive calls it reaches (`fusion::resolved`). `2×⌽v+1` then reads `v` backwards in one program. A read that copies (rustymath's `Mapping::copies`) moves only under a call that reads one array. A call whose two array arguments copy arrays of one layout through one map runs on those arrays, and the map then copies its result once (`fusion::copied_once`). A program reads any other array through a `Copy` step, which a block stage reads through without making the copy. A step reads an array's items in order under another shape through its operand's `program::Read`, which holds that layout. A block stage shrinks its blocks where its buffers wouldn't fit in the cache. Rustymath's `agreement::expanded` reads a forward run in place, and copies anything else through `gather::walk_plan`. A `Mapping::Steps` keeps the plan that `Steps::simplify` makes on its first walk (`agreement::Route`), and every later block's read walks that plan. A copy that 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, block by block, and hands `reduce::fold_pieces` each block's total (`reduce::Part`): sums, float products, and maxima and minima. A float fold keeps as many running results as rustymath's folds keep (`reduce::running`). Boolean folds, float folds by `⌈` and `⌊`, and blocks that cross a lane go through the block that the last step writes. On the pending route, a fold of a copy of windows along its own axis slides over the copy's source instead (`fusion::slide`, through rustymath's `structure::Slide` and `reduce::sliding`), and a fold along two window axes folds both in one pass (`fusion::fold_windows`). A short fold that a stage folds as one step for each window position (`blocks::short_window`) runs as a block stage that reads 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, such as padding, into a result whose last axis is longer than `DIRECT` (`fusion::staged`). Under `∂`, `fold_rule` takes a maximum's or minimum's positions in such windows from the source (`fusion::picks`). Under a trace, a program step (`Step::Picks`) gives each lane's pick, through the function that gives the untraced mask (`derivative::chosen`). A traced scan, or picks from sliding windows, by `⌈` or `⌊` stops the trace. +`search.rs` selects compact exact or tolerant backends. -Ordinary pervasive kernels also read pending copies through their maps. `fusion::source` snapshots the source and mapping without taking the pending work, and rustymath composes that mapping with the call's agreement. `pervasive::Input` keeps the snapshot alive while kernels read its storage through existing blocked readers. Folds by rustymath's reductions read a pending copy through `fusion::source` too (`pervasive::mapped_fold`). `reduce::along_mapped` folds a copy that reads a contiguous block of its source with its axes in another order, as `Mapping::block` finds it, in the block's own order. Transposes, reversals and windows that don't overlap read such blocks. Where the totals come out in another order, `pervasive::fold` makes them a copy through the tables that `along_mapped` gives, and the next fold reads that copy as a block again. Any other copy folds a run at a time where it reads runs of at least `reduce::RUN` items, with no copy, and copies a piece at a time otherwise. A copy that a name or another array holds, read in shorter runs (`reduce::rereads_cheaply`), folds instead from the copy that `native_fold` computes, which keeps its items for later reads (`fusion::held_copy`). A read of a few items from a pending array goes through `fusion::narrow`, which narrows the pending work to the positions read without consuming it. A chain of copies becomes a read of its source, for any item kind. An elementwise call, or the program that a fold left, becomes one over only those positions on its first nonempty partial read, regardless of sharing, when the read takes at most 1 in 128 of its items. Where maps don't compose, as a rotation's doesn't with an overtake when it carries source items across the end, such a read lists its items' offsets. The original array retains its work; the next actual read computes and caches its whole result. Copy unwrapping does not count separately, nor do empty or all-missing reads. `fusion::item` serves single items and picks, and `Gather::walk` serves small copies and gathers through `fusion::walk_through`. Checks of a whole input's values, such as bounds and finiteness, read only the items that its logical `ArrayView` selects. Unsupported compositions and dense readers retain the pending array's one cached compact result. +- Classification of 1,024 items or more first groups items sharing one array, before reading any (`search::shared_classes`). Imports make repeated texts share: `keyed::Texts` makes each distinct text once per `•csv` column, `•json` document and Python conversion, up to 65,536 distinct texts. +- Exact integer, Boolean and character storage uses keys; float searches use their width. Integer haystacks take the whole-number shortcut only for exactly whole float needles. Tolerant indexing validates whole-key matches unless the haystack is exactly whole, falling back to buckets on mismatch. Classification chains tolerance through sorted runs by design: it needn't match every member to one representative. Other cells: exact hashes or first-number buckets with `Value::matches` confirmation; pairwise comparison where indexing is unsuitable. Grade is stable. +- Position buffers: u32, becoming integer storage without another pass. `⍷` and monadic Key take each class's positions from `search::class_positions`, which keeps a class's own buffer for 32-bit positions and copies narrower ones once from the shared buffer. Dyadic Key selects rows through `Groups::each` slices. Key's group-fold idiom folds through `search::numbered`'s class numbers; where `fold_groups` declines, Key builds positions from those numbers. Key with operand `≢⊢` (also built by `≢∘⊢`) counts classes via `search::class_counts`, building no positions. +- Under Rank and in batched maps, `⍳`, `∊` and `⍸` whose haystack differs per cell search each cell's own haystack in one call on the whole arguments (`primitive::searched_cells`). -`explain.rs` holds the events that `•explain` reports. One check in `Function::reported` serves both `•prefs ["fusion":$t]` and `•explain`: a call that computes a pending argument writes the pref's line and records a `forced` event. `Blocks::run` records a fused group for each block stage of two or more steps, its fold included. `Idiom::call` records each fast combination taken, by its `Idiom::route`. `linear::scatter_add`, and the transpose of a gather that scatters a source's items, record each accumulation. `Selection::write_into`, where assignment and Under write, records each write that leaves a compact array mixed. `•explain` and Python `rm.explain` use the `rustymath::events` capture implementation. Each native module captures its worker threads through one process-wide sink. Captures can nest on one thread. The shared `Route` declaration supplies fast, decline and fixed itemwise IDs and their coverage inventory. Successful routes aggregate call and item counts. Optional declines remain separate from stopped traces, which record a slow fallback on every execution, including cached failures. Outside capture, a native event checks one atomic flag. Each record node and pending array holds an `explain::Live` guard, which counts it for the thread that made it until it is dropped, on any thread. `•memory` reports those counts, and `Value::footprint` walks a value's allocations for its `bytes`. +`Domain`, `with_field!`: select working reals, Complex64 or BigRational for generic numeric algorithms; compact storage selects the domain without scanning items. `number::read` converts once; `Field::array` builds compact float or complex results without a `Number` per item. Each caller lists the domains it supports. Compact complex input doesn't take a real path merely because all imaginary parts are zero; results built via `Value::complex` can normalize to float storage. -`derivative.rs` implements `∂`. A dual is a tag, a primal and a tangent: a `transform::Carrier`, held in `Value::Carried` for a number and in `Data::Carried` for an array, whose primal and tangent can be pending. A record of linear calls is the other kind of carrier. Routing (`transform::call`) hands a call to the newest level among its arguments, as `Value::newest` finds it. It reads their outer layer, but a newer level inside an argument held in mixed storage wins, as an array of dual atoms beside an older dual shows. A call whose newest level is a `∂` call goes to `derivative::call`, which reads how to run it from the form's facts in `Primitive::info()` (`primitive::Facts`). It runs a pervasive form's partial derivatives as primitive calls, from the recipe of the function object that the kernel table chooses for the call (`partials`, `pervasive::Dyad::differentiable`, `rustymath::derivative`), applies its own rule to `⌻`, and calls forms whose results carry no tangent on the primals. Structural glyphs, Under and selective assignment need no rules. Their own code moves each part of a carried array through the carrier's moves: `Carrier::view`, `Carrier::at`, `Carrier::copied`, and `transform::written` for writes, which a record answers through its ids. `Gather` copies a dual's primal and tangent together through `derivative::Join`. A glyph that copies its right argument through one plan, and a broadcast, copy a record tangent as one `Op::Copy` (`primitive::copied`, and `Form::Moves`'s `planned`). It holds the plan's `Steps` as a rustymath `Mapping` (`Mapping::walk`), the map type that a pending copy and a program's `Step::Copy` also hold. Its transpose reads through `linear::transposed`, built from rustymath's `Walk` and `Steps`: the inverse map for a copy that reads each item at most once, and a correlation for windows that move by 1. A copy of a copy transposes in one read where the composed copy still transposes by reads (`linear::composed`). Its maps compose through rustymath's `Mapping::through`, as fusion composes a read of a pending copy and a trace composes a copy of a copy. The record keeps the chain, because recognising windows needs each copy's own shape. A sum along a window's axis transposes as a sliding sum (`linear::slide`), and a spread axis as a sum along it. Windows that move by more than 1 transpose by rustymath's `signal::overlap_add`, which adds each window back where it read (`linear::strided_back`). Other copies, and copies of several sources, keep ids and scatter. `•explain` lists each scatter as an accumulation. In reverse mode, the rule for a `⌈` or `⌊` fold is a sum of the tangent times a mask of each lane's chosen item. Its transpose reads. Mix of a dual whose primal is nested mixes the primal and the tangent apart. `fold` and scans with `+`, `×`, `⌈` and `⌊`, `+.×` and outer products have rules, and kernels that read numbers directly leave active arguments to their general paths. Folds by primitives, products of primitives, `⌹`, `⊥` and stacks of matrices route through `transform::call` as operations (`primitive::Operation`), so their rules (`derivative::solve`, `decode`, `product_dual`, `determinant` and `fold_dual`) run on a `&Context` and call `Operation::call` for the products, folds and inverses that they need. Rank's routes for whole arguments send stacks that a level carries through `Pairing::Matrices`, so `⌹⍤2`, `+.×⍤2` and `-.×⍤2` take their stacked rules in one call, with the `batch` pref on or off. Attached rules, system functions, and folds and products by functions that aren't primitives route through the same `transform::call`, as `transform::Op`'s other variants. The rules for `⌹` and `-.×` keep the glyph's own result as the primal, so that a function computes the same values with or without `∂`. Their tangents and transposes multiply by `R`, the inverse that `numeric::matrix_inverse` takes from one factorization through rustymath's `Stacked::inverse`, or by exact row reduction for exact matrices. `regular_inverse` gives UNSUPPORTED where rustymath's regularity test finds the matrix singular. With only `B` active, `R` is `⌹A`, a pseudoinverse included. With one number in `x`, `f∂ x` runs `f` once with `x` seeded along it. With more, `linearized` pushes a record level just below its `∂` call's level, then runs `f` once with each part of `x` seeded with a record at that level, from `linear.rs`: a tangent whose items are never computed, which the same rules extend into a graph of linear calls. A call whose newest level is a record level records the linear map that its form's facts name (`linear::call`). `reverse` then reads the Jacobian from the record, by a transpose for each result item or a forward evaluation for each number of `x`, whichever are fewer. A function with a JVP rule that `f∂←g` attached calls the rule with `⍺` a dual of zeros whose tangent is the argument's, so in reverse mode the rule extends the record too (`derivative::attached`). A call of such a function with two arguments gives DOMAIN ERROR wherever `∂` needs a derivative through it (`derivative::two_arguments`). Code that reads a dual array's storage gets its items as dual atoms, which is correct at per-item speed. A rule packs an array that holds its level's duals, at any depth, into one dual before it splits the array (`derivative::packed`). A reader outside a call sees a value through `transform.rs`'s read point. `Value::observed`, which counts, positions, ranks, selectors, `≡` and guards' predicates use, reads a dual's primal. The integer readers of `Items` read carried items through it (`Value::observed_number`), so a count or position that `∂` carries reads its primal. It returns a `Result` whose error, `transform::Stop`, names the level that owns a batched run. A stop is `ErrorKind::Stop`, which no guard catches, even one that names it, and which a handler that catches every error lets pass (`ErrorKind::is_control`). Display reads through `Value::displayed`, because it never runs inside a batched run. `Value::computed`, which kernels use through `primitive::numeric`, gives UNSUPPORTED for a dual atom. Writes to bindings outside a `∂` call are recorded and stripped when it returns. +Rustymath owns mathematical algorithms; basedpl owns pervasion, storage, numeric promotion, axes and language policy. Code whose inputs and outputs are only buffers, shapes and plain numbers belongs in rustymath, even if only basedpl calls it. Approximate matrix solve, pseudoinverse and decomposition: rustymath factorizations; on macOS rustymath sends much of it to Accelerate (its DEV.md); no platform-specific adapter here. macOS deployment minimum 15, set by the Cargo configuration (Cargo's builds) and `pyproject.toml` (wheels). Exact solve, pseudoinverse and determinant: rational field algorithms. Generic permanent and polynomial helpers also run at floating and complex field types. Singular or wide matrix division: least-norm solutions. LU eligibility and SVD cutoffs: rustymath policy, not duplicated here. -`program.rs` and `program/` hold the typed programs that pending work and `∂` run. `∂` of a dfn runs from one program that holds its whole call, its value and its derivative (`derivative::differentiate`). The first call with given argument types, layouts and settings traces the call (`program/trace.rs`): the dfn runs on traced values, which carry a type and a layout but no items, and each call on them plans its result (`plan`) and adds a step through `program::add`. A trace that meets an effect, a read of a value, or a call that no plan covers stops, and the call runs in the interpreter. `Programs` keeps each trace's outcome, a program or a stop, on a dfn's `Definition`, or on the structure of any other function (`program::Tacit`, which keeps `STRUCTURES` of them). An outcome applies to a later call with the same argument signature whose free values still hold what the trace read (`Entry::reread`), so free values that cycle replay. A signature whose free values change on every call stops tracing after `CHANGES` traces in a row, and a function keeps at most `KEPT` outcomes. A replay runs the program on the call's values (`program::call`). `program/run.rs` replays a program one step at a time, as the reference that compiled code matches. Fusion's programs and traced programs pass through the same code. For index notation and its gradients, a program also holds steps that select by positions it computes (`Step::Gather`), write or accumulate at the places that a map or a selector gives (`Step::At`), add a windows copy's cotangent back to its source (`Step::OverlapAdd`) and mark the item that a `⌈` or `⌊` fold picks in each lane (`Step::Picks`). Under index notation's level 2, rustymath's selection planning (`structure::joined`) reads an index array whose positions are affine through strides instead of a table of offsets (`Table::affine`). `program/contraction.rs` runs a `+` fold of a `×` whose operands read their sources through affine maps as one product (`contracted`), and a fold of a copy that repeats items as a fold of the copy's source (`fold_of_copy`). `program/layout.rs` stores a program's maps in the storage order that their operands and readers need (`layout::ordered`). `blocks::plan` finds the runs of elementwise steps that block stages compute. `program/verify.rs` checks each program's contracts before it runs, in every reference case and page (`settings::verifies`). `•explain` lists the programs that a call built or replayed and the traces that stopped (`programs`, `declined`). +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. -`batch.rs` makes a map's batched call. `batch::tried`, which `each_pair` calls, begins a batched level for the run (`transform::Kind::Batched`), wraps each argument that has a frame in a `Batch` carrier at that level, calls the operand once, and ends the level. Each map first lays its arguments over its frame through its own agreement, the pairing that its calls on each cell read (`batch::Arguments::laid` and `agreement::laid_cells`). `each_pair` makes no batched call for one cell of atoms, which runs faster without one. Routing hands each call whose newest level is the run's to `batch::call`: a primitive, an operation that primitives name, such as a fold by a primitive or `+.×`, or an operation by a dfn. It leaves `⍕` and random draws to their own code. It runs any other system function once for each cell, on that cell's arguments, as the function runs once for each item of an argument that holds several, and joins the cells' results into one batch (`batch::system`, `batch::each_cell`). `batch::call` runs the call on the whole arguments by Rank's routes: `eval::whole_cells` through `batch::whole_call`, and `eval::folded_cells` for a fold. A primitive therefore batches wherever Rank runs it on whole arguments. A fold or scan that `folded_cells` doesn't cover, such as one by a dfn, runs the fold's own loop over the batch's items. Each stage is then one call, on item `j` of every cell (`Batch::at`). `fold_array` and `primitive_fold` join a result that holds stage batches into one batch (`joined`). Every fold and scan route decides its structural cases, such as a unit, empty lanes and the result's shape, through `FoldPlan`, and ends in `folded`, which alone gives the result its layout, fused folds included. A fold or scan by any function other than a primitive, over more than one lane, also tries a batched run of its own: `operators::by_lanes` moves the fold axis last and runs the derived fold over the lanes through `batch::tried`. `operators::by_triples` does the same for `f.g` where `g` isn't a primitive: one batched call of `g` on a frame of rows by contraction by columns, then the fold by `f` along the contraction, for each chunk of rows whose triples hold at most `batch::CHUNK` items (`operators::by_rows`, which products of primitives share). Each runs its usual loop from the start where its try gives `None`. A fold keeps its stops on its operand's `Definition` (`Function::remembers`). `batch::tries` holds the conditions for any try. A call that no route covers stops the run (`transform::Stop`). A call whose constants with axes, repeated over the frame, would hold more than 4 times the items of its arguments and more than `batch::CHUNK` items, 2²¹, stops the run with the cells of a chunk whose share holds at most `CHUNK` items (`oversized`, `Stop::chunk`). `tried` then runs the operand once on each chunk of that many cells (`batch::chunked`), each as a batched level on a frame of one axis, and joins the chunks' results as the run over cells assembles its cells' results. A chunk that stops for too many items again starts the chunks over with its own smaller chunk. Where one cell's share alone passes `CHUNK`, the map calls its operand once for each cell. Code that must run once for each cell asks the levels first (`Levels::effect`): output, input, `⍕` of a carried value, and the code in a system function that acts (`Effect::Acts`, through `Context::acts`), a guard or a call on prototypes that would catch an error (`Effect::Catches`), and a write to a binding outside the map's frames (`Effect::Writes`, through `Levels::record_write`). A read through `Value::observed`, as a count or a position makes, stops the run too. A guard whose predicate is a batch of the run splits the run instead (`Session::guard` through `operators::guard`). `batch::truths` gives each cell's Boolean, and each side runs once, as a guard case on its own cells (`Levels::case`), through the `Cells` that `⍚` uses. Each batched binding in the frames that the run started is read at the side's cells (`Session::restricted`, through `batch::read_cells` and `operators::cells_at`), and `Cells::assembled` joins the sides' results. During a case, the run's frame is the case's cells, a random draw draws for the case's cells, and a write to an enclosing frame stops the run. A map inside the run widens the run instead of beginning another, where the run is the innermost level (`batch::Entry::Widen`). The widening is a case with no definition, whose frame is the run's frame followed by the map's. The run's batches among the map's arguments lay over that frame (`batch::laid_over`), and a binding made before the widening lays over it when code reads it (`Session::read_binding`). A widened run that stops, or an Each whose results are arrays, falls back to calling the map's operand once for each of its cells inside the run. A guard of a dfn that already splits the cells in another frame stops the run as a recursive call (`Cause::Recursion`), and sides whose cells differ in layout or kind stop it as a call with no rule. Each stop carries its cause (`transform::Cause`), which says how long a dfn operand remembers it (`Cause::kept`). An effect, a write, an observing read or a recursive call lasts for every later call. A call that no route covers lasts while later calls repeat the arguments' kinds and cell shapes, and the memory guard lasts while they also repeat the frame's shape. A caught error never lasts, because whether an error arises depends on the cells' values. The dfn's `Definition` holds what it remembers (`Definition::remember`): `ALWAYS`, or a signature of the arguments (`batch::signature`), each holding the stop's cause (`batch::holding`). A later try that it covers falls back without calling the operand, with an `"an earlier stop"` event that names the cause. A stop's own `fallbacks` event names its cause (`Cause::reason`), and the glyph of a primitive whose call no rule covers (`Stop::refusing`). Code with no rule for batches that reads a batch's items gets item carriers, item i of every cell, and records an `itemwise` row, route `"batched items"`. `explain::itemwise` is the one helper for routes that handle items one at a time, and the innermost call claims its counts (`Function::reported`), as `forced` does. A kernel that then reads a number meets the stop that `Carrier::computed` gives a batch. A missed reader therefore costs speed or a fallback, never a wrong result. A batched run can also start inside a `∂` call. The run's level is newer than the `∂` tag, so the batch rules take each call first, and their calls on whole arrays reach the derivative rules. A batch's whole can then carry a dual, whose kind batch checks read through `Value::template_dtype`. Stacks of matrices whose wholes carry a dual take the derivative rules through `Pairing::Matrices` (`eval::whole_cells`), because the stacked kernels read compact storage. A `∂` call inside the run differentiates with respect to batches of atoms or of arrays. `derivative::numbers` counts a cell's numbers from the batch's template, every cell shares each direction's seed, and the Jacobian joins into one batch (`batch::joined`). A move with no plan on a dual whose primal is a batch runs on the primal and the tangent apart (`derivative::apart`). Adjoints that a transpose adds into a batch add as one sum on its whole (`batch::scatter_add`). An attached rule runs on the whole batch at its argument's `∂` tag (`batch::call`), and `batch::split_whole` and `batch::dual_of_wholes` keep the newest level outermost. A roll draws once, on its argument laid over the frame (`distribution::drawn_items`), and deals and samples draw for each cell in turn (`distribution::each_cell`), all from one stream in cell order. Where each cell draws once, seeded draws match a run cell by cell, except under a guard that splits the cells, where each side draws for its own cells in turn. A try that falls back puts back the state of each generator it drew from (`distribution::settle`). The map's result must be a batch at its level, a strand of batches whose kinds join (`joined`), or an atom or compact array with no carrier, as every cell's result. Any other result falls back. A pervasive call stops where its result's kind differs from the kind that one route gives every item (`pervasive::expected`). Each cell then keeps the kind that a run cell by cell gives it. `reference::check` runs each reference case with the `batch` pref off, then on, then on under fusion's small sizes (`fusion::Sizes::SMALL`), and compares the storage kinds of the later runs' values with the first's. +`•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. -Rustymath owns numeric folds, scans, adjacent pairs, grouped reductions and integer bounds. Basedpl selects kernels, tolerance, seeds, storage widths and exact fallbacks. Which reduction folds each primitive comes from rustymath's `facts!` table, through `pervasive::folds` and `truth_fold`, and so do the object by whose rule each takes characters, the comparison objects that `with_comparison!` makes, and the product kernel that each pair `f.g` names (`pervasive::known_product`). Folds by algebraic float operations can regroup. Contiguous subtraction/division folds use `Alternating`; quotients fall back to item-by-item folding if their products leave the normal range. Boolean scans compose truth-table maps; comparison scans compute both possible totals in bulk and use `reduce::chosen`. Seeds, axis lanes and scalar fallback preserve the language's accumulation direction. Generic reductions stay right-associated and preserve operand call order/effects. Monadic `f.g` is the exception: it memoizes each minor's expansion, and a cached minor does not repeat operand calls or their effects. Determinant/permanent fast paths use the appropriate rustymath backend. +`•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. -The automatic Rayon scheduler serves rustymath elementwise `many` and `pairs`, broadcasts through `agreement::blocks`, folds through `reduce::along`, block stages and matrix products. Scans don't split. Decompositions split in the products of their blocked updates, the passes of their Hermitian and bidiagonal reductions, and their divide-and-conquer merges. `rank` sends dyadic `+.×` and `⌹`, monadic `⌹` and `-.×`, and `•decompose` over the 2-cells of an unkeyed stack with compact storage to `primitive::stacked` and `stacked_decompositions`. They run each cell through the rustymath function it uses alone. A pervasive operand, or Rank nested down to one, runs as one call: each argument gets length-1 axes after each level's frame, so leading-axis agreement pairs frames with frames and cells with cells. An argument with a named frame axis goes through a batched map instead, whose frame carries no names, so names pair only between cells (`batch::tried`). Rustymath's `parallel::stack` splits whole cells across threads in rounds, with rustymath's interrupt check between rounds. A caller whose checks run far apart calls `check_now`, because `Context::check` reads the clock only on every 64th call. The scheduler adapts to call cost, uses `RAYON_NUM_THREADS` by default and accepts one override for the whole process through `parallel::set_threads` (0 default, 1 serial), which `•prefs ["threads":n]` writes directly. The timing harness sets one thread after each reset of the settings. Nested Rayon work does not split again; wasm is serial. A block stage splits in pieces of whole blocks: an elementwise stage through `parallel::extended`, and a folded one through `reduce::fold_pieces`. On one thread an elementwise stage appends straight to its result, because copying each block into a piece's slots cost up to 1.2 copies there. Small converted chunks limit the work available to the scheduler. Don't infer multicore execution from a large array alone. +## Kernels, fusion and threading -A pair that rustymath has a product kernel for, such as `+.×`, `⌈.+`, `∨.∧` or `+.=`, runs through `linalg::Product::run`, which Python's `inner` also takes (`pervasive::known_product`). Other native pairs use its bounded `inner_with` runner and `reduce::right_fold`, carrying the right tail across blocks. Basedpl supplies its primitive kernels, tolerance and exact-widening fallback. An integer product that doesn't fit 64 bits gives way to the exact path. `pervasive::inner` takes each operand's `Orientation`. Its float and complex products read a transposed operand in place, as reverse mode's `product_transpose` needs. A float `+.×` whose operand is a pending copy, or a view of one, hands rustymath's `Inner::inner_mapped` the copy's source and map (`fusion::source`, `pervasive::mapped_product`). A `+.×` whose operands are both pending conversions of compact `u8`, `i16` or f16 arrays to a wider type reads those arrays. Rustymath's kernel converts each item as it packs it (`fusion::widened`, `pervasive::widened_product`). A dfn program leaves such a conversion pending for its product (`Unit::Pending`). Rustymath gives gemm an operand that its map reads at strides, such as a transpose, where it lies (`Mapped::matrix`). It reads any other copy a block of rows at a time, except in a product with one column, where the copy's rows are adjacent in storage, as for a vector or a single-channel image. It then adds each column of the copy where it lies (`linalg::by_columns`). Neither builds the copy. A copy that a name holds is read again for each product rather than cached. Reverse mode transposes a product whose operand is windows that move by 1 as one product: the windows of the zero-padded cotangent with the kernel reversed (`linear::convolved_back`, rustymath's `signal::correlation`). Structural gather/compress and interval index also reuse rustymath. Measure changes to generic or backend code in both projects. The callers' codegen and layout decide costs that a kernel measured alone misses. +Dispatch selects a primitive before its loop; a pervasive call's plan picks its kernel table (`plan::Pervasive::computes`). Concrete `Kernels` tables call rustymath objects: `many` for slices, `one`/`checked` for scalar Numbers. `at_widths!` writes f32/f64 tables once. Arithmetic, circle-code conventions and scalar policy stay in the numeric modules; no second evaluator. + +Mixed item types: `Converting` where the object loops item by item in rustymath (`Results::INLINE`), converting each item as read; else `Chunked`, reading each argument through rustymath's `Side` a converted run at a time into one homogeneous implementation → no storage-type cross-product of machine code. Compile only operand pairings that can occur. Same-type arguments can reach rustymath in one call; other shapes expand reused chunks or use block mappings, avoiding per-item division. + +Custom elementwise closures: `rustymath::pairs` and its uninitialized append path; function objects use their own slice forms. Keep per-item bodies inlinable across codegen units: a closure passed by reference can add an uninlined `Fn` shim → pass it by value, or wrap the reference in a new closure. Hot loops: slices and fixed chunks, not iterator helpers that add calls in the incremental build. Avoid captured mutable flags held in memory: they block vectorization. + +A failed compact kernel leaves placeholder results and reruns the whole call on a wider, exact, complex or scalar path; complex fallback shares `Number` policy. Overflow detection belongs to rustymath: its arithmetic objects' integer forms, checked reductions, checked products. Cheap integer multiplication picks an exact width from bounds, with no per-item overflow check. No blanket NaN or signed-zero canonicalization pass; algorithm-specific failure checks and rustymath's SIMD kernels stay inside their algorithms. + +Fusion (`fusion.rs`; its module doc owns eligibility rules and limits: don't copy its thresholds here): + +- Calls a block stage can run (`plan::Pervasive::fuses`) defer: `Primitive::pervasive_mapped` returns a pending array (`Data::Node` holding a `fusion::Pending`): a typed `Program` of its one step, plan fixed at call time, plus the arrays and atoms it reads, each pending one in its own value (`fusion::held`). Pending arrays survive assignment and link to the pending arrays they read. Node-making code stays out of line from dispatch: inlined, it grows the frame kept on the stack at every recursion level. +- A read forces via `ArrayData::storage`: gathers in the programs of pending arrays it reads in order (`Pending::gathered`), runs the result through its plan's units. `program::planned` plans each program structure once, from a copy with inputs for held arrays, reusing the plan for later forces of the same chain on new arrays. Shape, kind and prototype readers (`Value::len`, `Value::environment`) don't force. `Value::force` forces, as timing does. +- Fusion and dfn traces both add nodes via `program::add`, which returns an earlier node making the same value: a held atom of the same identity, the same held array, or the same step on the same values. +- 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. +- 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.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 `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 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. +- Ordinary pervasive kernels read pending copies through their maps: `fusion::source` snapshots source and mapping without taking the pending work; rustymath composes that mapping with the call's agreement; `pervasive::Input` keeps the snapshot alive while existing blocked readers read its storage. Rustymath-reduction folds read pending copies via `fusion::source` too (`pervasive::mapped_fold`). `reduce::along_mapped` folds a copy reading a contiguous source block with reordered axes (per `Mapping::block`: transposes, reversals, non-overlapping windows) in the block's own order; totals in another order become a copy through `along_mapped`'s tables (`pervasive::fold`), which the next fold reads as a block again. Other copies fold a run at a time where runs reach `reduce::RUN` items, uncopied; else a piece at a time, copied. A held copy read in shorter runs (`reduce::rereads_cheaply`) folds from the copy `native_fold` computes and keeps for later reads (`fusion::held_copy`). +- A read of a few items goes through `fusion::narrow`, narrowing the pending work to the positions read without consuming it: a chain of copies → a read of its source, any item kind; an elementwise call, or a fold's leftover program, → one over those positions only, on its first nonempty partial read of at most 1 in 128 of its items, shared or not. Where maps don't compose (a rotation with an overtake carrying source items across the end), the read lists its items' offsets. The original keeps its work; its next full read computes and caches the whole result. Copy unwrapping, empty and all-missing reads don't count. `fusion::item` serves single items and picks; `Gather::walk` small copies and gathers via `fusion::walk_through`. Whole-input value checks (bounds, finiteness) read only items its logical `ArrayView` selects. Unsupported compositions and dense readers keep the pending array's one cached compact result. + +`explain.rs`: events `•explain` reports. One check in `Function::reported` serves `•prefs ["fusion":$t]` and `•explain`: a call computing a pending argument writes the pref's line and records `forced`. `Blocks::run` records a fused group per block stage of two or more steps, fold included; `Idiom::call` each fast combination taken, by `Idiom::route`; `linear::scatter_add`, and the transpose of a gather scattering a source's items, each accumulation; `Selection::write_into` (assignment, Under) each write leaving a compact array mixed. `•explain` and Python `rm.explain` use the `rustymath::events` capture: each native module captures its worker threads through one process-wide sink; captures can nest on one thread; the shared `Route` declaration supplies fast, decline and fixed itemwise IDs and their coverage inventory. Successful routes aggregate call and item counts. Optional declines stay separate from stopped traces, which record a slow fallback on every execution, cached failures included. Outside capture a native event checks one atomic flag. Each record node and pending array holds an `explain::Live` guard counting it for its making thread until dropped, on any thread; `•memory` reports the counts; `Value::footprint` walks a value's allocations for its `bytes`. + +`∂` (`derivative.rs`): + +- Dual: tag, primal, tangent: a `transform::Carrier` in `Value::Carried` (number) or `Data::Carried` (array); primal and tangent can be pending. A record of linear calls is the other carrier. +- Routing (`transform::call`) sends a call to the newest level among its arguments (`Value::newest`), reading their outer layer; a newer level inside an argument in mixed storage wins (an array of dual atoms beside an older dual). Newest level a `∂` call → `derivative::call`, run by the form's facts in `Primitive::info()` (`primitive::Facts`): a pervasive form's partial derivatives run as primitive calls, from the recipe of the function object the kernel table picks (`partials`, `pervasive::Dyad::differentiable`, `rustymath::derivative`); `⌻` has its own rule; forms whose results carry no tangent run on primals. +- Structural glyphs, Under and selective assignment need no rules: their code moves each part of a carried array through the carrier's moves (`Carrier::view`, `Carrier::at`, `Carrier::copied`; writes: `transform::written`, which a record answers through its ids). `Gather` copies a dual's primal and tangent together (`derivative::Join`). +- A glyph copying its right argument through one plan, and a broadcast, copy a record tangent as one `Op::Copy` (`primitive::copied`; `Form::Moves`'s `planned`), holding the plan's `Steps` as a rustymath `Mapping` (`Mapping::walk`), as pending copies and `Step::Copy` do. Its transpose reads through `linear::transposed` (rustymath's `Walk`, `Steps`): the inverse map for a copy reading each item at most once; a correlation for windows moving by 1. A copy of a copy transposes in one read where the composed copy still transposes by reads (`linear::composed`); maps compose via rustymath's `Mapping::through`, as in fusion and traces. The record keeps the chain: recognising windows needs each copy's own shape. A sum along a window axis transposes as a sliding sum (`linear::slide`); a spread axis as a sum along it. Windows moving by more than 1: rustymath's `signal::overlap_add`, adding each window back where it read (`linear::strided_back`). Other copies, and copies of several sources, keep ids and scatter (`•explain`: one accumulation each). +- Rules: reverse mode's `⌈`/`⌊` fold: the tangent times a mask of each lane's chosen item, summed; its transpose reads. Mix of a dual with a nested primal mixes primal and tangent apart. `fold` and scans with `+ × ⌈ ⌊`, `+.×` and outer products have rules; kernels reading numbers directly leave active arguments to general paths. Folds by primitives, products of primitives, `⌹`, `⊥` and stacks of matrices route through `transform::call` as operations (`primitive::Operation`): their rules (`derivative::solve`, `decode`, `product_dual`, `determinant`, `fold_dual`) run on a `&Context`, calling `Operation::call` for needed products, folds and inverses. Rank's whole-argument routes send carried stacks through `Pairing::Matrices` → `⌹⍤2`, `+.×⍤2`, `-.×⍤2` take stacked rules in one call, `batch` on or off. Attached rules, system functions, and folds and products by non-primitives take the same `transform::call`, as `transform::Op`'s other variants. +- `⌹`, `-.×` keep the glyph's own result as primal → same values with or without `∂`. Tangents and transposes multiply by `R`: the inverse `numeric::matrix_inverse` takes from one factorization via rustymath's `Stacked::inverse`, or exact row reduction for exact matrices; `regular_inverse` gives UNSUPPORTED where rustymath's regularity test finds the matrix singular. Only `B` active: `R` is `⌹A`, pseudoinverse included. +- One number in `x`: `f∂ x` runs `f` once with `x` seeded. More: `linearized` pushes a record level just below its `∂` call's level, runs `f` once with each part of `x` seeded with a record at it (`linear.rs`): a tangent whose items are never computed, which the same rules extend into a graph of linear calls. Newest level a record level → records the linear map its form's facts name (`linear::call`). `reverse` reads the Jacobian from the record: a transpose per result item or a forward evaluation per number of `x`, whichever fewer. +- An `f∂←g` JVP rule is called with `⍺` a dual of zeros whose tangent is the argument's → in reverse mode it extends the record (`derivative::attached`). Such a function called dyadically: DOMAIN ERROR wherever `∂` needs a derivative through it (`derivative::two_arguments`). +- Code reading a dual array's storage gets dual atoms (correct, per-item speed). A rule packs an array holding its level's duals, at any depth, into one dual before splitting it (`derivative::packed`). Readers outside a call use `transform.rs`'s read point: `Value::observed` (counts, positions, ranks, selectors, `≡`, guard predicates) reads a dual's primal; `Items`' integer readers read carried items through it (`Value::observed_number`). Its `Result`'s error, `transform::Stop`, names the level owning a batched run. A stop is `ErrorKind::Stop`: no guard catches it, even one naming it; catch-all handlers let it pass (`ErrorKind::is_control`). Display reads via `Value::displayed` (never inside a batched run). `Value::computed` (kernels, via `primitive::numeric`) gives UNSUPPORTED for a dual atom. +- Writes to bindings outside a `∂` call are recorded and stripped when it returns. + +Typed programs (`program.rs`, `program/`), run by pending work and `∂`: + +- `∂` of a dfn runs one program holding its whole call, value and derivative (`derivative::differentiate`). The first call with given argument types, layouts and settings traces it (`program/trace.rs`): the dfn runs on traced values (type and layout, no items); each call plans its result (`plan`) and adds a step via `program::add`. Where the trace stops (`nbs/autodiff.qmd`, "What stops a trace"), the call runs in the interpreter. +- `Programs` keeps each trace's outcome, program or stop, on a dfn's `Definition` or another function's structure (`program::Tacit`, at most `STRUCTURES`). An outcome serves later calls with the same argument signature whose free values still hold what the trace read (`Entry::reread`) → cycling free values replay. Free values changing every call stop tracing after `CHANGES` traces in a row; a function keeps at most `KEPT` outcomes. +- `program::call` replays the program on the call's values; `program/run.rs` replays step by step, the reference compiled code matches. Fusion's and traced programs share this code. +- Index notation and its gradients: `Step::Gather` selects by computed positions; `Step::At` writes or accumulates where a map or selector says; `Step::OverlapAdd` adds a windows copy's cotangent back to its source; `Step::Picks` marks each lane's `⌈`/`⌊` fold pick. Level 2: rustymath's selection planning (`structure::joined`) reads an index array with affine positions through strides, not an offset table (`Table::affine`). +- `program/contraction.rs`: a `+` fold of a `×` whose operands read sources through affine maps runs as one product (`contracted`); a fold of a copy repeating items as a fold of its source (`fold_of_copy`). `program/layout.rs`: stores maps in the order operands and readers need (`layout::ordered`). `blocks::plan`: finds runs of elementwise steps for block stages. `program/verify.rs`: checks each program's contracts before it runs, in every reference case and page (`settings::verifies`). +- `•explain` lists programs a call built or replayed, and stopped traces (`programs`, `stopped`, `declined`). `["sites":$t]•explain` fills `site` on `forced` and `stopped` rows: the innermost basedpl functions that computed the items or stopped the trace (`explain::site`, from a Rust backtrace, skipping the event's own bookkeeping, `BOOKKEEPING`). Use it to place a forced read or a trace stop, rather than a temporary backtrace. + +Batched maps (`batch.rs`): + +- `batch::tried` (from `each_pair`): begins a batched level (`transform::Kind::Batched`), wraps each framed argument in a `Batch` carrier at it, calls the operand once, ends the level. A map first lays its arguments over its frame by its own agreement, the pairing its per-cell calls read (`batch::Arguments::laid`, `agreement::laid_cells`). One cell of atoms: no batched call (faster). +- Routing sends calls whose newest level is the run's to `batch::call`: primitives, operations primitives name (a fold by a primitive, `+.×`), operations by dfns. `⍕` and random draws keep their own code; other system functions run per cell, results joined into one batch (`batch::system`, `batch::each_cell`). `batch::call` runs whole arguments by Rank's routes (`eval::whole_cells` via `batch::whole_call`; `eval::folded_cells` for folds) → a primitive batches wherever Rank runs it whole. +- Folds and scans `folded_cells` misses (e.g. by a dfn) run the fold's own loop over the batch's items, one call per stage on item `j` of every cell (`Batch::at`); `fold_array`, `primitive_fold` join stage batches into one (`joined`). Every fold and scan route decides structural cases (unit, empty lanes, result shape) in `FoldPlan` and ends in `folded`, the only place giving the result its layout, fused folds included. Folds and scans by non-primitives over several lanes try their own batched run: `operators::by_lanes` moves the fold axis last and runs the derived fold over lanes via `batch::tried`; `operators::by_triples`, for `f.g` with non-primitive `g`: one batched `g` call on a frame of rows by contraction by columns, then the `f` fold along the contraction, per chunk of rows whose triples hold at most `batch::CHUNK` items (`operators::by_rows`, shared with products of primitives). A try giving `None` → the usual loop from the start. A fold keeps its stops on its operand's `Definition` (`Function::remembers`). +- Stops: `batch::tries` holds every try's conditions. A call no route covers stops the run (`transform::Stop`). Constants with axes that, spread over the frame, would exceed both 4× the arguments' items and `batch::CHUNK` (2²¹) items stop it, naming a chunk of cells whose share is at most `CHUNK` (`oversized`, `Stop::chunk`); `tried` then runs the operand once per chunk (`batch::chunked`), each a batched level on a one-axis frame, joining results as the per-cell run would. A chunk stopping for size restarts with its own smaller chunk. One cell's share alone over `CHUNK` → one call per cell. +- Code that must run once per cell asks the levels first (`Levels::effect`): output, input, `⍕` of a carried value, acting system-function code (`Effect::Acts`, via `Context::acts`); a guard or prototype call that would catch an error (`Effect::Catches`); a write outside the map's frames (`Effect::Writes`, via `Levels::record_write`). An observing read (`Value::observed`: a count, a position) stops the run too. +- A guard on a batch of the run splits it (`Session::guard` via `operators::guard`): `batch::truths` gives each cell's Boolean; each side runs once as a guard case on its cells (`Levels::case`), through `⍚`'s `Cells`; batched bindings of the run's frames read at the side's cells (`Session::restricted`, via `batch::read_cells`, `operators::cells_at`); `Cells::assembled` joins the sides. In a case, the run's frame is the case's cells, random draws draw for them, and a write to an enclosing frame stops the run. +- A map inside the run, the run innermost, widens it (`batch::Entry::Widen`): a case with no definition, frame = the run's frame then the map's; the run's batches among the map's arguments lay over it (`batch::laid_over`); bindings made before the widening lay over it when read (`Session::read_binding`). A widened run that stops, or an Each with array results, falls back to per-cell calls inside the run. A dfn guard already splitting cells in another frame stops the run as recursion (`Cause::Recursion`); sides differing in layout or kind stop it as a call with no rule. +- Each stop carries its cause (`transform::Cause`), setting how long a dfn operand remembers it (`Cause::kept`): effect, write, observing read, recursion: always; no covering route: while later calls repeat argument kinds and cell shapes; memory guard: while they also repeat the frame shape; caught error: never (it depends on cell values). `Definition::remember` holds `ALWAYS` or an argument signature (`batch::signature`), each with the cause (`batch::holding`). A covered later try falls back without calling the operand, with an `"an earlier stop"` event naming the cause. A stop's `fallbacks` event names its cause (`Cause::reason`) and, for an uncovered primitive call, the glyph (`Stop::refusing`). +- Code with no batch rule reading a batch's items gets item carriers (item i of every cell) and records an `itemwise` row, route `"batched items"`. `explain::itemwise`: the one helper for item-at-a-time routes; the innermost call claims the counts (`Function::reported`), as for `forced`. A kernel then reading a number meets the stop `Carrier::computed` gives a batch → a missed reader costs speed or a fallback, never a wrong result. +- A batched run can also start inside a `∂` call: its level is newer than the `∂` tag → batch rules take each call first; their whole-array calls reach the derivative rules. A batch's whole can carry a dual (kind checks read `Value::template_dtype`). Stacks of matrices whose wholes carry a dual take derivative rules via `Pairing::Matrices` (`eval::whole_cells`): stacked kernels read compact storage. `∂` inside the run differentiates by batches of atoms or arrays: `derivative::numbers` counts a cell's numbers from the batch's template; all cells share each direction's seed; the Jacobian joins into one batch (`batch::joined`). A planless move on a dual whose primal is a batch runs on primal and tangent apart (`derivative::apart`). Adjoints a transpose adds into a batch add as one sum on its whole (`batch::scatter_add`). An attached rule runs on the whole batch at its argument's `∂` tag (`batch::call`); `batch::split_whole`, `batch::dual_of_wholes` keep the newest level outermost. +- Random: a map's draws read streams that the outermost map's count, each map's place, axis by axis, and the draw's place among its cell's draws pick (`distribution::map`, `distribution::cell`, `rustymath::random::Key::folded`); a batched run's cells take their places from its frame. Batched and per-cell runs draw the same numbers, under a splitting guard too; a batched run in chunks that draws falls back to its cells. A batched try outside any map that falls back restores each generator's count (`distribution::settle`). +- The result must be a batch at the run's level, a strand of batches whose kinds join (`joined`), or a carrier-free atom or compact array as every cell's result; else fallback. A pervasive call stops where its result kind differs from the one kind a route gives every item (`pervasive::expected`) → each cell keeps its per-cell kind. +- `reference::check` runs each reference case with `batch` off, on, and on under fusion's small sizes (`fusion::Sizes::SMALL`), comparing storage kinds with the first run's. + +Folds: rustymath owns numeric folds, scans, adjacent pairs, grouped reductions and integer bounds; basedpl selects kernels, tolerance, seeds, storage widths and exact fallbacks. Rustymath's `facts!` table gives, via `pervasive::folds` and `truth_fold`, the reduction folding each primitive, the object by whose rule each takes characters, the comparison objects `with_comparison!` makes, and the product kernel each pair `f.g` names (`pervasive::known_product`). Folds by algebraic float operations can regroup. Contiguous subtraction and division folds use `Alternating`; quotients fall back to item-by-item folding if their products leave the normal range. Boolean scans compose truth-table maps; comparison scans compute both possible totals in bulk and use `reduce::chosen`. Seeds, axis lanes and scalar fallback keep the language's accumulation direction. Generic reductions stay right-associated, keeping operand call order and effects; exception: monadic `f.g` memoizes each minor's expansion, and a cached minor repeats no operand calls or effects. Determinant and permanent fast paths use the appropriate rustymath backend. + +Threads: the automatic Rayon scheduler serves rustymath's elementwise `many` and `pairs`, broadcasts via `agreement::blocks`, folds via `reduce::along`, block stages and matrix products. Scans don't split. Decompositions split in their blocked updates' products, their Hermitian and bidiagonal reductions' passes, and their divide-and-conquer merges. + +- `rank` sends dyadic `+.×` and `⌹`, monadic `⌹` and `-.×`, and `•decompose` over the 2-cells of an unkeyed compact stack to `primitive::stacked` and `stacked_decompositions`, each cell through the rustymath function it uses alone. A pervasive operand, or Rank nested down to one, runs as one call: each argument gets length-1 axes after each level's frame → leading-axis agreement pairs frames with frames, cells with cells. An argument with a named frame axis goes through a batched map instead, whose frame carries no names → names pair only between cells (`batch::tried`). Rustymath's `parallel::stack` splits whole cells across threads in rounds, with its interrupt check between rounds. +- `Context::check` reads the clock only on every 64th call → a caller whose checks run far apart calls `check_now`. +- The scheduler adapts to call cost: `RAYON_NUM_THREADS` by default; one process-wide override via `parallel::set_threads` (0 default, 1 serial), which `•prefs ["threads":n]` writes directly. The timing harness sets one thread after each settings reset. Nested Rayon work doesn't split again; wasm is serial. +- A block stage splits in pieces of whole blocks: elementwise via `parallel::extended`, folded via `reduce::fold_pieces`. On one thread an elementwise stage appends straight to its result (copying each block into a piece's slots cost up to 1.2 copies there). +- Small converted chunks limit the scheduler's work: don't infer multicore execution from a large array alone. + +Products: a pair with a rustymath product kernel (`+.×`, `⌈.+`, `∨.∧`, `+.=`) runs via `linalg::Product::run`, which Python's `inner` also takes (`pervasive::known_product`). Other native pairs: its bounded `inner_with` runner and `reduce::right_fold`, carrying the right tail across blocks; basedpl supplies primitive kernels, tolerance and the exact-widening fallback. An integer product not fitting 64 bits gives way to the exact path. + +- `pervasive::inner` takes each operand's `Orientation`; float and complex products read a transposed operand in place, as reverse mode's `product_transpose` needs. +- A float `+.×` whose operand is a pending copy, or a view of one, hands rustymath's `Inner::inner_mapped` the copy's source and map (`fusion::source`, `pervasive::mapped_product`). A `+.×` whose operands are both pending conversions of compact `u8`, `i16` or f16 arrays to a wider type reads those arrays; rustymath's kernel converts each item as it packs it (`fusion::widened`, `pervasive::widened_product`). A dfn program leaves such a conversion pending for its product (`Unit::Pending`). +- Rustymath gives gemm an operand its map reads at strides (a transpose) where it lies (`Mapped::matrix`), and reads any other copy a block of rows at a time, except in a one-column product whose copy's rows are adjacent in storage (a vector, a single-channel image), where it adds each column of the copy where it lies (`linalg::by_columns`). Neither builds the copy. A copy a name holds is read again for each product, not cached. +- Reverse mode reads a windows copy's cotangent back through rustymath's `Transpose` (`linear::read_back`): one read for each position in a window, a correlation, with fills between frames that move by more than 1 (`structure::correlated`), as a transposed strided convolution reads its cotangent. No scatter-add or zero-padded copy of the cotangent is made. +- Structural gather and compress and interval index also reuse rustymath. Measure changes to generic or backend code in both projects: the callers' codegen and layout decide costs a kernel measured alone misses. ## Evaluation and frontend boundaries -`eval_with`/`call_with` install `EvalOptions`. Echo defaults to true; disabling it suppresses implicit display, not explicit output or display commands. Output is ordered `Output { kind, data }` MIME bundles. An optional sink receives events as produced; otherwise the evaluation retains them. Values, functions, operators, outputs and errors remain separate results. +`eval_with`/`call_with` install `EvalOptions`. Echo (default true) off suppresses implicit display, not explicit output or display commands. Output: ordered `Output { kind, data }` MIME bundles, to an optional sink as produced, else retained by the evaluation. Values, functions, operators, outputs and errors stay separate results. -`Execution::begin` puts each evaluation's interrupt handle, deadline and poll in `execution::STOPS`, a static that every thread reads. Its `Running` guard restores the stops of any evaluation it ran inside, such as the one that called `⍎`. `execution::stopped` reads them, and the first evaluation installs it as rustymath's interrupt check (`rustymath::interrupt`). Rustymath's long computations, such as exact matrix functions, primes and stacks of matrices, then stop for basedpl's interrupts and deadlines, on its worker threads as well. A thread without a turn, such as the one Python's `in` runs on, also stops at an interrupt of the running evaluation. +`Execution::begin` puts each evaluation's interrupt handle, deadline and poll in `execution::STOPS`, a static every thread reads; its `Running` guard restores those of an enclosing evaluation (e.g. the one calling `⍎`). `execution::stopped` reads them; the first evaluation installs it as rustymath's interrupt check (`rustymath::interrupt`) → rustymath's long computations (exact matrix functions, primes, stacks of matrices) stop for basedpl's interrupts and deadlines, on its worker threads too. A thread without a turn (e.g. Python's `in`) also stops at the running evaluation's interrupt. -Evaluation code reads the process's one session through `session()`, and takes a `&Span` where it reports errors. Checks at interpreter boundaries and in long loops call `execution::check`, which counts down a static before it reads clocks and flags or calls the poll hook. It takes no span and no session, and code without a turn, such as Python's `in`, calls it too. Individual BigInt operations and bounded native kernels are not preempted. The session's state sits in `Cell`s and `RefCell`s whose methods hand out owned values, so no caller holds a borrow of it while a dfn evaluates (`meta/session.md`). An optional `Input` supplies `⎕` and `•nget "-"`; without one they raise IO. Python uses cooperative cancellation. The process worker can kill an unresponsive process after a grace period, losing its state; requests are never replayed. +Evaluation code reads the process's one session via `session()` and takes a `&Span` where it reports errors. Boundary checks and long loops call `execution::check`: counts down a static before reading clocks and flags or calling the poll hook; takes no span or session; turnless code (Python's `in`) calls it too. Single BigInt operations and bounded native kernels aren't preempted. Session state sits in `Cell`s and `RefCell`s whose methods return owned values → no caller holds a borrow while a dfn evaluates (`meta/session.md`). An optional `Input` supplies `⎕` and `•nget "-"`; without one they raise IO. Python cancels cooperatively. The process worker can kill an unresponsive process after a grace period, losing its state; requests are never replayed. -`Session::call` resolves and calls a function expression on existing arrays without temporary argument bindings. Retained functions use `call_function_with`. `Function::call` gives each error that a function's code raised without a span the call's span. The evaluator gives its own errors the spans of the nodes they report, and a statement gives any error still without a span its own. An error raised outside an evaluation, as by Python's indexing, has no span. Diagnostics display with Unicode width. Python translates spans to character offsets; process JSON keeps bytes. Native function/operator handles cross the in-process Python boundary, not process JSON. +`Session::call` resolves and calls a function expression on existing arrays, with no temporary argument bindings; retained functions use `call_function_with`. Spans: `Function::call` gives each spanless error from a function's code the call's span; evaluator errors take the spans of the nodes they report; a statement gives any still-spanless error its own; errors outside an evaluation (e.g. Python indexing) have none. Diagnostics display with Unicode width. Python translates spans to character offsets; process JSON keeps bytes. Native function and operator handles cross the in-process Python boundary, not process JSON. ### Python -`python/basedpl/__init__.py` owns the shared `bpl` workspace and `Array`/result/error wrappers. An `Array` whose value holds a function as an item is a `FunctionArray`, which `Array._classify` chooses in `__init__` and after `__setitem__`, so `callable()` is false for every other array. `fn` aliases `bpl.fn`; builtins and magics share that workspace. Registering the magics gives it the REPL's display defaults, through `Session::make_interactive`. Plain calls print explicit output and return Array, Function or Operator values. Capturing calls return `Result`. Capture mode is positional-only because keywords bind BPL names. `_Session.eval` and `.call` are the PyO3 entry points; `request` is their internal Rust helper. +`python/basedpl/__init__.py` owns the shared `bpl` workspace and the `Array`, result and error wrappers. An `Array` with a function item is a `FunctionArray`, chosen by `Array._classify` in `__init__` and after `__setitem__` → `callable()` is false for every other array. `fn` aliases `bpl.fn`; builtins and magics share the workspace; registering the magics gives it the REPL's display defaults (`Session::make_interactive`). Plain calls print explicit output and return Array, Function or Operator values; capturing calls return `Result`; capture mode is positional-only (keywords bind BPL names). `_Session.eval`, `.call`: the PyO3 entry points; `request`: their internal Rust helper. + +`functions.py` builds native nodes and lazy exports from Rust metadata. Builtins win name collisions with workspace names; operation names choose valence and currying. `__all__` omits Python-builtin collisions; explicit imports still work. Keyword calls use a keyed left argument. One positional argument stays the right argument unchanged; zero or several form a vector. Operator properties and `_Pending` assemble native nodes, never generated BPL source. `printing.py` reads node parts without evaluating or resolving names. -`functions.py` constructs native nodes and lazy exports from Rust metadata. Builtins win name collisions with workspace names; operation names choose valence/currying. `__all__` omits Python-builtin collisions but leaves explicit imports available. Function calls with keywords use a keyed left argument. A single positional argument stays the right argument unchanged; zero or multiple positional arguments form a vector. Operator properties and `_Pending` assemble native nodes, never generated BPL source. `printing.py` reads node parts without evaluating or resolving names. +`.fn()` keeps a late-bound expression: calls resolve affected nodes against the calling session, sharing unaffected graph nodes → primitive fast paths, inverses and reduction identities survive. The binder still resolves ordinary names during execution. Cyclic resolution: LIMIT error. -`.fn()` retains a late-bound expression. Calls resolve affected nodes against the calling session while sharing unaffected graph nodes. This preserves primitive fast paths, inverses and reduction identities. The binder still resolves ordinary names during execution. Cyclic resolution is a LIMIT error. +Arrays keep native values; `.py` and `.np` copy; NumPy is optional and lazy. -Arrays retain native values. `.py` and `.np` make copies; NumPy is optional and lazy. The native `_Array` buffer export lends compact items read-only. An unbuilt structural copy whose read is affine (`Value::affine`) lends its source's items with strides, so windows reach NumPy without being built. Numeric buffers import with one copy through `rustymath_buffer::read_array`. `Value::imported_buffer` preserves supported item widths. The shared reader normalizes signed bytes, wider unsigned integers and foreign byte order. Sibling snapshots carry keys and names. A NumPy uint64 array containing a value above int64's range imports item by item as exact integers. `own()` moves compact items into a writable NumPy array where nothing else holds them, holding Booleans as `u8` and characters as `i32`, because NumPy can write any bytes. Exact integers/fractions cross through native integers, not decimal strings. String/object arrays convert per item. Python data converts in Rust: the session's `array` (`import` in `py/src/lib.rs`) walks nested lists through `rustymath_buffer::walk`, the walker rustymath's Python package uses, with a collector that nests a level whose items differ in shape. Lists follow the shared import rule, and dictionaries read as literals do. `rustymath_buffer::labels` is the one rule for a dict's keys and a frame's column labels, in both Python packages. A frame reads through `rustymath_buffer::frame` as a keyed vector of its columns, which `.df` turns back into a frame. `None` reads as NaN as an item. A bare `None` doesn't convert, so an operator given one returns `NotImplemented`. `syntax.py` wraps the native syntax tree with character spans and span-editing helpers. +- The native `_Array` buffer export lends compact items read-only. An unbuilt structural copy with an affine read (`Value::affine`) lends its source's items with strides → windows reach NumPy unbuilt. `own()` moves compact items into a writable NumPy array where nothing else holds them, Booleans as `u8`, characters as `i32` (NumPy can write any bytes). +- Numeric buffers import with one copy via `rustymath_buffer::read_array`; `Value::imported_buffer` keeps supported item widths; the shared reader normalizes signed bytes, wider unsigned integers and foreign byte order. Sibling snapshots carry keys and names. A NumPy uint64 array with a value above int64's range imports item by item as exact integers. Exact integers and fractions cross as native integers, not decimal strings. String and object arrays convert per item. +- Python data converts in Rust: the session's `array` (`import`, `py/src/lib.rs`) walks nested lists via `rustymath_buffer::walk`, rustymath's Python package's walker, with a collector that nests a level whose items differ in shape. Lists follow the shared import rule; dicts read as literals do. `rustymath_buffer::labels`: the one rule for dict keys and frame column labels, in both Python packages. A frame reads via `rustymath_buffer::frame` as a keyed vector of columns; `.df` turns it back. `None` as an item reads as NaN; a bare `None` doesn't convert → an operator given one returns `NotImplemented`. +- `syntax.py` wraps the native syntax tree with character spans and span-editing helpers. -`Array(existing)` creates an independent native handle. Assignment and `.keys`/`.names` setters replace the owner's handle without changing prior views or captured functions. Both Python packages read written selectors as `structure::IndexPart` tokens through `rustymath/buffer/src/selection.rs`, and plan them with `structure::plan_parts`, the structural planner that `⌷` also uses. `structure::addressed_parts` routes source groups and expands `Rest` against the applicable frame. On a BPL array, Python's own kinds (`selection::native`: slices, keys, integers and lists of keys) decode in the buffer crate, Boolean masks select where they hold true, and other selectors become BPL values that decode as `⌷` decodes them (`Value::planned`). Pick paths select alone, as `⌷` reads them, keeping the written parts until the ending cell's geometry is known. `_Array.select` and `_Array.assign` use carrier-aware structural selection and writeback. Python assignment uses leading-axis broadcasting. Language writeback retains its own agreement. +`Array(existing)` makes an independent native handle. Assignment and `.keys`/`.names` setters replace the owner's handle, leaving prior views and captured functions unchanged. Both Python packages read written selectors as `structure::IndexPart` tokens via `rustymath/buffer/src/selection.rs` and plan them with `structure::plan_parts`, `⌷`'s structural planner; `structure::addressed_parts` routes source groups and expands `Rest` against the applicable frame. On a BPL array, Python's own kinds (`selection::native`: slices, keys, integers, lists of keys) decode in the buffer crate; Boolean masks select where true; other selectors become BPL values decoding as `⌷` decodes them (`Value::planned`). Pick paths select alone, as `⌷` reads them, keeping the written parts until the ending cell's geometry is known. `_Array.select`, `_Array.assign`: carrier-aware structural selection and writeback. Python assignment uses leading-axis broadcasting; language writeback keeps its own agreement. -Each `_Session` request waits for its evaluator mutex without the GIL, then evaluates on the caller's thread with the GIL released. A separate interrupt handle supports interruption from another thread. Signal polling briefly takes the GIL. Python input and optional streaming-output callbacks also cross at explicit boundary adapters. No Python numeric objects enter core values; arrays can be dropped on any thread without an unsafe Send implementation. +Each `_Session` request waits for its evaluator mutex without the GIL, then evaluates on the caller's thread with the GIL released. A separate interrupt handle allows interruption from another thread; signal polling briefly takes the GIL. Python input and optional streaming-output callbacks cross at explicit boundary adapters. No Python numeric objects enter core values; arrays can drop on any thread without an unsafe Send implementation. ### Process, CLI and notebooks -`protocol.rs` owns JSON request/value/error/output encoding. Requests are objects with fields such as code, call, args and bindings. Exact integers use arbitrary-precision JSON numbers; fractions and complex values have tags. Output bytes use base64. The worker omits eligible keyed entries holding functions/operators and returns `value: null` for other non-exportable values, retaining display output. JSON data serialization is a separate contract and can reject such values. +`protocol.rs` owns JSON request, value, error and output encoding. Requests: objects with fields such as code, call, args, bindings. Exact integers: arbitrary-precision JSON numbers; fractions and complex values: tagged; output bytes: base64. The worker omits eligible keyed entries holding functions or operators and returns `value: null` for other non-exportable values, keeping display output. JSON data serialization is a separate contract and can reject such values. -`cli/src/worker.rs` processes JSON lines in order. A separate reader handles interrupt controls but never evaluates BPL. IDs are optional, and bad requests don't end the session. `python/basedpl/worker.py` owns process lifetime, deadlines and hard-kill fallback. Protocol serialization walks native values directly rather than building an intermediate JSON value tree for every reply. +`cli/src/worker.rs` processes JSON lines in order; a separate reader handles interrupt controls, never evaluating BPL. IDs optional; bad requests don't end the session. `python/basedpl/worker.py` owns process lifetime, deadlines and hard-kill fallback. Protocol serialization walks native values directly, with no intermediate JSON tree per reply. -CLI `-e`, files and REPL stream outputs to stdout. Failed writes interrupt execution; a closed pipe exits successfully. `-e`, files and interactive input provide `StandardInput`; program text read from stdin does not. The REPL parses for completeness without evaluating twice. Rustyline owns terminal editing/history/modes; only typed backtick sequences auto-expand. Paste/history/navigation cancel automatic expansion. Shared symbol metadata supplies completion/highlighting. Keep pseudo-terminal tests for Ctrl-C/Ctrl-D and paste behaviour. +CLI `-e`, files and REPL stream outputs to stdout; a failed write interrupts execution; a closed pipe exits successfully. `-e`, files and interactive input provide `StandardInput`; program text read from stdin doesn't. The REPL parses for completeness without evaluating twice. Rustyline owns terminal editing, history and modes; only typed backtick sequences auto-expand; paste, history and navigation cancel expansion. Shared symbol metadata supplies completion and highlighting. Keep pseudo-terminal tests for Ctrl-C, Ctrl-D and paste. -The Jupyter adapter in `cli/src/kernel.rs` uses kernmini and a `ThreadWorker`. It keeps transport/interrupts responsive, maps explicit output to streams and implicit output to results, and routes notebook input through `input_request`. The wheel and local install use the same kernelspec. `inspection.rs` embeds the glyph/system pages for help and uses the same names as the REPL and Python. Change names and shortcuts in `symbols.rs`/shared layout data, not each frontend. +Jupyter adapter (`cli/src/kernel.rs`): kernmini and a `ThreadWorker`; keeps transport and interrupts responsive; explicit output → streams, implicit → results; notebook input via `input_request`. Wheel and local install share the kernelspec. `inspection.rs` embeds the glyph and system pages for help, under the REPL's and Python's names. Change names and shortcuts in `symbols.rs` and shared layout data, not per frontend. ### Browser -`cargo wasm` builds an incremental release-profile npm package with wasm-bindgen glue, language/input assets, grammar and canvas support. `--out DIR` leaves the docs' local playground assets alone; `--strip` removes function-name metadata for distribution. Keep wasm-bindgen glue and crate versions aligned through the workspace lock. Avoid fat LTO here because rebuild cost and enlarged stack frames conflict with browser development and recursion limits. Liftoff, V8's first compiler, gives each wasm local its own stack slot, whether or not the path that uses it runs. So a function that recursion passes through keeps its general path out of line, as Each, Rank and the outer product do (`each_items`). For the same reason, `ErrorKind`, which the `Result` in every frame holds, stays at 16 bytes in wasm32 (`transform::Stop`). To find a frame that grew, read the function names in the stack of the `RangeError` at an overflow, then compare those functions' wasm locals between builds. `wasm-objdump -h DIR/basedpl_wasm_bg.wasm` lists the module's section sizes, including `name`. `twiggy diff` matches functions by full name, including crate hashes that change between builds. To compare two builds by function, compare their `twiggy top -n 1000000 -f json` listings with the hashes removed. +`cargo wasm`: an incremental release-profile npm package with wasm-bindgen glue, language and input assets, grammar and canvas support. `--out DIR` leaves the docs' local playground assets alone; `--strip` removes function-name metadata for distribution. Keep wasm-bindgen glue and crate versions aligned through the workspace lock. Avoid fat LTO here: its rebuild cost and larger stack frames conflict with browser development and recursion limits. -Browser `Session.run` streams MIME events and returns a protocol error or null. `eval` transfers values through `js.rs` but discards output and cannot export BPL functions/operators; `bind` imports page values/functions. Typed arrays transfer without another page-worker copy. JavaScript functions live in a thread-local registry and core function nodes hold slots, preserving Send/Sync values. `host_web.rs` implements the browser's clock, synchronous I/O/fetch and RegExp behaviour. File/URL reads resolve against the configured base. Unsupported native filesystem/locale operations stay unavailable; browser `•nput` writes only stdout and `•js` is browser-only. +Stack frames: Liftoff, V8's first compiler, gives each wasm local its own stack slot, used or not → a function that recursion passes through keeps its general path out of line, as Each, Rank and the outer product do (`each_items`), and `ErrorKind`, held by every frame's `Result`, stays 16 bytes in wasm32 (`transform::Stop`). In wasm, `Binder::reduce` and `Bindings::enter` stay out of line, keeping their locals out of the frames held at every recursion level. To find a frame that grew: read the function names in the stack of the overflow's `RangeError`, then compare those functions' wasm locals between builds; TurboFan frame size per function, in "stack slots": `node --no-liftoff --print-wasm-code-function-index=N`. `wasm-objdump -h DIR/basedpl_wasm_bg.wasm`: section sizes, `name` included. `twiggy diff` matches functions by full name, including crate hashes that change between builds → compare two builds' `twiggy top -n 1000000 -f json` listings with hashes removed. -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, otherwise import the same public module from jsDelivr. Restarting a trapped/stopped worker loses its state. `worker.js`, `page.js` and `gallery.js` own their respective protocols/UI; `nbs/driver.js` documents canvas drawing data. Gallery demos are `.bpl` sources with shared preferences, checked natively by `language_examples`. Browser recursion tests cover both V8 compilers and fail with LIMIT before stack exhaustion. +Recursion limits: CI's two `wasm` jobs (`Test`, `Speed`) take Node's version from `.node-version`: TurboFan frame sizes vary between V8 versions enough to decide whether a recursion test overflows (one build's `each` shape: 0.68 MB for 171 levels under Node 24, 0.62 MB under Node 26). `tests/wasm.mjs` checks depth with Node's 192 KB reserve plus 80% of a Chrome worker's roughly 470 KB, 0.5575 MB in all → frames growing into the remaining 20% fail it. CI's x86-64 V8 takes more stack per level than Apple silicon's: `MAX_CALL_DEPTH` (`src/eval.rs`), the browser's depth limit, allows levels up to 1.48 times their Apple-silicon size. `basedpl.bpltests.wasm_stack(DIR)` measures each recursion shape's stack per level and below its first level, the figures the limit is set from; each trial runs in a fresh `node` process whose worker compiles the module, as the test's does (code compiled once and shared across workers gets different frames). -CI's two `wasm` jobs, in the `Test` and `Speed` workflows, take Node's version from `.node-version`. TurboFan's frame sizes vary between V8 versions by enough to decide whether a recursion test overflows. On 28 October 2026, one build's `each` shape needed 0.68 MB of stack to reach 171 levels under Node 24's TurboFan, against 0.62 MB under Node 26's. `tests/wasm.mjs` runs its depth checks with Node's 192 KB reserve plus 80% of the roughly 470 KB that a Chrome worker gives, 0.5575 MB in all. A build whose frames grow into the remaining 20% fails the test. CI's x86-64 V8 takes more stack per level than Apple silicon's. The browser's depth limit, `MAX_CALL_DEPTH` in `src/eval.rs`, allows for levels up to 1.48 times their size on Apple silicon. `basedpl.bpltests.wasm_stack(DIR)` measures the stack that each recursion shape takes per level and below its first level. The limit is set from those figures. In wasm, `Binder::reduce` and `Bindings::enter` stay out of line, to keep their locals out of the frames that stay on the stack at every level of recursion. To find a frame that grew, print each function's TurboFan frame size, in "stack slots", with `node --no-liftoff --print-wasm-code-function-index=N`. `wasm_stack` runs each trial in a fresh `node` process whose worker compiles the module, as the test's worker does. Code compiled once and shared across workers gets different frames. +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. ## System functions and rendering -`system.rs` owns the system table: one `SystemFunction` for each system function, holding its case-insensitive name, call, valence, extension, result prototype and inverse. Its extension (`primitive::Extension`) is the field that each primitive's form holds too. A system function takes its right argument whole, or takes items (`primitive::Item`) and maps over a right argument that holds several, with its left argument whole for each. A function that takes text also maps over each row of a character matrix, as `⍎` does. A function that acts asks the running levels where it acts (`Context::acts`), as output and input do: where it reads or writes files, the network, the clock or the terminal, or makes something new on each call, as `•index` and `•rand` do. Any other system function asks nothing, and a batched run calls it once for each cell. The parser resolves a known system name to its table entry (`system::Entry`) through `Builtin::find`, which finds a glyph's primitive too, and `primitive::Builtin` holds either. A system name is therefore a `NodeKind::Function` node, compiles to a constant and resolves to the function that one process-wide table holds for each builtin (`Builtin::function`), as a glyph does. An unknown system name stays a `NodeKind::System` node, which is a VALUE error when evaluated. Code identifies a table function by the `Entry` that its `SystemFunction` records, as in `Entry::PREFS`, never by its name. A function that a factory made, such as `•element "div"`, has no entry, although it shares its factory's name. Native records use ordinary function nodes and calls. Empty native item mapping returns its declared prototype without calling the function. `data::Options` is the shared keyed-option decoder; formats don't invent separate option parsers. +`system.rs` owns the system table: one `SystemFunction` per system function: case-insensitive name, call, valence, extension, result prototype, inverse. Its extension (`primitive::Extension`) is the field each primitive form holds too. A system function takes its right argument whole, or takes items (`primitive::Item`), mapping over a right argument holding several, with the left argument whole for each; one taking text also maps over each row of a character matrix, as `⍎` does. + +- A function that acts asks the running levels where it acts (`Context::acts`), as output and input do: reading or writing files, network, clock or terminal, or making something new each call (`•index`, `•rand`). Others ask nothing; a batched run calls them once per cell. +- The parser resolves a known system name to its table entry (`system::Entry`) via `Builtin::find`, which also finds glyph primitives; `primitive::Builtin` holds either → a system name is a `NodeKind::Function` node, compiles to a constant and resolves to the function a process-wide table holds per builtin (`Builtin::function`), as a glyph does. An unknown system name stays a `NodeKind::System` node: VALUE error when evaluated. +- Code identifies a table function by its `SystemFunction`'s `Entry` (`Entry::PREFS`), never by name. A factory-made function (`•element "div"`) has no entry, though it shares its factory's name. +- Native records use ordinary function nodes and calls. Empty native item mapping returns its declared prototype without calling the function. `data::Options`: the shared keyed-option decoder; formats invent no option parsers of their own. -`optimize.rs` implements `⇄` on `rustymath::optimize`. The left argument holds the options. When the method can use a derivative (none named, L-BFGS, Levenberg–Marquardt, CG or 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`. When the search runs L-BFGS, each value comes from a linearisation that `gradient` then transposes at the same point (`linearized_at`), so a value and its gradient run the function once. With `"solve":$t`, `Residual` is the `Mapping` that `solve` takes, or that `solve_affine` takes with `"linear":$t` as well. Where `∂` differentiates the function, `Residual` gives JVPs and vector–Jacobian products from one record at each point, or from the start's record for an affine function. At a later point where `∂` gives an UNSUPPORTED error, `Residual::jvp` declines, and rustymath takes forward differences for that Jacobian and later ones. A square linear system takes no products with the transpose, so it runs GMRES on A, not the normal equations. The function in option `precondition` gives the preconditioner. +`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 triggers float promotion when every number converts losslessly. Finite fills alone do not trigger it. Otherwise items keep their kinds. JSON objects and Python dicts read as literals do, through `keyed::vector` and `Value::from_parts`. Binary data uses exact bytes. Writes validate before opening and create exclusively unless overwrite is explicit. CSV/JSON/XML exports use `•csv⁻¹`, `•json⁻¹` and `•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/replace native functions; hosts retain their own regex syntax. `xml.rs` represents keyed element trees and distinguishes XML escaping/validation from HTML raw script/style and void-element rules. SVG uses its own renderer. `image.rs` owns pictures, their options and animation timing, as plain pixel buffers. Its `codec` module decodes and encodes those buffers through ImageIO on Apple targets (`image/imageio.rs`) and through the `image` and `png` crates elsewhere (`image/codecs.rs`). Native/browser renderers use the MIME/byte formats their frontends support. `plot.rs` stores specs and draws SVG on demand; the renderer owns defaults/validation. Rendering options belong 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; 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. -`display.rs` owns plain/boxed/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 the items, not their storage: `Value::shown_dtype` (`array/text.rs`) gives the dtype that compact storage of the items would take, by `storage::items_dtype`'s rule for items, which the shared `ₓ` or width marker, plain display's choice between brackets and a strand, the bracketed grid that plain display and `⍕` share (`display::table` and `display::bracketed`), a keyed table's corner and the boxed marker all read. `•mime` calls a renderer with echo disabled and cancellation active. Failed implicit rendering falls back to text; interrupts/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 as separate contracts. +`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. ## Tests and reference data -`tests/core.rs` checks public storage/layout/API behaviour, ownership, parser diagnostics and recovery. Use `equiv_in!` for session workflows and `fails_in` for errors; each expected expression runs in a fresh session after its code, under the settings that the code leaves. `tests/core.rs` never changes settings. The settings are one store for the whole process, so the checks that change them live in `tests/settings.rs`, which runs them in turn in one test, each from the defaults. Both binaries share the helpers in `tests/common/mod.rs`. Keep constructors for representation/prototype/buffer checks. Self-contained language cases belong in `tests/reference/core.bpl`. `cli/tests/cli.rs` and Python's pseudo-terminal tests exercise the installed process/editor boundary. +`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. + +`reference.rs` serves Rust tests and worker requests. -`reference.rs` serves Rust tests and worker requests. Every reference case uses a fresh session; documentation examples share a session within each BPL code block, which starts from the default settings. Each page starts with no kept plans or compiled kernels, as a new process does (`program::forget_plans`, `jit::forget_kernels`), so that an example that shows block counts or compiles gives the same output whether its page runs alone, as under `check_page`, or after other pages, as under `language_examples`. A reference file's leading `•prefs` line sets the settings of all its cases. An optional `⍝⍝ setup` supplies shared definitions, initialized independently for each run and its expectation. A section's inline `•prefs` suffix runs before setup. Each case runs five times, with the `batch` pref off, then on, then on with fusion's small sizes, under which every block stage compiles its parts, then the same with no compiled parts, then on with its code as the body of a dfn that one session calls twice (`twice`), and each run starts from the defaults and the file's header. 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, and each node's description is the one that the shared planners give its step on its operands. A program that breaks one of these panics with its listing. The run with no compiled parts takes place only where the run before it used one (`blocks::compiled_runs`). The last run checks that a dfn's second call, which runs the statements its first call recorded, gives the first call's value, storage kind, error and output. Code that can't be a dfn's body skips it. `tests/reference.rs` checks the cases in a child process of itself for each core, and Python's `check_file` in a worker for each core, because a process evaluates one case at a time. `batched` leaves out the run with the `batch` pref off, `unfused` the runs with the small sizes, and `compiled` the run with no compiled parts, for a case whose value says whether a stage compiled. Each expectation evaluates after its code, under the settings that the code leaves. A float matches its expectation within 1e-13, or within 8 units in the last place of its own width where that's greater, unless the case's 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. A local expectation also fixes where storage is mixed: the result must be held in mixed storage exactly where the expectation is, at each level of nesting (`kind_difference`), unless the case's header carries `mixed`. Pending gaps belong in inventory JSONL, not passing error tests. See `tests/reference/README.md` for format and review/activation workflow. `python/basedpl/reference.py`, `bpltests.py` and `scripts/reference.py` own import, capture, inventory and worker checks. `lib/*.bpl` are readable dfn examples; modules import other library files with private relative names. +- 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`). +- 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 five times, each from the defaults and the file's header: `batch` off; on; on with fusion's small sizes, every block stage compiling its parts; the same with no compiled parts, only where the previous run used one (`blocks::compiled_runs`); on, with its code as a dfn body one session calls twice (`twice`), checking that the second call, which runs the statements the first recorded, gives the first's value, storage kind, error and output (skipped for code that can't be a dfn body). Header flags drop runs: `batched` the `batch`-off run; `unfused` the small-size runs; `compiled` the no-compiled-parts run, for a case whose value says whether a stage compiled. +- 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. +- `tests/reference.rs` checks cases in a child process of itself per core, Python's `check_file` in a worker per core: a process evaluates one case at a time. +- 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. -`tests/scaling.rs` measures time/allocation growth and result-size allocation budgets. `tests/speed.rs` reads native/browser budgets from `speed/*.bpl` through `timing.rs`; the worker and Python measurement tools reuse it. Both tests register rustymath's memory guard and turn on its checks (`memory::set_checks`) in processes that run one case at a time, `tests/reference.rs` in its children: a reference case that leaves more bytes held in large allocations than it found, on each of two runs, fails as a leak, and so does a speed case whose timed runs leave more than its first run left. A free or reallocation that passes more bytes than its block holds aborts with a backtrace. `tests/units.rs` reads existing incremental object files and checks unit size without building. Python tests cover conversion/cancellation/frontend behaviour, not a second Rust semantic suite. Combine related checks into readable tests. +`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. `tests/units.rs` reads existing incremental object files, checking unit size without building. Python tests cover conversion, cancellation and frontend behaviour, not a second Rust semantic suite. Combine related checks into readable tests. ## Documentation and generated assets -`nbs/index.ipynb` generates the homepage and README. Reference pages in `nbs/glyphs/` and `nbs/system/` describe forms/options/errors; concept pages explain them. `sidebar.yml` owns navigation. Edit source notebooks/pages, not generated outputs. `magics.ipynb` and `dyalog.ipynb` export Python modules; other Python modules are hand-written. Cargo owns versions. `nbs/llms.txt` is the documentation's index for LLMs: an entry for each page, with how BPL differs from Dyalog, an example and the page that defines it. `src/inspection.rs` includes it in the build, so `]help llms` shows it after `cargo develop`, and the project's CLAUDE.md includes it in every session's instructions. +`nbs/index.ipynb` generates the homepage and README. Reference pages (`nbs/glyphs/`, `nbs/system/`) describe forms, options and errors; concept pages explain them. `sidebar.yml` owns navigation. Edit source notebooks and pages, not generated outputs. `magics.ipynb` and `dyalog.ipynb` export Python modules; other Python modules are hand-written. Cargo owns versions. `nbs/llms.txt`: the docs' index for LLMs: per page, how BPL differs from Dyalog, an example, the defining page. `src/inspection.rs` builds it in (`]help llms` after `cargo develop`); the project's CLAUDE.md includes it in every session's instructions. ```bash bpl-nb @@ -368,18 +532,24 @@ 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/figures and `nbdev-test` for Python notebook code. Rust `language_examples` checks BPL page/gallery examples; pytest checks Python page examples. Use `BASEDPL_PAGE` to focus 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, as "Release" describes. +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"). -`cargo develop` and `ws-sync` run `basedpl.editors` in a fresh Python process after installing the extension and before copying wheel data. It generates grammars/highlighters, editor templates, keyboard bundles and glyph metadata, including Rust-derived `browser/language.js`. JavaScript UI changes are authored modules read directly by Vite: 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` supports generation without macOS. `basedpl.keyboards.capture()` refreshes native data when needed, and `keyboards.write()` generates regional macOS bundles/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. See `editors/jetbrains/README.md` and `editors/windows/README.md` for target-specific work. Rebuild the common icon only when its design changes. +`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. -`nbs/bpl.xml`, the APL/BQN grammars and vim rules share generated glyph classes. `hpl.xml` includes BPL and adds Hanzi aliases from the same symbol table. Use `hpl` fences for Hanzi highlighting, `bpl` for Hanzi names. For Hanzi notebooks, set `kernelspec.language` to `hpl` and keep `kernelspec.name` as `bpl`. These labels do not enable runtime Hanzi aliases. TextMate exports feed Monaco/Shiki. `fonts.css` owns the BPL/APL font choices; rerun affected magics after changing them. `tests/highlighting.mjs`, `input.mjs`, keylayout/Windows tests and glyph-page checks cover their respective generated interfaces. All artifact generation belongs in repository functions/scripts, not ad hoc kernel transformations. +`nbs/bpl.xml`, the APL and BQN grammars and vim rules share generated glyph classes. `hpl.xml` includes BPL and adds Hanzi aliases from the same symbol table. Fences: `hpl` for Hanzi highlighting, `bpl` for Hanzi names. Hanzi notebooks: `kernelspec.language` `hpl`, `kernelspec.name` `bpl`; these labels don't enable runtime Hanzi aliases. TextMate exports feed Monaco and Shiki. `fonts.css` owns BPL and APL font choices; rerun affected magics after changing them. `tests/highlighting.mjs`, `input.mjs`, keylayout and Windows tests and glyph-page checks cover their generated interfaces. All artifact generation lives in repository functions and scripts, not ad hoc kernel transformations. ## Release -Before approval, verify successful tests for the release commit and the relevant installed artifacts. Reserve isolated wheel installs for packaging-sensitive work or release preparation, using a small relevant subset of existing tests. `ship-rs-build --profile dist` builds the distributed configuration; bare `ship-rs-build` uses release for local checks. +Before approval: verify successful tests for the release commit and the relevant installed artifacts. Isolated wheel installs only for packaging-sensitive work or release preparation, with a small relevant subset of existing tests. `ship-rs-build --profile dist` builds the distributed configuration; bare `ship-rs-build` uses release, for local checks. + +Before each release, run `bpl-tests prerelease` from basedpl's root, in a shell, not a kernel, while no one else edits either repository. It runs both repositories' release checks and saves: + +1. Builds, tests and saves, in parallel, each step starting once the steps whose output it reads pass: in each repository `cargo develop`, `pytest -q`, `nbdev-prepare`, and `cargo test --profile checked` in its own target directory. basedpl's development build includes editor generation; its Node tests, notebooks and `target_builds` wait for that data; browser tests follow `target_builds`. +2. Timings, one at a time, nothing else running: `cargo test` in each repository with `_COSTS` set, rustymath's Python speed test, `thread_scaling`, the browser speed test. +3. `nbdev-clean` in both repositories, even after a failure. -Before each release, run `bpl-tests prerelease` from basedpl's root, in a shell rather than a kernel, while no one else is editing either repository. It runs both repositories' release checks and saves. First come the builds, tests and saves, in parallel, with each step starting once the steps whose output it reads have passed. In each repository these are `cargo develop`, `pytest -q`, `nbdev-prepare`, and `cargo test --profile checked` in a target directory of its own. basedpl's development build includes editor generation; its Node tests, notebooks and `target_builds` wait for that fresh data. Browser tests follow `target_builds`. Then the timings run one at a time, with nothing else running: `cargo test` in each repository with `_COSTS` set, rustymath's Python speed test, `thread_scaling` and the browser speed test. Last, `nbdev-clean` runs in both repositories, even after a failure. The first failure ends the run, unless `--keep-going` is given. The summary gives each step's status, time and log. `--dry-run` lists the steps in their order without running them. Before it starts, the run stops if `node_modules` or the Node version that `.node-version` names is missing. It also notes when basedpl needs an unreleased rustymath version, because rustymath then ships first. The run never commits, pushes or tags. +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. It stops before starting if `node_modules` or the Node version `.node-version` names is missing, and notes when basedpl needs an unreleased rustymath version (rustymath then ships first). It never commits, pushes or tags. -The run's `cargo test --profile checked` uses the test profile with debug assertions on, which adds `session()`'s turn check and the standard library's checks in unsafe code. Those checks cost 12 to 55% per call, too much for the test profile that the budgets measure. The speed and scaling tests mark themselves ignored under debug assertions, so that run checks correctness only. +`cargo test --profile checked`: the test profile with debug assertions on, adding `session()`'s turn check and the standard library's checks in unsafe code. They cost 12-55% per call, too much for the test profile the budgets measure; the speed and scaling tests mark themselves ignored under debug assertions → that run checks correctness only. -Confirm the clean tree and `[workspace.package].version`, then run `ship-release` without flags. Python gets that version through `dynamic = ["version"]`. Fastship pushes/tags the current version, leaves publication to the Release workflow and bumps Cargo without rebuilding. There is no changelog step. Never commit, push, tag or publish without approval. +Confirm the clean tree and `[workspace.package].version`, then run `ship-release` without flags. Python gets that version via `dynamic = ["version"]`. Fastship pushes and tags the current version, leaves publication to the `Release` workflow, and bumps Cargo without rebuilding. No changelog step. Never commit, push, tag or publish without approval. diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 2fb702eb..05718269 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -128,12 +128,12 @@ pub(crate) fn run(args: &[String]) -> i32 { let (args, program_args) = args.split_at(program); let stdin = io::stdin(); let stdout = io::stdout(); - let stderr = io::stderr(); - let mut err = stderr.lock(); + // Each write locks stderr and stdout for itself. A lock held through an evaluation would block every write from a pool thread, such as a panic's report, while this thread waits for the pool: the process would hang. + let mut err = io::stderr(); let result = match args { [] => repl(&mut err, stdin.is_terminal() && stdout.is_terminal()), - [flag] if flag == "--worker" => crate::worker::run(&mut stdout.lock()).map(|_| 0), - [flag, file] if flag == "--costs" => costs(file, &mut stdout.lock(), &mut err), + [flag] if flag == "--worker" => crate::worker::run(&mut io::stdout()).map(|_| 0), + [flag, file] if flag == "--costs" => costs(file, &mut io::stdout(), &mut err), [flag, code] if flag == "-e" => expression(Source::new("", code.as_str()), true, program_args, &mut err), [flag] if flag == "--help" || flag == "-h" => write!(stdout.lock(), "{USAGE}").map(|_| 0), [flag] if flag == "--version" => writeln!(stdout.lock(), "basedpl {}", env!("CARGO_PKG_VERSION")).map(|_| 0), diff --git a/cli/src/kernel.rs b/cli/src/kernel.rs index bb9b3d32..94dac9dd 100644 --- a/cli/src/kernel.rs +++ b/cli/src/kernel.rs @@ -19,11 +19,19 @@ fn language_error(error: basedpl::Error) -> LanguageError { fn expression(session: &Session, code: &str, interrupt: &InterruptHandle) -> Result { let result = session.eval_with(code, EvalOptions { interrupt: interrupt.clone(), echo: false, output: Some(Arc::new(|_| {})), ..EvalOptions::default() }); if let Some(error) = result.error { return Err(error); } - let (kind, data) = if let Some(value) = result.value { - (Some("array"), json!(session.render(&value, interrupt.clone())?)) - } else if let Some(f) = result.function { (Some("function"), json!({"text/plain": f.bpl()})) } - else if let Some(op) = result.operator { (Some("operator"), json!({"text/plain": op.to_string()})) } - else { (None, json!({})) }; + let item = if let Some(value) = result.value { Some(("array", value)) } + else if let Some(f) = result.function { Some(("function", basedpl::Value::Function(f))) } + else { result.operator.map(|op| ("operator", basedpl::Value::Operator(op))) }; + let (kind, data) = if let Some((kind, value)) = item { + let value = value.read(basedpl::ReadKind::Value)?; + session.raise()?; + let data = match value { + basedpl::Value::Function(f) => json!({"text/plain": f.bpl()}), + basedpl::Value::Operator(op) => json!({"text/plain": op.bpl()}), + value => json!(session.render(&value, interrupt.clone())?), + }; + (Some(kind), data) + } else { (None, json!({})) }; Ok(json!({"status": "ok", "data": data, "metadata": kind.map_or(json!({}), |kind| json!({"type": kind}))})) } diff --git a/cli/src/worker.rs b/cli/src/worker.rs index ba9a8017..be7fa7df 100644 --- a/cli/src/worker.rs +++ b/cli/src/worker.rs @@ -56,14 +56,18 @@ impl serde::Serialize for Line { } } -/// The result of one request, or why it can't run. +/// The result of one request, or why it can't run. Its `timeout_ms` limits the whole request, which a timing or costs request counts from when it holds the timing lock (`timing::started`). 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("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).map(Reply::Json); } - basedpl::protocol::request(session, request, options).map(|e| Reply::Response(basedpl::protocol::Response(e))) + let (interrupt, limit) = (&options.interrupt, options.timeout); + if let Some(spec) = request.get("timing") { return basedpl::timing::request(spec, interrupt, limit).map(Reply::Json); } + if let Some(spec) = request.get("costs") { return basedpl::timing::file_request(spec, interrupt, limit).map(Reply::Json); } + if let Some(limit) = limit { interrupt.expire_after(limit); } + if let Some(case) = request.get("case") { return Ok(Reply::Json(basedpl::reference::check(case, interrupt))); } + if let Some(spec) = request.get("reference") { return basedpl::reference::file_request(spec, interrupt).map(Reply::Json); } + if let Some(spec) = request.get("page") { return basedpl::reference::page_request(spec, interrupt).map(Reply::Json); } + if let Some(spec) = request.get("route") { return basedpl::timing::route_request(spec, interrupt).map(Reply::Json); } + basedpl::protocol::request(session, request, options) + .and_then(|result| basedpl::protocol::Response::new(result).map(Reply::Response).map_err(|error| error.to_string())) } diff --git a/editors/jetbrains/BPL.tmbundle/Syntaxes/bpl.tmLanguage.json b/editors/jetbrains/BPL.tmbundle/Syntaxes/bpl.tmLanguage.json index 75b88c01..341eb11d 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" }, { @@ -53,11 +53,11 @@ "name": "support.function.bpl" }, { - "match": "(?:[¯]?[∞]|[¯]?(\\d+([.]\\d+)?|[.]\\d+)([eEₑ][¯]?\\d+)?([jJⱼ][¯]?(\\d+([.]\\d+)?|[.]\\d+)([eEₑ][¯]?\\d+)?|[rᵣ][¯]?\\d+)?)(?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ)?|[ₑⱼᵣ]", + "match": "(?:[¯]?[∞]|[¯]?(\\d+([.]\\d+)?|[.]\\d+)([eEₑ][¯]?\\d+)?([jJⱼ][¯]?(\\d+([.]\\d+)?|[.]\\d+)([eEₑ][¯]?\\d+)?|[rᵣ][¯]?\\d+)?)(?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ|ₐ)?|[ₑⱼᵣ]", "name": "constant.numeric.bpl" }, { - "match": "[⍬](?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ)?", + "match": "[⍬](?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ|ₐ)?", "name": "constant.numeric.bpl" }, { @@ -69,13 +69,13 @@ "name": "string.quoted.double.bpl" }, { - "begin": "[\\])](?=(?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ))", + "begin": "[\\])](?=(?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ|ₐ))", "beginCaptures": { "0": { "name": "source.bpl" } }, - "end": "(?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ)|$", + "end": "(?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ|ₐ)|$", "contentName": "constant.numeric.bpl", "patterns": [], "applyEndPatternLast": true, @@ -90,11 +90,11 @@ "name": "support.function.bpl" }, { - "match": "(?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ)", + "match": "(?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ|ₐ)", "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 6161b516..3540a2a1 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" }, { @@ -27,11 +27,11 @@ "name": "support.function.bpl" }, { - "match": "(?:[¯负負]?[∞永]|[¯负負]?(\\d+([.内內]\\d+)?|[.内內]\\d+)([eEₑ][¯负負]?\\d+)?([jJⱼ][¯负負]?(\\d+([.内內]\\d+)?|[.内內]\\d+)([eEₑ][¯负負]?\\d+)?|[rᵣ][¯负負]?\\d+)?)(?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ)?|[ₑⱼᵣ]", + "match": "(?:[¯负負]?[∞永]|[¯负負]?(\\d+([.内內]\\d+)?|[.内內]\\d+)([eEₑ][¯负負]?\\d+)?([jJⱼ][¯负負]?(\\d+([.内內]\\d+)?|[.内內]\\d+)([eEₑ][¯负負]?\\d+)?|[rᵣ][¯负負]?\\d+)?)(?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ|ₐ)?|[ₑⱼᵣ]", "name": "constant.numeric.bpl" }, { - "match": "[⍬空](?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ)?", + "match": "[⍬空](?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ|ₐ)?", "name": "constant.numeric.bpl" }, { diff --git a/editors/vim/syntax/bpl.vim b/editors/vim/syntax/bpl.vim index e17a4334..07693bea 100644 --- a/editors/vim/syntax/bpl.vim +++ b/editors/vim/syntax/bpl.vim @@ -2,15 +2,15 @@ if exists('b:current_syntax') finish endif -syntax match bplName /\v%(_\d+|_\S|_|[^\s\x00-\x1f\x7f-\x9f0-9←⎕•⍺⍵⍶⍹∇⍢⍝⋄⁝\?¯∞⍬\+\-×÷⌈⌊\|\*⍟○π√!∧∨⊼⊽\~=≠<≤>≥⍳↦:⍴≢≡,⍪⊂⊃⊆∊∪∩⍋⍒↑↓⌽⊖⍉⊤⊥⍎⍕⌷⌹\#↕⍭⨸⌻⍸⍷⊢⊣¿¨ᵇ⍨↣⍤∘⍠⍥↢\.⊗⌸⍣⇄⊘@∂⍚⌺\/⌿\\⍀⁰¹²³⁴⁵⁶⁷⁸⁹⁻ᵀᵘₓ₀₁₂₃₄₅₆₇₈₉₋ₓ₁ₓ₂ₓ₄ₓ₈ₓₕₛₚ\(\)\[\]\{\}'";\$_ₑⱼᵣ])+/ +syntax match bplName /\v%(_\d+|_\S|_|[^\s\x00-\x1f\x7f-\x9f0-9←⎕•⍺⍵⍶⍹∇⍢⍝⋄⁝\?¯∞⍬\+\-×÷⌈⌊\|\*⍟○π√!∧∨⊼⊽\~=≠<≤>≥⍳↦:⍴≢≡,⍪⊂⊃⊆∊∪∩⍋⍒↑↓⌽⊖⍉⊤⊥⍎⍕⌷⌹\#↕⍭⨸⌻⍸⍷⊢⊣¿¨ᵇ⍨↣⍤∘⍠⍥↢\.⊗⌸⍣⇄⊘@∂⍚⌺\/⌿\\⍀⁰¹²³⁴⁵⁶⁷⁸⁹⁻ᵀᵘₓₐ₀₁₂₃₄₅₆₇₈₉₋ₓ₁ₓ₂ₓ₄ₓ₈ₓₕₛₚₐ\(\)\[\]\{\}'";\$_ₑⱼᵣ])+/ syntax match bplSystem /\v[•][A-Za-z_∆⍙][A-Za-z0-9_∆⍙]*/ syntax match bplString /\v\$[ad]/ syntax match bplFunction /[+×÷⌈⌊|*⍟○π√!∧∨⊼⊽~=≠<≤>≥⍳↦:⍴≢≡,⍪⊂⊃⊆∊∪∩⍋⍒↑↓⌽⊖⍉⊤⊥⍎⍕⌷⌹#↕⍭⨸⌻⍸⍷⊢⊣¿-]/ -syntax match bplMonadicOperator #\v%(ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ)|[¨ᵇ⍨⊗⌸⇄∂/⌿\\⍀⁰¹²³⁴⁵⁶⁷⁸⁹⁻ᵀᵘₓ]# +syntax match bplMonadicOperator #\v%(ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ|ₐ)|[¨ᵇ⍨⊗⌸⇄∂/⌿\\⍀⁰¹²³⁴⁵⁶⁷⁸⁹⁻ᵀᵘₓₐ]# syntax match bplDyadicOperator /[↣⍤∘⍠⍥↢.⍣⊘@⍚⌺]/ syntax match bplArgument /[⍺⍵⍶⍹∇⍢]/ syntax match bplKeyword /[←⎕⋄⁝?₀₁₂₃₄₅₆₇₈₉₋]/ -syntax match bplNumber /\v%([¯]?[∞]|[¯]?(\d+([.]\d+)?|[.]\d+)([eEₑ][¯]?\d+)?([jJⱼ][¯]?(\d+([.]\d+)?|[.]\d+)([eEₑ][¯]?\d+)?|[rᵣ][¯]?\d+)?)%(ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ)?|[ₑⱼᵣ]|[⍬]%(ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ)?/ +syntax match bplNumber /\v%([¯]?[∞]|[¯]?(\d+([.]\d+)?|[.]\d+)([eEₑ][¯]?\d+)?([jJⱼ][¯]?(\d+([.]\d+)?|[.]\d+)([eEₑ][¯]?\d+)?|[rᵣ][¯]?\d+)?)%(ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ|ₐ)?|[ₑⱼᵣ]|[⍬]%(ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ|ₐ)?/ syntax match bplNumber /\v([\])])@<=ₓ/ syntax match bplNumber /\v\$[tfn]/ syntax match bplCharacter /'.\?'/ diff --git a/lib/regex.bpl b/lib/regex.bpl index 06bd01be..31fc3675 100644 --- a/lib/regex.bpl +++ b/lib/regex.bpl @@ -1,6 +1,6 @@ ⍝ pattern (f regex_replace) text — transform each match, retaining intervening text. regex_replace←{ - s←⍵ ⋄ p←•r ⍺ ⋄ m←p.matches s + s←⍵ ⋄ m←s •r ⍺ starts←m.position 0ₓ=≢starts?s; ends←starts+≢¨m.text diff --git a/nbs/arrays.ipynb b/nbs/arrays.ipynb index 8b110409..434c54f9 100644 --- a/nbs/arrays.ipynb +++ b/nbs/arrays.ipynb @@ -861,7 +861,7 @@ "id": "5b19c54a", "metadata": {}, "source": [ - "Selecting an item gives back a function you can call. A [subscript](scripts.ipynb#subscripts) selects one position:" + "Selecting an item gives back a function. A [subscript](scripts.ipynb#subscripts) selects one position. To call the function, name it first, as with any function that a call or a selection gives:" ] }, { @@ -881,7 +881,8 @@ } ], "source": [ - "2 fs₁ 3" + "times←fs₁\n", + "2 times 3" ] }, { diff --git a/nbs/assignment.ipynb b/nbs/assignment.ipynb index dd58b4a3..b9d6d651 100644 --- a/nbs/assignment.ipynb +++ b/nbs/assignment.ipynb @@ -908,6 +908,26 @@ "total" ] }, + { + "cell_type": "markdown", + "id": "db7f2179", + "metadata": {}, + "source": [ + "Such a write keeps the outer name an array, a function or an operator, whichever it held, because statements outside the dfn bound by it. With `x←1 2 3`, `{x₀←- ⋄ 0}0` is a `SYNTAX` error. A write that keeps `x` an array works:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a96ead6a", + "metadata": {}, + "outputs": [], + "source": [ + "x←1 2 3\n", + "_←{x₀←9 ⋄ 0}0\n", + "x" + ] + }, { "cell_type": "markdown", "id": "049ce896", @@ -946,6 +966,51 @@ "source": [ "Value semantics cost a copy only where another name or array holds the array. Above, `u₀←9` copies `u` first, because `w` holds it too. A write to part of an array that nothing else holds changes the array in place. `x,←v` and `x⍪←v` add to the end of the array's storage. `x←x,v` always copies, because `x` still holds the old array while `x,v` is computed." ] + }, + { + "cell_type": "markdown", + "id": "e61843b6", + "metadata": {}, + "source": [ + "## Names that change within a statement" + ] + }, + { + "cell_type": "markdown", + "id": "224da943", + "metadata": {}, + "source": [ + "A statement binds by what each name it reads holds when the statement starts: an array, a function or an operator. A plain assignment in the statement shows its change in the text, so a read later in the statement binds the value it assigns. Here the left `x` reads 3, though `x` held `+` when the statement started:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72536890", + "metadata": {}, + "outputs": [], + "source": [ + "x←+\n", + "x+(x←3)" + ] + }, + { + "cell_type": "markdown", + "id": "7cb08baa", + "metadata": {}, + "source": [ + "Other changes don't show in the text. A selective write that turns a name between an array, a function and an operator, as `x₀←1` turns `x←[- 2]` into numbers, makes a read of that name later in the statement a `SYNTAX` error: `x,(x₀←1)`. So does a change made by `⍎`: with `x←1`, `x+⍎\"x←+ ⋄ 2\"` is a `SYNTAX` error. A name that is unbound when the statement starts binds as an array, so `⍎` can assign it:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1568e7fe", + "metadata": {}, + "outputs": [], + "source": [ + "z+⍎\"z←3\"" + ] } ], "metadata": { diff --git a/nbs/autodiff.qmd b/nbs/autodiff.qmd index 56f0d9cd..0022d924 100644 --- a/nbs/autodiff.qmd +++ b/nbs/autodiff.qmd @@ -61,7 +61,7 @@ Functions that move items, such as reshape, transpose, take and windows, move ea Forward mode takes one pass for each number in `X`. A network with a million parameters would take a million passes. Where `X` holds more than one number, `∂` instead runs `f` once with a record in place of each derivative. A record's items are never computed. Each call on a record adds a linear step to it, built from the same partial derivatives that forward mode uses. The steps are few: scaling by a coefficient, adding, copying from positions, broadcasting, folding by `+`, and matrix products with a constant. When `f` returns, the record holds the linearisation of `f` at `X`. That's the linear map from a small change in `X` to the change it causes in `f X`. -`∂` then reads the Jacobian from the record. A run through the record backwards, through each step's transpose in reverse order, gives one row of the Jacobian. A run forwards gives one column. `∂` takes whichever direction needs fewer runs. A function that gives a number, such as a loss, needs one backward run. Its gradient costs a small multiple of one run of `f`, however many numbers `X` holds. Dex and JAX also build reverse mode as the transpose of a linearisation. One set of tangent rules then serves both modes. +`∂` then reads the Jacobian from the record. A run through the record backwards, through each step's transpose in reverse order, gives one row of the Jacobian. A run forwards gives one column. `∂` takes the direction that needs fewer runs. It makes them as one batched run, whose inputs are the rows or columns of an identity matrix, as JAX's `jacrev` maps `vjp` over a basis. A function that gives a number, such as a loss, needs one backward run. Its gradient costs a small multiple of one run of `f`, however many numbers `X` holds. Dex and JAX also build reverse mode as the transpose of a linearisation. One set of tangent rules then serves both modes. Each linear step has one transpose: @@ -139,23 +139,38 @@ w←1 2 3 ⋄ g←(+/w×⊢)∂ 4 5 6 w←7 8 9 ⋄ (•explain "(+/w×⊢)∂ 4 5 6").programs.event ⍝ ["replayed"] ``` -A trace stops at anything that a program can't hold: output or another effect, a random draw, `⍎`, a read of a differentiated value, as a guard on one makes, or a call that no plan covers. `∂` then runs the call through duals and records. `∂` keeps the stop for those argument types and shapes. `•explain` gives the reason in its `stopped` rows: +### What stops a trace + +A trace stops where its program can't hold what the function does. `∂` then runs that call through duals and records, more slowly. It keeps the stop for those argument types and shapes. `•explain` gives the reason in its `stopped` rows. A trace stops at: + +- an effect that a program can't hold (`"an effect"`): an effect on a nested or differentiated value; a system function that acts and gives text, a record or a table, such as `•host`, `•nget` or `•remove`; `•prefs`, `•load`, `•ex`, `•explain` and `•rand`, which change the session; input through `⎕`; and `⍕` of a differentiated value. Output (`⎕←`), `•time`, `•delay` and `•nput` don't stop a trace: each replay runs them in order, after the work before them. A number that one gives is new on each call, so a guard on it stops the trace; +- a write to a name outside the function (`"a write"`); +- an error that an error guard catches (`"a caught error"`); +- a random draw that no program step holds (`"a random draw"`): a draw inside a map, a deal, a discrete distribution's samples, and a roll whose bounds the trace can't show to be all 0, or all whole numbers of at least 1; +- text that `⍎` runs (`"text that ⍎ runs"`); +- a read of a differentiated value's items (`"a read of a traced value"`): a guard's predicate, a count or positions computed from one, or Agenda's (`⍚`) selector computed from one where a case can't run on every item, as a dfn case can't; +- a function called on each item of a differentiated value (`"a function called on each item"`): a map that calls its function once for each cell, as where its batched call falls back; a fold by a primitive that `∂` has no rule for, such as `!/`; and a fold or scan by a dfn whose body no loop node holds, as where the body itself stops a trace; +- a call whose result type depends on its values, under dynamic kinds, where the first run's items give the other type or the call is `!` (`"a result whose type depends on values"`), described below; +- a call that programs don't hold yet, such as a selection by computed positions along axes or with a fill (its reason names the call); +- a definition traced 9 times in a row with no replay between, as when a free value or an argument's lengths change on every call (`"the retrace limit"`). + +A guard on a differentiated value stops a trace: ```bpl pos←{+/⍵ > 0 ? +/⊼⍵ ; 0} (•explain "pos∂ 1 2 3").stopped.reason ⍝ ["a read of a traced value"] ``` -Each replay reads the current value of every name that the function reads from outside its body, and of every array that a function other than a dfn binds. A name or an array read as positions, a count or a guard's condition is part of the program itself. A replay checks its value. A new value traces the function again. +Reading a type or a shape never stops a trace. A guard on `≢⍵`, or a value computed from a length such as the divisor in `÷≢⍵`, holds for every call with the argument types and shapes that the program holds. Each replay reads the current value of every name that the function reads from outside its body, and of every array that a function other than a dfn binds. A name or an array read as a count or a guard's condition is part of the program itself. A replay checks its value. A new value traces the function again. A name read as positions isn't: each replay selects at its current positions, and raises INDEX ERROR at one outside its axis. -A program fixes every type before it runs. Under the default dynamic kinds, a result's type can depend on its values: `√` of a negative number gives a complex number. A trace stops at such a call. Under static kinds, which `•prefs ["kinds":"static"]` sets, every type follows from the argument types. The same call then traces. Static kinds are the setting for work where speed matters. +A program fixes every type before it runs. Under the default dynamic kinds, a result's type can depend on its values: `√` of a negative number gives a complex number. On reals, the calls that can do this are `√` and every root but the cube root `3√`, `⍟`, `!`, `0○`, and `*` whose exponent isn't a whole number or is the value being differentiated. For each of these but `!`, a trace assumes the type that its first run's items give, together with the range of values on which that type always results, such as from 0 up for `√`. Each replay checks that its items stay in that range. A replay whose items leave it traces the function again. That trace counts toward the retrace limit. A trace stops at `!`. It also stops where its first run's items give the other type, as `√` of a negative number does. Under static kinds, which `•prefs ["kinds":"static"]` sets, every type follows from the argument types, and replays check nothing. Static kinds are the setting for work where speed matters. `√ₐ`, `⍟ₐ` and `*ₐ` give real results whatever the values, under either setting. Their traces neither stop nor check a range ([Numbers](numbers.qmd#approximate)). ```bpl roots←{+/√⍵} -(•explain "roots∂ 1 2 3").stopped.reason ⍝ ["a result whose type depends on values"] +(•explain "roots∂ 1 2 3").programs.event ⍝ ["built"] ``` -Write a function for `∂` under static kinds, with each branch as Agenda (`⍚`) or Under at positions (`f@[p]`) rather than a guard. A guard reads a differentiated value to pick one body for the whole call, which a program can't hold. `⍚` and `@` pick items within whole arrays. Today they also stop a trace where their choice or their values come from differentiated values. `∂` still gives the right result there, more slowly. Tracing them is planned, as "Not built yet" describes. +Write a function for `∂` under static kinds, with each branch as Agenda (`⍚`) or Under at positions (`f@[p]`) rather than a guard. A guard reads a differentiated value to pick one body for the whole call, which a program can't hold. `⍚` and `@` pick items within whole arrays. A program holds such a branch as a masked select where every case can run on every item, as pervasive primitives and constants can. Each case then reads the differentiated value with a zero tangent at the items that picked another case, so a case's infinite slope there can't make the gradient NaN. A branch with any other case, such as a dfn, runs each case on its own items instead. That stops the trace, and `∂` still gives the right result, more slowly. ## Index notation @@ -202,9 +217,8 @@ BPL also differs from all of them. `∂` strips its tags from values that escape These are planned: -- Branches in programs. Agenda (`⍚`) with a selector computed from differentiated values stops a trace, as a guard on them does. The plan is to trace such a selector as a masked select, as JAX traces `select`. - Programs inside maps. `∂` inside a map, as in `{f∂⍵}¨x`, runs without a program. `•explain` gives the reason "a call under another level". Batching inside traces is planned. -- Joins. `,`, `⍪` and `@` with differentiated values stop a trace. `∂` still gives the right result, more slowly. A program step for them is designed. +- Joins. A program step is designed for `,`, `⍪` and `@` with differentiated values. - Whole training steps. A program holds one call of `∂`. The update, and the loop around it, run outside the program. A tracing point for whole training steps, such as `⍣`, would run a step's forward pass, gradient and update as one program. - Deferred Jacobians. `f∂ x` builds every item of the Jacobian. The plan holds a large Jacobian as its linearisation, and builds its items only when something reads them. `J+.×v` would then run as a JVP, `u+.×J` as a vector–Jacobian product, and `b⌹J` as an iterative solve, as in the Gauss–Newton step `x - r⁝x ⌹ r∂x`. - The GPU. Programs run on the CPU. A later stage adds a WebGPU back end, with arrays kept on the device. diff --git a/nbs/bpl.xml b/nbs/bpl.xml index 8d9491c2..8dc1dcfe 100644 --- a/nbs/bpl.xml +++ b/nbs/bpl.xml @@ -7,22 +7,22 @@ - + - - + + - + - - + + - + diff --git a/nbs/distributions.ipynb b/nbs/distributions.ipynb index ba0bb4ec..72c85055 100644 --- a/nbs/distributions.ipynb +++ b/nbs/distributions.ipynb @@ -256,9 +256,9 @@ "id": "e9297039", "metadata": {}, "source": [ - "`•rand seed` makes a generator with its own stream. Put it on the left of `sample` for reproducible draws. The same seed gives the same draws. A generator also has `roll` and `deal`, which work like `¿`. Copies of a generator share one stream.\n", + "`•rand seed` makes a generator. Put it on the left of `sample` for reproducible draws. The same seed gives the same draws. A generator also has `roll` and `deal`, which work like `¿`. Copies of a generator share its count of draw calls.\n", "\n", - "Without a generator, `sample` and `¿` use the session's stream, initially seeded by the operating system. `•prefs [\"seed\":42]` restarts that stream with seed 42." + "Without a generator, `sample` and `¿` use the session's generator, which the operating system keys when the session starts. `•prefs [\"seed\":42]` restarts it with seed 42." ] }, { diff --git a/nbs/evaluation.qmd b/nbs/evaluation.qmd index 5a173dd2..eab5fe9f 100644 --- a/nbs/evaluation.qmd +++ b/nbs/evaluation.qmd @@ -1,6 +1,6 @@ # Evaluation -After [Syntax](syntax.qmd) divides a statement into runs, BPL binds the runs as it evaluates them. Binding depends on values. A name can hold a subject, a function or an operator. A name's case doesn't decide which, as it does in BQN. BPL looks a name up when the statement runs. +After [Syntax](syntax.qmd) divides a statement into runs, BPL compiles the statement, then runs it. How the statement binds depends only on its text and on whether each name it reads holds a subject, a function or an operator when the statement starts. A name's case doesn't decide which, as it does in BQN. A write that changes which of the three a name holds, where the statement's text doesn't show it, is a `SYNTAX` error, as [Assignment](assignment.ipynb) describes. ## Binding @@ -21,6 +21,16 @@ a←1 ⋄ b←2 ⋄ a b+1 ⍝ 1 3 x←5 ⋄ x 1 2 ⍝ [5 [1 2]] ``` +A call's result binds where a subject would, whatever it holds. So do a field read from a call's result, a subscript's selection and a scope's result. A function that a call returns therefore can't take arguments or join a train where the call stands: in `+/÷(f 0)` with `f←{≢}`, `÷` gets `≢` as its argument, which is a `DOMAIN` error. Name a returned function before you call it, and call a function selected from an array with [Agenda](glyphs/diamond-underbar.qmd). A dot path names a field, so `T.f 3` calls the function that field `f` holds, where the scope `T.(f)` gives it as a value. + +```bpl +f←{≢} ⋄ g←f 0 ⋄ +/÷g 1 2 3 ⍝ 2 +f←{≢} ⋄ (f 0) 1 2 3 ⍝ [≢ [1 2 3]] +fs←[- ×] ⋄ fs₁ 3 ⍝ [× 3] +fs←[- ×] ⋄ 2 (1⍚fs) 3 ⍝ 6 +T←["f":-] ⋄ (T.f 3) (T.(f) 3) ⍝ [¯3 [- 3]] +``` + An array that holds a function as an item binds as a function only where a subject follows it. There it calls each function on the whole arguments, and each other item is its own result, as [Functions in arrays](arrays.ipynb#functions-in-arrays) shows. Followed by a function, or by nothing, it's a subject: a left argument, a strand item or an argument. `f fs x` applies `f` to `fs x`, and `fs f x` gives `f` the left argument `fs`. ```bpl @@ -76,7 +86,7 @@ A space before an argument keeps the train separate from it. `+/÷≢ x` applies `⁝` keeps adjacent names distinct without separating their run: `1+⌽f⁝x` applies `f` to `x`. A space there changes the grouping: `1+⌽f x` applies the train `1+⌽f` to `x`. A literal can touch a name without a separator, as it touches a glyph: `1+⌽f5`. A run that starts with a function is a call: `×2` is the sign of 2. -A [fast combination](terms.qmd#term-fast-combination) is a combination of functions that BPL computes by an algorithm of its own. `10↑⍋v` selects the first 10 positions of the grade without ordering the rest. A combination can also be more accurate than its functions computed in turn: `⍟1+x` keeps the digits of a small `x` that `1+x` rounds away. BPL recognises a combination in any spelling: an expression, a train, a composition or an operator's operand, where its functions and constants are glyphs and literals. `10↑⍋v`, `10↑⍋ v` and `(10↑)∘⍋ v` take the same algorithm from their first call. `one←1 ⋄ ⍟one+x` computes `⍟` of `1+x`, as `ln←⍟ ⋄ ln 1+x` does. Results can differ between spellings, as the [Accuracy](numbers.qmd#accuracy) section of Numbers describes. The glyph pages list each combination under "Fast combinations". [`•explain`](system/explain.qmd) reports each selection, rank, product and Key combination that a run takes. Deferred results give other fast paths: `+/50↕v` folds the windows that `↕` defers by sliding over `v`, without building them. +A [fast combination](terms.qmd#term-fast-combination) is a combination of functions that BPL computes by an algorithm of its own. `10↑⍋v` selects the first 10 positions of the grade without ordering the rest. A combination can also be more accurate than its functions computed in turn: `⍟1+x` keeps the digits of a small `x` that `1+x` rounds away. BPL recognises a combination in any spelling: an expression, a train, a composition or an operator's operand, where its functions and constants are glyphs and literals. `10↑⍋v`, `10↑⍋ v` and `(10↑)∘⍋ v` take the same algorithm from their first call. `one←1 ⋄ ⍟one+x` computes `⍟` of `1+x`, as `ln←⍟ ⋄ ln 1+x` does. Results can differ between spellings, as the [Accuracy](numbers.qmd#accuracy) section of Numbers describes. The glyph pages list each combination under "Fast combinations". [`•explain`](system/explain.qmd) reports each selection, rank and product combination that a run takes. Deferred results give other fast paths: `+/50↕v` folds the windows that `↕` defers by sliding over `v`, without building them. ## Agreement and pervasion @@ -179,7 +189,7 @@ The map calls its operand once for each cell instead where the operand: - has a guard that catches an error - gives a result that holds arrays as items - calls a primitive that can't run on all the cells at once -- has a guard on a cell's value whose two sides give results of different kinds or shapes, write to a name outside the dfn, or call the dfn again +- has a guard on a cell's value whose two sides give results of different shapes, or of different kinds under dynamic kinds, write to a name outside the dfn, or call the dfn again - combines each cell with an array that every cell shares, where repeating that array for every cell would hold more than about 2 million items and 4 times the items of the call's arguments, and the array alone holds more than about a million items - has an argument whose copy for every cell would pass the size limit for one array diff --git a/nbs/foundations.qmd b/nbs/foundations.qmd index df9357b6..60eab95b 100644 --- a/nbs/foundations.qmd +++ b/nbs/foundations.qmd @@ -87,13 +87,11 @@ Static graph-numeric execution has a non-raising policy for data errors. An inva ## Share numerical algorithms at the right size -An elementary operation normally has one width-specific formula over a small set of arithmetic operations, called `Ops`. Native kernels and generated code instantiate that same formula. Partial derivatives and range rules belong beside the mathematical object. A substantial algorithm, such as incomplete gamma's convergence calculation, can instead run as one whole-array `Call` through the common graph runner. One algorithm does not require every function to have the same granularity. +A mathematical object owns the common computation, its derivative and range facts, and any repair needed for rare inputs. A formula over `Ops` describes arithmetic without choosing scalar, SIMD or generated execution. An optional repair predicate is another such formula, producing one Boolean per item. It is distinct from a comparison whose result happens to be Boolean. Range proofs can remove the repair test when the same predicate is always false. Otherwise consumers see the corrected result, never an uncorrected intermediate. -The recommended native backend implements actual SIMD lanes and masks through rustymath's existing abstractions, with explicit tails, widths and bounded mixed-width operations. Scalar per-item callbacks are not a substitute. The preferred mixed-width support preserves f32 trig's existing f64 reduction on non-FMA targets. A table read is added only where a retained algorithm needs one after Pow simplification; a machine without gather instructions can require one scalar load per lane. +Native execution reuses rustymath's optimized drivers and SIMD operations. RM and BPL share interfaces designed for both, without a compatibility layer around an old engine. Shared formulas remain monomorphic in hot loops; rare repair and error reporting use the existing two-pass pattern. A substantial iterative algorithm can remain one whole-array Call through the common graph runner. The foundation needs neither a new SIMD engine nor a general numerical-loop IR or blanket vector-call ABI. -The recommended numerical design composes general positive floating Pow/Root from shared Exp/Ln and keeps short ordinary trig formulas with rare object-owned scalar repair. Native SIMD repairs flagged items after its first pass. Generated code calls the same repair only for flagged lanes, before fused consumers use them. Compact Cbrt and genuine iterative specials keep their existing algorithmic roles. Gamma and compensated-constant-power simplification are later opportunities, not blockers for the basic build. - -Shared formulas and one home per mathematical fact are agreed principles. The numerical changes above are concrete recommendations awaiting approval, not an undecided choice between tables and a larger compiler. We still need to validate and agree any function-specific float-error scope, verify native/wasm repair providers and implement the SIMD/repair interfaces. GPU repair is a later device choice. No new accuracy exception is approved or published. [The target](target.qmd#numerical-implementation-choices) records the designs and their status. +We prefer simpler, faster algorithms to legacy machinery for unnecessary edge-case accuracy, while protecting common numerical uses and exact results. That can mean composing existing operations or repairing rare items instead of translating an elaborate algorithm into every backend. The [target](target.qmd#numerical-algorithms) defines numerical bodies, type coverage, repair ordering and accuracy requirements. ## One home for each decision diff --git a/nbs/getting-started.ipynb b/nbs/getting-started.ipynb index 58ecb77c..bd0ea2db 100644 --- a/nbs/getting-started.ipynb +++ b/nbs/getting-started.ipynb @@ -764,7 +764,7 @@ "id": "acbecb2a", "metadata": {}, "source": [ - "`•r` compiles a regular expression into two functions: `matches` gives a table of each match's text, position and groups, and `replace` replaces each match:" + "`text •r pattern` gives a table of each match's text, position and groups, and `text •replace [pattern template]` replaces each match:" ] }, { @@ -793,9 +793,9 @@ } ], "source": [ - "codes←•r \"([A-Z]+)-([0-9]+)\"\n", - "codes.matches \"AB-12 CD-3\"\n", - "\"$2:$1\" codes.replace \"AB-12 CD-3\"" + "codes←\"([A-Z]+)-([0-9]+)\"\n", + "\"AB-12 CD-3\" •r codes\n", + "\"AB-12 CD-3\" •replace [codes \"$2:$1\"]" ] }, { diff --git a/nbs/glyphs/at.qmd b/nbs/glyphs/at.qmd index 477e8b22..6f5728a5 100644 --- a/nbs/glyphs/at.qmd +++ b/nbs/glyphs/at.qmd @@ -47,11 +47,10 @@ When a function `g` returns items of `Y` that selecting primitives took, `f@g Y` (1+)@{⍵>15 # ⍵} 10 20 30 ⍝ 10 21 31 ``` -When `g` selects a position more than once, the last value wins. A value at a fill, such as overtaking `↑` adds, is dropped. +When `g` selects a position more than once, which of its values that position keeps isn't promised. A value at a fill, such as overtaking `↑` adds, is dropped. ```bpl ≥@↕ 1 2 3 ⍝ 2 3 4 -(10 20+)@[0 0] 1 2 3 ⍝ 21 2 3 -@ 5↑ 1 2 3 ⍝ ¯1 ¯2 ¯3 ``` diff --git a/nbs/glyphs/diamond-underbar.qmd b/nbs/glyphs/diamond-underbar.qmd index 41e1b9ff..b87a33c2 100644 --- a/nbs/glyphs/diamond-underbar.qmd +++ b/nbs/glyphs/diamond-underbar.qmd @@ -44,7 +44,7 @@ m←["i":2 "j":3]⍴⍳6ₓ (4ₓ<+⌿)⍚[⊢ -] m ⍝ ["i":2 "j":3]⍴[0 ¯1 ¯2 3 ¯4 ¯5]ₓ ``` -A NaN in the selector picks no case, and its cell gets the result's fill. Results keep their kinds, as Each's results do. Result cells keep the keys and names they agree on and pad differing shapes, as Rank does. A single value fills every item of its cells; when every result fills, the argument gives the cell shape. An argument with no cells gives an empty result: as Each does with prototypes, the case that the selector's fill picks runs on no cells, and the result is empty, with that case's cells. +A NaN in the selector picks no case, and its cell gets the result's fill. Under dynamic kinds, results keep their kinds, as Each's results do. Under static kinds, they join to one compact type, as a write's items do. Result cells keep the keys and names they agree on and pad differing shapes, as Rank does. A single value fills every item of its cells; when every result fills, the argument gives the cell shape. An argument with no cells gives an empty result: as Each does with prototypes, the case that the selector's fill picks runs on no cells, and the result is empty, with that case's cells. ```bpl 0 1 2⍚[5 (1↓)⍤1 ⊢] [1 2 3⋄4 5 6⋄7 8 9] ⍝ [5 5 5⋄5 6 0⋄7 8 9] diff --git a/nbs/glyphs/dot.qmd b/nbs/glyphs/dot.qmd index cccefbbc..accc2b54 100644 --- a/nbs/glyphs/dot.qmd +++ b/nbs/glyphs/dot.qmd @@ -68,7 +68,7 @@ T←["a":1 "x":[10 20 30]] ⊢.x.[[2 0]] T ⍝ 30 10 ``` -After a keyed vector `T`, `T.(expr)` evaluates `expr` with each key of `T` as a name for its item. After a matrix, or any array with keys on its first axis, each key names its major cell. Names that aren't keys resolve in the current scope, and a key hides a name it shares. A key that holds a function binds as a function. Assignments inside the parentheses stay there, and leave `T` and the current scope unchanged. [Execute](hydrant.qmd) does the same for code in text: `T⍎"price×qty"` is `T.(price×qty)`. A scope doesn't map over an array of records. Write `{⍵.(expr)}¨recs` for that. +After a keyed vector `T`, `T.(expr)` evaluates `expr` with each key of `T` as a name for its item. After a matrix, or any array with keys on its first axis, each key names its major cell. Names that aren't keys resolve in the current scope, and a key hides a name it shares. A key that holds a function binds as a function. The scope's own result binds where a subject would, whatever it holds, as a call's result does: `T.f 3` calls the function that field `f` holds, where `T.(f) 3` is a strand of that function and 3. Assignments inside the parentheses stay there, and leave `T` and the current scope unchanged. [Execute](hydrant.qmd) does the same for code in text: `T⍎"price×qty"` is `T.(price×qty)`. A scope doesn't map over an array of records. Write `{⍵.(expr)}¨recs` for that. ```bpl T←["price":2 3;"qty":10 20] @@ -77,6 +77,7 @@ rate←0.5 ⋄ T.(price×rate) ⍝ 1 1.5 r←T.(price←0) ⋄ T.price ⍝ 2 3 M←"price" "qty":[2 3⋄10 20] M.(price×qty) ⍝ 20 60 +T←["f":-] ⋄ (T.f 3) (T.(f) 3) ⍝ [¯3 [- 3]] ``` A dot followed by a digit is a decimal point: `.2` is a number. To index position 2, write `v.[2]` or the [subscript](../scripts.ipynb#subscripts) `v₂`. Assignment through a dot path writes the items it reads, and adds missing keys. diff --git a/nbs/glyphs/hydrant.qmd b/nbs/glyphs/hydrant.qmd index c9bebe1b..0eadb949 100644 --- a/nbs/glyphs/hydrant.qmd +++ b/nbs/glyphs/hydrant.qmd @@ -16,7 +16,11 @@ On a character matrix, `⍎` evaluates each row. On an array of strings, it eval ⍎"1+1" "2×3" ⍝ 2 6 ``` -Assignments and explicit output in `⍎Y` take effect in the current scope. +Assignments and explicit output in `⍎Y` take effect in the current scope. A statement binds by what its names hold when it starts, so a change that `⍎Y` makes to a name, between an array, a function and an operator, makes a later read of that name in the same statement a `SYNTAX` error: with `x←1`, `x+⍎"x←+ ⋄ 2"` fails. A name that is unbound when the statement starts binds as an array, so `⍎Y` can assign it. + +```bpl +z+⍎"z←3" ⍝ 6 +``` `T⍎Y` evaluates `Y` with each key on the first axis of `T` as a name, as [Dot](dot.qmd)'s `T.(expr)` does. Names that aren't keys resolve in the current scope. Assignments stay inside the evaluation, and leave `T` unchanged. diff --git a/nbs/glyphs/inverted-question.qmd b/nbs/glyphs/inverted-question.qmd index ff61859d..364a358b 100644 --- a/nbs/glyphs/inverted-question.qmd +++ b/nbs/glyphs/inverted-question.qmd @@ -17,7 +17,7 @@ Function. Keys: `Alt-/`. Hanzi: `掷/抽` (`擲/抽`). Ranks: `0` monadic, `0 0` Bounds are nonnegative whole numbers; `M≤N`. Exact arguments give exact integer draws. Where integers wrap, under static kinds or at a fixed width, a roll of integers gives integers, and a bound of 0, which rolls a float, is a DOMAIN error. A float bound of 0 still rolls a float, as `¿0` does. Dyalog APL writes Roll and Deal as `?`, which BPL uses for [predicates](question.qmd). -`¿` draws from the session's stream of random numbers, which the operating system seeds when the session starts. [`•prefs ["seed":n]`](../system/prefs.qmd) seeds it with the nonnegative whole number `n`, and the same seed gives the same draws. In a [map](../terms.qmd#term-map), the cells take the stream's numbers in turn. A [batched call](../evaluation.qmd#functions-and-effects) takes every cell's numbers for one `¿` at once, in cell order. [`•rand`](../system/rand.qmd) makes a separate stream. +`¿` draws from the session's generator, which the operating system keys when the session starts. [`•prefs ["seed":n]`](../system/prefs.qmd) seeds it with the nonnegative whole number `n`, and the same seed gives the same draws. In a [map](../terms.qmd#term-map), the cells take the stream's numbers in turn. A [batched call](../evaluation.qmd#functions-and-effects) takes every cell's numbers for one `¿` at once, in cell order. [`•rand`](../system/rand.qmd) makes a separate stream. ```bpl •prefs ["seed":42] ⋄ a←¿6 6 6 ⋄ •prefs ["seed":42] ⋄ a≡¿6 6 6 ⍝ $t diff --git a/nbs/glyphs/quad-equal.qmd b/nbs/glyphs/quad-equal.qmd index 28e1fdfb..04f6ed04 100644 --- a/nbs/glyphs/quad-equal.qmd +++ b/nbs/glyphs/quad-equal.qmd @@ -12,17 +12,11 @@ Monadic operator. Keys: `Alt-q =`. Hanzi: `组` (`組`) Keys match with [tolerance](../evaluation.qmd#equality-and-ordering), as in `≠`. Results assemble with fill. Empty input calls `f` once for its prototype. -## Fast combinations +## How Key runs -BPL computes each of these combinations by an algorithm of its own, in any spelling. [Trains](../evaluation.qmd#trains) says which spellings BPL recognises. - -| Combination | Applies to | Benefit | -|---|---|---| -| `≢⊢ ⌸Y` or `K (≢⊢)⌸Y` | any keys | counts each group's major cells without building the groups | -| `K (f/⊢)⌸Y` or `K (f⌿⊢)⌸Y`, with `f` of `+ ⌈ ⌊`, or `×` of floats | a vector `Y` of floats, integers or Booleans | folds every group in one pass, without a call of the operand for each group | +Key calls `f` once on all its groups where it can, whatever `f`'s spelling, as a [map](../evaluation.qmd#functions-and-effects) calls its operand once on all its cells. A fold or scan of each group of a vector, such as `K (+/⊢)⌸Y` or `K {+/⍵}⌸Y`, reads each item of `Y` by its group's number. [`•explain`](../system/explain.qmd) reports that route as `"key classes"`. ```bpl -≢⊢ ⌸"banana" ⍝ [1 3 2]ₓ -1 1 2(+/⊢)⌸10 20 30 ⍝ 30 30 -(•explain "1 1 2(+/⊢)⌸10 20 30").fast.combination ⍝ ["group folds"] +1 1 2(+/⊢)⌸10 20 30 ⍝ 30 30 +(•explain "1 1 2{+/⍵}⌸10 20 30").routes.route ⍝ ["key classes" "batching"] ``` diff --git a/nbs/hpl.xml b/nbs/hpl.xml index 4eb548fc..624780fb 100644 --- a/nbs/hpl.xml +++ b/nbs/hpl.xml @@ -5,10 +5,10 @@ - + - - + + diff --git a/nbs/index-notation.qmd b/nbs/index-notation.qmd index 12f31176..15d2c843 100644 --- a/nbs/index-notation.qmd +++ b/nbs/index-notation.qmd @@ -34,14 +34,14 @@ mlp←{ h←7○b(+⍤1)x+.×w +/÷≢ ⊼y-c+h+.×v } -[x y]mlp p ⍝ 0.3179859389 +[x y]mlp p ⍝ 0.3262591588 mlpi←{ [x y]←⍺ ⋄ [w b v c]←⍵ [[n i] h]←•index[⍴x ≢b] a←7○b.[h]+x.[n i]+.×⍠i⁝w.[i h] +/÷≢ ⊼y.[n]-c+a+.×⍠h⁝v.[h] } -[x y]mlpi p ⍝ 0.3179859389 +[x y]mlpi p ⍝ 0.3262591588 g←[x y]mlp∂ p ⋄ gi←[x y]mlpi∂ p 1E¯12>⌈/,|g.w-gi.w ⍝ $t ``` @@ -61,7 +61,7 @@ heads←{h←0 2 1 3⍉⍵ ⋄ (¯2↓⍴h , ×/¯2↑⍴h)⍴h} attn←{ [wqkv wo]←⍺ [q k v]←1 3 0 2 4⍉⍵+.×wqkv - s←mask(+⍤2) q+.×⍤2[0 1 3 2]⍉k ÷ √⊢/⍴q + s←mask(+⍤2) q+.×⍤2⍉⍤2⁝k ÷ √⊢/⍴q e←*s-⌈/s (heads e÷+/e +.×⍤2 v)+.×wo } @@ -88,6 +88,45 @@ Line by line, in `attni`: - `e÷+/⍠u⁝e` is the softmax over `u`. `o` sums the values over `u`, each weighted by its share. - `wo.[j+(≢j)×h d]` reads the row of `wo` for head `h` and feature `j`. BPL reads through positions computed from indices as a map. The fold over `[h j]` merges the heads. `:[b t d]⍉⁻¹` orders the result's axes and removes their names, to give `attn`'s plain shape. +## A Gaussian mixture + +This objective scores how well a mixture of Gaussian components fits a set of points, with a prior on each component's precision. `gp` holds each component's log weight `alpha` and mean `mu`. Its precision factor has the exponentials of `q` on its diagonal and `l` below it. `low` masks `l` to the part below the diagonal. `eye` is the identity matrix. `•detach` makes the log-sum-exp shifts constants to `∂`. + +`gmm` is the rank form. `l×⍤2 low` masks each component's matrix, `(*q)×⍤[1 2]eye` puts each component's row of `*q` on a diagonal, `⍉⍤2` transposes each component's factor, and `⌿` folds along the first axis, the components. In the index form, `gmmi`, `n` names the points, `k` the components, and `d` and `e` the dimensions. Both forms give the same objective and the same gradient: + +```bpl +•prefs ["seed":42] +gx←¿[50 4]⍴0 ⋄ eye←=⊗⍨⍳4 ⋄ low←>⊗⍨⍳4 +gp←["alpha":¿3⍴0 "mu":¿[3 4]⍴0 "q":0.1׿[3 4]⍴0 "l":0.1׿[3 4 4]⍴0] +gmm←{ + [alpha mu q l]←⍵ + L←l×⍤2 low + Q←L+(*q)×⍤[1 2]eye + sq←+/⊼(⍺(-⍤1)⍤[2 1]mu)+.×⍤2⍉⍤2⁝Q + t←(alpha++/q)-0.5×sq + m←⌈⌿•detach t + am←⌈/•detach alpha + (+/m+⍟+⌿*t-⍤1m) + (0.5×(+/,⊼*q)++/,⊼L) - (≢⍺)×am+⍟+/*alpha-am +} +gx gmm gp ⍝ ¯0.8795002466 +gmmi←{ + [alpha mu q l]←⍵ + [[n d] [k e]]←•index[⍴⍺ ⍴mu] + L←l.[k d e]×low.[d e] + Q←L + *q.[k d] × eye.[d e] + sq←+/⍠d⊼Q+.×⍠e⁝⍺.[n e]-mu.[k e] + t←alpha.[k]++/⍠d⁝q.[k d] - 0.5×sq + m←⌈/⍠k•detach t + am←⌈/•detach alpha + +/m+⍟+/⍠k*t-m + (0.5× +/,⊼*q + +/,⊼L) - ≢⍺ × am+⍟+/*alpha-am +} +gx gmmi gp ⍝ ¯0.8795002466 +g←gx gmm∂ gp ⋄ h←gx gmmi∂ gp +1E¯12>⌈/,|g.l-h.l ⍝ $t +``` + +Each form reads better in different lines. Rank suits the lines that work on whole components: `⍤2` and `⍤[1 2]` pair each component's matrix and row, and `⌿` folds along the components. Index notation suits `sq`. There `⍺.[n e]-mu.[k e]` pairs every point with every mean by name, where the rank form needs the nested Rank `⍺(-⍤1)⍤[2 1]mu`. `+.×⍠e` also names the axis that the product sums along. + ## Why it works this way No feature of BPL requires names or keys. A function uses them where they express something. Code written for plain data can give other values on named or keyed data, because there the names say which axes align. @@ -98,4 +137,4 @@ Each, Rank and Stencil (`⌺`) need no new syntax. They suit a function written ## Not built yet -[Automatic differentiation](autodiff.qmd#not-built-yet) lists what traced programs don't cover yet. Two of its items apply to models like these. A program holds one call of `∂`: a training step's update and the loop around it run outside the program. A per-item branch, such as a selector that Agenda (`⍚`) computes from differentiated values, stops a trace. +[Automatic differentiation](autodiff.qmd#not-built-yet) lists what traced programs don't cover yet. Two of its items apply to models like these. A program holds one call of `∂`: a training step's update and the loop around it run outside the program. The other is branches in programs, such as Agenda (`⍚`) with a selector computed from differentiated values (see [What stops a trace](autodiff.qmd#what-stops-a-trace)). diff --git a/nbs/keyed.ipynb b/nbs/keyed.ipynb index e8503a8e..0bc67bea 100644 --- a/nbs/keyed.ipynb +++ b/nbs/keyed.ipynb @@ -2571,7 +2571,7 @@ "id": "4954f2fc", "metadata": {}, "source": [ - "Agreement first pairs axes with equal names, wherever they sit. Unpaired axes broadcast. The result has the left argument's axes first, then the axes only the right argument has:" + "Agreement first pairs axes with equal names, wherever they sit. Unpaired axes broadcast. Where every axis of the left argument pairs and the right argument has more axes, the result takes the right argument's axis order. Otherwise the left argument's axes come first, then the axes only the right argument has:" ] }, { @@ -2601,7 +2601,7 @@ "id": "dc8a2d61", "metadata": {}, "source": [ - "The order of the arguments decides the order of the result's axes, but `C+Cᵀ` still pairs each axis with the one of the same name:" + "`monthly+C` takes the order of `C`, the larger argument, as `C+monthly` does. Arguments with the same axes give the left argument's order, and `C+Cᵀ` pairs each axis with the one of the same name:" ] }, { diff --git a/nbs/llms.txt b/nbs/llms.txt index f481e068..c44f6701 100644 --- a/nbs/llms.txt +++ b/nbs/llms.txt @@ -15,7 +15,7 @@ Entry: Dyalog diff, example, defining page. Not the reference: read the page bef ## Language - [Syntax](syntax.html.md): BQN stranding: mark strands with `[]` (per BQN's 5-year retrospective), except spaced literals (step 1) and subjects side by side (step 3); like BQN, a space is semantic. `⁝` separates tokens and stops literal stranding without ending the run: `h⁝m`≡`h(m)`, `f⍣3⁝0`≡`(f⍣3)0`. 3 steps: 1. spaced literals join, 1 vector: `1 2`, `"ab" "cd"`; even operand+arg: `+/⍠2 3 3 3↕m` has operand `2 3 3 3`, write `+/⍠2 3⁝3 3↕m`. 2. each other run→`(run)`: `a+b × c+d`≡`(a+b)(×)(c+d)`; fn between two runs: space both sides: `a×b + c`≡`(a×b)+c`; `a×b +c`≡`[a×b ; +c]`; `a×b+ c`≡`(a×b+)c`, train `a×(b+c)`; `1+k (⌈/⊢)⌸ a`≡`(1+k)((⌈/⊢)⌸)(a)`. 3. read as APL, right to left; subjects side by side strand: `a b`≡`(a)(b)`≡`[a b]` only if neither touches; `1+a b`≡`(1+a)(b)`→`11 20`, 1 plus both: `1+[a b]`→`11 21`; `n 2⍴0`≡`(n)(2⍴0)`→`[3 [0 0]]`, names never join literal strands: write `[n 2]⍴0`. no needless spaces; join by deleting spaces, not adding parens: `1+k(⌈/⊢)⌸a`. name fn, name arg: `h⁝m` `h[m n]`. name operand, glyph arg: `+/⍠k⍵`. list: `[+/x ; +/x ÷ ≢x]`. name digits: `x_1`; `x1`≡`x(1)`. -- [Evaluation](evaluation.html.md): run ending in fn: train. `+/÷≢ x` mean; `+/÷≢x`≡`+/÷(≢x)`. `⁝` doesn't group a train: `+/÷≢⁝x` still means the latter. A space before an arg makes the run on its left a train: `10÷√1++/⍠i x` calls dyadic `√`, write `10÷√1++/⍠i⁝x`; `+/,m+⍤2 P` applies the train `+/,m+⍤2` to `P`, write `+/,m(+⍤2)P`. +- [Evaluation](evaluation.html.md): run ending in fn: train. `+/÷≢ x` mean; `+/÷≢x`≡`+/÷(≢x)`. `⁝` doesn't group a train: `+/÷≢⁝x` still means the latter. A space before an arg makes the run on its left a train: `10÷√1++/⍠i x` calls dyadic `√`, write `10÷√1++/⍠i⁝x`; `+/,m+⍤2 P` applies the train `+/,m+⍤2` to `P`, write `+/,m(+⍤2)P`. Calls give values: a call's result, a field read from it, a subscript's selection and a scope's result bind as arrays even when they hold a function, so `+/÷(f 0)` is an expression, never a train; name a returned function first: `g←f 0 ⋄ +/÷g`; call a selected function with Agenda: `1⍚fs x`, `2 (1⍚fs) 3`. - [Composition](composition.html.md): read before writing tests or examples. trains: `f⁝g Y`≡`f(g Y)`, `f⁝g⁝h Y`≡`(f Y)g(h Y)`; `∘` atop, `⍥` over, `↣` `↢` bind left, right; `⍨` self, commute; `⊢` `⊣` stand for args in trains; `⁻¹` inverse; `@` under. worse→better, same results: `0.5×⊢+2÷⊢`→`⊽⁻¹+÷`; `{⍵,1}`→`,↢1`; `×⍨x`→`x²`; `≥⍣3⊢5`→`≥⍣3⁝5`; `+/⍠i⊢m`→`+/⍠i⁝m`. Prefer spaces for top-level groups/subtrains: `+/÷≢ ⊼v`, `mean_square←+/÷≢ ⊼`, `f⁝g⁝h x`. A space groups the whole left run, including any prefix. Nested `()`: try the inner pair first. Prefer `⁝` where a boundary is needed: `f⍣3⁝0`, `(+/÷≢)⍤1⁝2 3⍴⍳6`; with a left arg too, `Q+.×⍠e⁝r`, or group the derived fn, `10 20(+⍤1)m`: whichever reads better. Keep compound operands grouped: `⌊@(10×)x`. Add nothing where tokens already stay distinct: `+/⍤1m`. - [Agreement](evaluation.html.md#agreement-and-pervasion): pervasive fns, Each, Rank align leading axes: `[1 2 3⋄4 5 6]+10 20`→`[11 12 13⋄24 25 26]`. length-1 axes expand. - [Maps](evaluation.html.md#functions-and-effects): Each, Rank, stencils, outer products call a dfn once on all cells where possible: `{⍵×⍵+1}¨v` costs ≈ `v×v+1`. value predicate: each body once, on its cells. output, nested result, value-dependent shape: a call per cell. dfn fold/scan along matrix rows: a call per column, on all rows. `f.g`: dfn `g` once, on all pairings. @@ -24,12 +24,12 @@ Entry: Dyalog diff, example, defining page. Not the reference: read the page bef - [Strings](strings.html.md): `"abc"` char vector; `'a'` char atom; `'abc'` error. - [Indexing](indexing.html.md): no `Y[I]`. `I⌷A` cross product; `A.[I J]` pairs index arrays (NumPy): `i←⍳3ₓ`: `m.[i i]` diagonal. positions from 0; negative from end. - [Numbers](numbers.html.md): bare number: float (Python `1.`). `3ₓ` [weak](numbers.html.md#weak-and-strong-numbers) ordinary int, exact (default dynamic kinds). `ₓ₁ ₓ₂ ₓ₄ ₓ₈`: fixed `u8 i16 i32 i64`, wrap. `vₓ`: ordinary ints (dynamic: exact, shares `v`'s data; static: `int` width). `1r3` rational. dynamic default Mathish: `0÷0`→`0` for [Mathish types](terms.html.md#term-mathish-types), tolerant f64 compare; `•prefs ["mathish":$f]` disables both. pervasive float fns within [4 ulp](numbers.html.md#accuracy) of correctly rounded, except those rustymath lists. not promised, vary by platform, run, threads, size, route, spelling: last bits (`+/` too), NaN results, zero's sign. -- [Scripts](scripts.html.md): `x²`≡`x*2`; `f⁻¹` inverse; `v₁`≡`1⌷v`; `a₁ ₂`≡`a.[1 2]`, `a₁₂` index 12, `(a₁)₂` sequential; `f₁ ₂`≡`f⍠[1 2]` selects axes; `nₓ`≡`⌊ₓn`. `ₓ` after fn floors results to exact: `7÷ₓ2`→`3ₓ`. between names: `xⱼy`≡`x+y×0ⱼ1`; `xₑy`≡`x×10*y`; `xᵣy`≡`xₓ÷yₓ`, exact. +- [Scripts](scripts.html.md): `x²`≡`x*2`; `f⁻¹` inverse; `v₁`≡`1⌷v`; `a₁ ₂`≡`a.[1 2]`, `a₁₂` index 12, `(a₁)₂` sequential; `f₁ ₂`≡`f⍠[1 2]` selects axes; `nₓ`≡`⌊ₓn`. `ₓ` after fn floors results to exact: `7÷ₓ2`→`3ₓ`. `ₐ` converts to real floats at the args' float width, the real part outside the real domain: `√ₐ¯4 9`→`0 3`, `(3j4)ₐ`→`3`. between names: `xⱼy`≡`x+y×0ⱼ1`; `xₑy`≡`x×10*y`; `xᵣy`≡`xₓ÷yₓ`, exact. - [Keyed](keyed.html.md): axes have names, keys. `T.a` record field; `⊢.a`≡`{⍵.a}`. arithmetic pairs axes by name: `i←⍳["i":3]`, `j←⍳["j":3]`: `i+j` 3×3. -- [Autodiff](autodiff.html.md): how `∂` works. one number in `X`: forward mode, tagged duals. more: one run records linear steps, then reads the Jacobian by transposes or forward runs, whichever fewer: a loss's gradient is one transpose. selection transposes to accumulation. `∂` of a dfn traces a program on the first call with given types and shapes, replayed after: `(•explain t).programs`. a trace stops at effects, random draws, `⍎`, guards on differentiated values, result types that depend on values (dynamic kinds: `√`): `(•explain t).stopped`. write `∂` and fast code for static kinds, branching by `⍚` or `f@[p]`, not guards; today those also stop a trace on differentiated values (planned). +- [Autodiff](autodiff.html.md): how `∂` works. one number in `X`: forward mode, tagged duals. more: one run records linear steps, then reads the Jacobian by one batched transpose over its rows or one batched forward run over its columns, whichever fewer: a loss's gradient is one transpose. selection transposes to accumulation. `∂` of a dfn traces a program on the first call with given types and shapes, replayed after: `(•explain t).programs`. a trace stops where its program can't hold the call: effects on the session or giving text (`⎕←` `•time` `•delay` `•nput` record), draws in maps, deals, `⍎`, reads of differentiated items (guards, `⍚` selectors), value-dependent types where the first run gives the other type (dynamic kinds: `√` of a negative; a replay checks the range its trace assumed, and traces again outside it), not shapes: [list](autodiff.html.md#what-stops-a-trace), `(•explain t).stopped`. write `∂` and fast code for static kinds, branch and update with approaches like `⍚`, `f@g`, `f@[p]`, `x.[p]←`, not guards. - [Index notation](index-notation.html.md): the formula for one item, run on whole arrays; tacit stays the main style. indices: `[[n i] h]←•index[⍴x ≢b]`; select: `x.[n i]`; contract along an index: `a←7○b.[h]+x.[n i]+.×⍠i⁝w.[i h]`. same index pairs, different indices build a grid: `x.[b k]+.×⍠k⁝w.[k o]`≡`+/⍠k⁝x.[b k]×w.[k o]`≡`x+.×w`, run as a product. read under a new index once `:` removes names: `[q k v]←:z ⋄ k.[b u h j]`. index arithmetic reads as a map: `wo.[j+(≢j)×h d]`. - [Dfns](dfns.html.md): use a dfn only where a tacit form is longer or clearly awkward: `+/÷≢ v`, not `{+/⍵ ÷ ≢⍵}v`. no tradfns, no `:If` etc. last statement returns. predicate picks body (BQN): `{⍵<0?-⍵;⍵} ¯3`→`3`. absent `⍺` drops out (BQN `𝕨`). operator operands `⍶` `⍹`, not `⍺⍺` `⍵⍵`. `⍢` recursive operator call. error guard kinds as text: `"DOMAIN"::`. -- [Assignment](assignment.html.md): `←` local. modified/selective updates: nearest binding. `[a b]←Y` destructures items by position. space before `←`: whole run is target: `[[0 2]]⌷v ←0`. +- [Assignment](assignment.html.md): `←` local. modified/selective updates: nearest binding. `[a b]←Y` destructures items by position. repeated position: which value it keeps isn't promised. space before `←`: whole run is target: `[[0 2]]⌷v ←0`. - [Modules](modules.html.md): `•load "file.bpl"` runs module, gives record of public names. `_` names private. - [Hanzi](syntax.html.md#hanzi-spellings): hanzi are name chars. leading `•prefs ["hanzi_aliases":$t]` enables aliases: `加总` for `+/`. non-alias hanzi: names. `_` quotes next non-space char into name: `_加_总`. - [Constants](system/constants.html.md): `$` names: `$t $f $n` true false NaN; `$a` uppercase alphabet; `$d` digits. @@ -96,7 +96,7 @@ Entry: Dyalog diff, example, defining page. Not the reference: read the page bef ## System functions - [System functions](system-functions.html.md): names ignore case. options: usually keyed left arg. format fn reads, inverse writes: `•json` `•json⁻¹`. page `system/.html.md`, no `•`: read before use. -- [Text](system-functions.html.md#text): `•c` `•ucs` `•normalize` `•r`. +- [Text](system-functions.html.md#text): `•c` `•ucs` `•normalize` `•r` `•replace`: `text •r pattern` matches, `text •replace [pattern template]`. - [Data and files](system-functions.html.md#data-and-files): `•csv` `•json` `•literal` `•vfi` `•date` `•nget` `•nput` `•fetch` `•path` `•metadata` `•readdir` `•copy` `•rename` `•remove` `•mkdir` `•deflate` `•hash` `•uuid`. - [Display](system-functions.html.md#display): `•element` `•xml` `•svg` `•mime` `•plot` `•image` `•canvas` `•prefs`. - [Linear algebra](system-functions.html.md#linear-algebra): `•decompose`. diff --git a/nbs/numbers.qmd b/nbs/numbers.qmd index 9f779457..b0ba9952 100644 --- a/nbs/numbers.qmd +++ b/nbs/numbers.qmd @@ -225,7 +225,7 @@ Some results are exact. Powers, roots and cube roots whose exact result is a flo •prefs ["mathish":$t] ``` -Last bits aren't reproducible. A calculation can give results that differ in their last bits on another platform or build, in another run, with another number of threads, at another array size, or by another route, such as one fused loop in place of separate calls. `+/` adds in an order that depends on the array's size, the number of threads and the route. The result at a NaN argument, which NaN it is and the sign of a zero result can differ in the same ways, as rustymath's [Accuracy page](https://answerdotai.github.io/rustymath/accuracy.html) describes. `⌈` and `⌊`, for example, can give either argument where one is NaN or both are zeros of opposite signs. Compare floats from different routes within a tolerance, as Mathish mode's `=` does. +Last bits aren't reproducible. A calculation can give results that differ in their last bits on another platform or build, in another run, with another number of threads, at another array size, or by another route, such as one fused loop in place of separate calls. `+/` adds in an order that depends on the array's size, the number of threads and the route. The result at an infinity or a NaN argument, which NaN it is and the sign of a zero result can differ in the same ways, as rustymath's [Accuracy page](https://answerdotai.github.io/rustymath/accuracy.html) describes. FMA and reassociation can overflow differently near the range limits. Finite results, including subnormals, retain the accuracy bounds above. `⌈` and `⌊`, for example, can give either argument where one is NaN or both are zeros of opposite signs. Compare floats from different routes within a tolerance, as Mathish mode's `=` does. A form that BPL doesn't recognise as a [fast combination](terms.qmd#term-fast-combination), such as one that names a constant, can give different digits, beyond the last bits where the combination is more accurate. `⍟1+x` computes through `log1p` and keeps the digits of a tiny `x`. `one←1 ⋄ ⍟one+x` computes `⍟` of `1+x`, which loses them. @@ -288,7 +288,7 @@ Some functions choose a result's kind from their arguments' values. `√¯4` is •prefs ["kinds":"dynamic"] ``` -Ordinary integers compute at the width that the `int` pref sets, 32 bits by default, or at a wider integer's width, and wrap where a result doesn't fit. An ordinary integer is a fixed integer of the `int` width. Display marks a fixed integer of that width with plain `ₓ`. Their storage takes that width too, whatever their values. Code that wants narrow storage writes a fixed type, such as `ₓ₁`. New floats take 32 bits, unless the `float` pref sets a width. Fixed integers retain their explicit type. An integer beside a float takes the float's width. A weak number beside floats takes their width, as under dynamic kinds. Exact fractions stay exact. `⌹` and polar `∧` give integers floats of the width that `÷` gives them. A roll of integers gives integers, and a bound of 0, which rolls a float, is a DOMAIN error. Floating comparisons are exact. An ordinary integer literal that doesn't fit the `int` width is a LIMIT error. [`•csv`](system/csv.qmd) and [`•json`](system/json.qmd) read integers at the `int` width too, and their `types` option gives a column another type: +Ordinary integers compute at the width that the `int` pref sets, 32 bits by default, or at a wider integer's width, and wrap where a result doesn't fit. An ordinary integer is a fixed integer of the `int` width. Display marks a fixed integer of that width with plain `ₓ`. Their storage takes that width too, whatever their values. Code that wants narrow storage writes a fixed type, such as `ₓ₁`. New floats take 32 bits, unless the `float` pref sets a width. Fixed integers retain their explicit type. An integer beside a float takes the float's width. A weak number beside floats takes their width, as under dynamic kinds. Exact fractions stay exact. `⌹` and polar `∧` give integers floats of the width that `÷` gives them. A roll of integers gives integers, and a bound of 0, which rolls a float, is a DOMAIN error. Floating comparisons are exact. An ordinary integer literal that doesn't fit the `int` width is a LIMIT error, and so is a count or position that doesn't fit it, such as the length that `≢` gives of an axis too long for it. [`•csv`](system/csv.qmd) and [`•json`](system/json.qmd) read integers at the `int` width too, and their `types` option gives a column another type: ```bpl •prefs ["kinds":"static"] @@ -320,6 +320,41 @@ v←[1 2 3]ₓ₁ ⋄ v.[[1]]←0.5 ⋄ (•storage v).kind ⍝ "float32" •prefs ["kinds":"dynamic"] ``` +Results computed apart join the same way: the cells of a map, such as Each, Rank, an outer product, a stencil or Key, the states that `⍣` keeps for a list of counts, and the cases of a compacting select, such as `⍚` or a guard. Under static kinds the result takes one compact type, so its type doesn't depend on which case each cell picks, or on whether the map ran on all its cells at once. Under dynamic kinds, each cell's result keeps its own kind: + +```bpl +{(0<⍵)⍚[(1<) (2×)]⍵}¨¯1.5 2 3 ⍝ $f 4 6 +•prefs ["kinds":"static"] +{(0<⍵)⍚[(1<) (2×)]⍵}¨¯1.5 2 3 ⍝ 0 4 6 +•prefs ["kinds":"dynamic"] +``` + +## Approximate + +`ₐ` converts to real floats, as `ₓ` converts to exact integers. After a value, a real float keeps its width, a complex number gives its real part at its parts' width, and an exact number or a Boolean gives a float of the default width: + +```bpl +(3j4)ₐ ⍝ 3 +1r3ₐ ⍝ 0.3333333333 +x←(1.5 2)ₛ ⋄ (•storage xₐ).kind ⍝ "float32" +``` + +After a function, `fₐ` calls `f` and converts its results to real floats of the width that the arguments' float promotion gives, weak literals included. Float32 arguments give float32 results, even where `f` itself gives float64 or complex numbers. The width follows from the arguments' kinds and the preferences, never from values. Outside the real domain a result is the real part of the principal value. `√ₐ¯4` is 0, the real part of `0j2`. With a negative base, `¯8*ₐ÷3` is 1, the real part of the principal root 1+i√3, not `¯2`. The real part of arcsin of 2 is π÷2: + +```bpl +√ₐ¯4 9 ⍝ 0 3 +⍟ₐ¯1 ⍝ 0 +¯8*ₐ÷3 ⍝ 1 +1○⁻¹ₐ 2 ⍝ 1.570796327 +x←(4 9)ₛ ⋄ (•storage √ₐx).kind ⍝ "float32" +``` + +`√ₐ`, `⍟ₐ` and `*ₐ` each run as one kernel, and so do `○ₐ` and `○⁻¹ₐ` with a constant code. Because their result's kind never depends on values, they fuse and trace under dynamic kinds as they do under static kinds. Under [`∂`](glyphs/partial.qmd), their slopes are the real part's: `√ₐ` has slope 0 at and below 0, `⍟ₐ` has slope 1÷x, and arcsin's real part has slope 0 beyond ¯1 and 1. Any other `fₐ` converts the results that `f` gives. Under static kinds, a NaN that `f` gives outside its domain stays NaN: + +```bpl +{+/√ₐ⍵}∂¯1 4 ⍝ 0 0.25 +{+/1○⁻¹ₐ⍵}∂¯2 0.5 ⍝ 0 1.154700538 +``` ## Booleans Comparisons give Booleans. So does every function whose result is a truth value, such as `~`, `∊`, `⍷` and `≡`. `$t` is true and `$f` is false: diff --git a/nbs/performance.qmd b/nbs/performance.qmd index 4c6af094..b5e0797f 100644 --- a/nbs/performance.qmd +++ b/nbs/performance.qmd @@ -1,101 +1,107 @@ -# Performance +# Writing fast BPL -BPL interprets each function call once, then runs it on the whole array in compiled Rust. Interpreting a call costs about the same whatever the size of its arguments. On large arrays, the time goes into the data. BPL also runs chains of calls on large arrays together, in one pass over memory, and compiles that pass into machine code. Static kinds let more calls take these routes. +Write hot numerical code for static kinds, give data suitable widths, and express branches and updates with array operations. Whole-array expressions, trains and dfns use the same graph. Choose the spelling that reads best. -## Static kinds +This guide describes the new core under construction. Its execution and cost claims are design targets, not measured performance promises for the released implementation. [Computational foundations](foundations.qmd) explains the design. -Dynamic kinds, the default, suit interactive analysis. Integers are exact and never wrap, and some results take their kind from their values, as `√¯4` gives a complex number. Static kinds, which `•prefs ["kinds":"static"]` sets, are the setting for performance work. Each result's kind follows from its arguments' kinds. BPL then knows the type of every result before a computation runs. More calls can run together. Under dynamic kinds, `⌊` gives exact integers and runs on its own. Under static kinds, it gives floats and runs with the multiplication and the sum: +## Static kinds and widths + +Put the setting at the start of a file or notebook cell. Choose widths for data and constants that the calculation will keep: ```bpl -y←¿200000⍴0 -(•explain "+/⌊10×y").fused.glyphs ⍝ 0⍴⊂"" •prefs ["kinds":"static"] -(•explain "+/⌊10×y").fused.glyphs ⍝ ["×⌊+/"] -•prefs ["kinds":"dynamic"] +x←[1 2 3]ₛ +i←[0 1 2]ₓ₄ +rate←0.01ₛ ``` -Code meant for static kinds gives each integer the width that its values need and gives `•csv` and `•json` a `types` record for their columns. [Numbers](numbers.qmd#static-kinds) shows how code written for dynamic kinds can give other results under static kinds. +`ₛ` and `ₚ` select 32-bit and 64-bit floats. `ₓ₄` and `ₓ₈` select signed 32-bit and 64-bit integers. Fixed integers wrap on overflow. Choose a width that holds every intermediate you need, not only the inputs. Ordinary exact `ₓ` is not an explicit storage width. -## Calls that wait +Convert imported data once, or give its columns types when reading it. Keep loop state and branch results at compatible kinds and widths. A strong 64-bit float beside 32-bit data widens the arithmetic. A weak literal takes its partner's width. [Numbers](numbers.qmd) gives the promotion rules. -An elementwise call on 65,536 numbers or more doesn't compute at once. It waits, and the calls that read its result join it. When something needs the items, the whole chain runs together, block by block. It reads each input array once and writes only what it must keep. A chain of arithmetic that ends in a reduction runs in about the time that one copy of its data takes. [`•explain`](system/explain.qmd) lists the calls of each group that ran together: +A wider reduction seed widens its accumulator without widening the stored input: ```bpl -y←¿1000000⍴0 -(•explain "+/ 2×y+1.5 - 1").fused.glyphs ⍝ ["+×-+/"] +0ₓ₈+/i ``` -Waiting costs a fixed amount of planning on each call. It pays only on large arrays. +A result conversion such as `(+/)ₓ₈` happens after the reduction and cannot recover an overflow. -Structural functions such as `⌽`, `⍉`, `↑`, `↓` and `↕` wait too, on large arrays of any kind. They hold only where each item of their result comes from. Such a view stays a view until a call needs its items in memory. Calls that run together read through it without building it: +## Choose the intended numerical operation -```bpl -w←1000↕¿1000000⍴0 -(•storage w).pending ⍝ $t -``` +Use the real forms `√ₐ`, `*ₐ`, `⍟ₐ` and `○ₐ` where values can leave a real domain and you want real results. These forms, including inverse-circle real forms, have value-independent result kinds and are required fast paths. Unmarked `√`, `*`, `⍟` and `○` have no such speed guarantee. The real forms preserve the arguments' promoted float width. -[`•prefs`](system/prefs.qmd) lists the glyphs whose calls run together, under `fusion`. Any other call, such as `⍋`, computes the waiting items it reads first. So does display. The `fusion` setting writes a line where each group ends. A waiting call keeps the settings in force when it was called. +`fₐ` gives the real part of the principal result outside the real domain. For example, `√ₐ¯4` gives 0, not NaN or an error. Choose that meaning intentionally. Use Agenda when different inputs need different calculations. -## Compiled loops +`a÷ₓb` floors the quotient to an exact integer. It is the fast form for floor division, not exact rational division and not truncation towards zero. For a floating quotient, convert integer data to the intended float width before dividing. Use ordinary `÷` on exact integers when you need its numerical meaning, rather than replacing it for speed. -A group of 3,145,728 items or more compiles its loop into machine code. The compiled loop keeps intermediate values in registers. A smaller group compiles too where the process has already compiled its loop. A compile takes about a millisecond. `•explain` records whether each group compiled: +16-bit and complex operations use whole-array kernels, but this core does not fuse their steps into compiled block stages. Narrower storage alone does not guarantee faster execution. -```bpl -y←¿3145728⍴0 -(•explain "+/ 2×y+1.5 - 1").fused.compiled ⍝ [$t] -``` +Fusion and reassociation can change last bits, infinities and NaNs, and the sign of zero. They still follow the finite-result accuracy bounds and exact results on [Numbers](numbers.qmd#accuracy). The [target's approved numerical direction](target.qmd#numerical-algorithms) has not established new measured bounds. -The browser build compiles nothing at run time. Its groups run on kernels built into the module, as [BPL on the web](web.ipynb#differences-from-native-bpl) describes. +## Work on arrays; write readable functions -## Maps +These spellings express the same sum of squares: -Each, Rank, stencils and outer products call a dfn once, on all their cells together, where they can. `{⍵×⍵+1}¨v` then costs about what `v×v+1` costs. [Evaluation](evaluation.qmd#functions-and-effects) gives the rules. With the `batch` setting at `$f`, a map calls its operand once for each cell. +```bpl +loss←+/⊼ +loss x +{+/⊼⍵}x +``` -## The cost of a call +Calling a dfn does not impose a different numerical execution route. Each, Rank and stencils can batch a function over its cells instead of calling an interpreter for every item. Prefer those operations to a user-written item loop. [Maps](evaluation.qmd#functions-and-effects) explains their cell model. -Each call pays a fixed cost to interpret it, whatever the size of its arguments. A call of `{⍵}` costs about 1,500 machine instructions. Each call of a recursive Fibonacci function costs about 4,600. A loop over the items of a million-item array pays that cost a million times. Code that works on whole arrays pays it once for each call. Compiling doesn't remove this cost. Only the loops inside a group compile. +Use [index notation](index-notation.qmd) when naming the axes makes a formula easier to express. Named selections and contractions participate in the same read-mapping and product rules as whole-array spellings. Index notation is an option, not a requirement for speed. -## Names and writes +Spaces are semantic. `+/÷≢ x` applies the mean train; `+/÷≢x` means something else. Use `⁝` to keep an operator's operand distinct from its argument without ending the run. Keep parentheses that clarify a compound operand. Follow [Composition](composition.qmd) and [Style](style.qmd), rather than removing punctuation to make code look faster. -A dfn releases each local name after the last statement that reads it. A waiting computation that only the name held then runs with the calls that read it. In `f`, the arithmetic and the sum run as one group, and nothing is stored on the way: +## Select cases and update selected data + +[Agenda](glyphs/diamond-underbar.qmd) chooses a calculation for each cell: ```bpl -big←¿1000000⍴0 -f←{a←⍵×2 ⋄ b←a+1 ⋄ +/a×b} -(•explain "f big").fused.glyphs ⍝ ["×+×+/"] -≢(•explain "f big").stored.glyphs ⍝ 0ₓ +(0≤)⍚[0 √ₐ] x ``` -[Dfns](dfns.ipynb#scope) describes what a released name means for `⍎`. +This gives zero for negative items and their square roots otherwise. Use Agenda instead of dfn guards on traced values in hot code. Static elementwise cases can use masked execution. Cases needing cross-item work, differing layouts or effects gather their selected cells first. Checked dynamic cases that can raise a data error gather their live cells from the outset. -A write to part of an array that nothing else holds changes the array in place. `x,←v` and `x⍪←v` add to the end of the array's storage. `x←x,v` copies the whole array, because `x` still holds the old array while `x,v` is computed. [Assignment](assignment.ipynb) describes each form of write. +[Under](glyphs/at.qmd) applies a function to a selection and writes its results back. `f@g` uses the selection that `g` makes. `f@[p]` uses the cells that predicate `p` picks. For example, `0@[0>]x` replaces negative items with zero. -## Threads +Preallocate a cache and update its selected rows instead of rebuilding it at every step. Here `p` is the row position and `x+.×Wk` gives a row of 64 items: -Large calls split across the machine's cores. A call splits only when it has enough work to repay the extra threads. The `threads` setting sets how many threads a call can use. Several BPL processes running at once compete for the cores unless each sets `threads` to 1. The browser build runs every call on one thread. +```bpl +K←[1000 64]⍴0ₛ +K.[p]←x+.×Wk +``` -## Seeing what happened +Keep the update's kind compatible with the cache. Storage can be reused when no other value needs the old array. An alias or an observation of the old value can require a copy. [Assignment](assignment.ipynb) covers writes and value semantics. -[`•explain`](system/explain.qmd) runs code and records each route it took: groups that ran together, compiles, calls that computed waiting items, and maps that fell back to a call for each cell. [`•memory`](system/memory.qmd) counts the waiting arrays and other values that a thread holds, and the bytes that a value keeps alive. [`•storage`](system/storage.qmd) shows how an array is held, and whether it's still waiting. +Under static graph-numeric execution, invalid numeric indexed reads give the fill and invalid numeric writes skip those positions. Dynamic execution checks them. Shape, kind, name and host errors remain checked in both modes. Keep cache positions within their intended bounds. -## Derivatives +## Keep repeated work inside a loop -[`∂`](glyphs/partial.qmd) traces the function it differentiates into a program of typed steps. The program runs through the same groups and compiled loops. Later calls replay it while their arguments and free names still fit it. Under dynamic kinds, a call whose result type can depend on its values stops a trace. On reals, those calls are `√` and every root but the cube root `3√`, `⍟`, `!`, `0○`, and `*` whose exponent isn't a whole number or is the value being differentiated. `∂` then runs without a program: +[Power](glyphs/star-diaeresis.qmd) expresses a repeated step as one loop with a recorded body: ```bpl -(•explain "{+/√⍵}∂1 2 3").programs.event ⍝ 0⍴⊂"" -•prefs ["kinds":"static"] -(•explain "{+/√⍵}∂1 2 3").programs.event ⍝ ["built"] -•prefs ["kinds":"dynamic"] +n←100ₓ₄ ⋄ step⍣n⁝params ``` -[Automatic differentiation](autodiff.qmd) describes how `∂` works. +Here `step` is the update function and `params` its initial state. A Python loop that repeatedly calls BPL rebuilds each call's graph. Keeping iteration in BPL avoids that construction work. A loop's derivative and batching rules transform its body through the same execution system. + +Stable kinds and shapes across calls and iterations make reuse easier. A changed length does not automatically compile another version. The length key distinguishes 0, 1 and more; within a class, refill updates concrete shapes and read mappings while the plan's other assumptions hold. Changed kinds, ranks, structural parameters, axis equalities or range assumptions can require another plan. The JIT compiles worthwhile structural versions on demand, with counts passed at runtime. + +A growing state needs more allocation and copying than a fixed-capacity cache. Preallocation is often clearer and cheaper even when refill permits plan reuse. + +## Understand reads and recording stops + +An assignment or local name does not force a numerical result. A read computes the pending work it reaches. Display, conversion to Python, a guard reading an item value, structural parameters, data-dependent counts and operand reads required by a call are read boundaries. A data-dependent length can continue through count read and refill without ending the recording. Output and other effects force only their own operands and keep their order. + +Recording stops at host control flow depending on traced values, calls with host-kind data, recursion, `⍎`, error guards, writes to outer bindings and effects that change bindings or settings. A stopped recording runs ordinary code. Under `∂`, writing a differentiated value to an outer binding is an error. A dfn guard on a framed value can become a select, but guards on traced values are not fast-code targets. [Automatic differentiation](autodiff.qmd) covers differentiation-specific boundaries. + +Dynamic kinds remain intended for interactive analysis. The design target is correct execution within four times the equivalent static-kind code at the same size, except agreed cases. An ordinary run builds nodes and looks up plans. A cache miss also schedules the graph. Recorded bodies reuse that construction and scheduling. Neither static kinds nor the JIT makes every small call free. -## Not built yet +## Inspect the work -The plan includes: +Use [`•explain`](system/explain.qmd) on a representative expression. Inspect `stopped` and `fallbacks` for host execution, `forced` for intermediate reads, `stored` for materialized intermediates, and `programs` and `compiles` for reuse. A stored value is sometimes the right schedule; investigate its reason instead of trying to eliminate every record. -- One pass for several reductions of the same data. Today each reduction, as in `+/v ÷ ⌈/v`, makes its own pass. -- A tracing point for whole training steps, such as `⍣`. A repeated step's forward pass, gradient and update would then run as one program. -- Programs for maps whose cells take different branches, so that `∂` can trace them. -- The GPU, through WebGPU, with 16-bit brain floats (bf16), and compiled code in the browser. +[`•memory`](system/memory.qmd) reports retained values and bytes. [`•storage`](system/storage.qmd) shows how an array is held. When several Python worker processes compete for cores, [`•prefs`](system/prefs.qmd) with `"threads":1` keeps each on one thread. The browser runs block stages without the native JIT. diff --git a/nbs/python.ipynb b/nbs/python.ipynb index 052e7d36..0c83d03b 100644 --- a/nbs/python.ipynb +++ b/nbs/python.ipynb @@ -1670,7 +1670,7 @@ "id": "02b913b9", "metadata": {}, "source": [ - "`r` compiles a regex into keyed functions, which Python indexes by name:" + "`r` gives a keyed table of a regex's matches in a text, whose columns Python reads by name. `replace` replaces the matches:" ] }, { @@ -1691,8 +1691,8 @@ } ], "source": [ - "p = b.r(r'([a-z]+)([0-9]+)')\n", - "p['replace']('$2:$1', 'ab12')" + "m = b.r('ab12 cd34', r'([a-z]+)([0-9]+)')\n", + "m['text'], m['position'], b.replace('ab12', [r'([a-z]+)([0-9]+)', '$2:$1'])" ] }, { diff --git a/nbs/regex.ipynb b/nbs/regex.ipynb index 7696cb2a..41a1c9d5 100644 --- a/nbs/regex.ipynb +++ b/nbs/regex.ipynb @@ -6,7 +6,7 @@ "metadata": {}, "source": [ "# Regex\n", - "> Search and replace text with compiled regular expressions" + "> Search and replace text with regular expressions" ] }, { @@ -61,7 +61,7 @@ "id": "f6464060", "metadata": {}, "source": [ - "Natively, patterns use [fancy-regex syntax](https://docs.rs/fancy-regex/latest/fancy_regex/#syntax), including lookaround and backreferences. Exceeding the engine's backtracking limit raises `DOMAIN`. In the browser they use [JavaScript's](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions). `p.matches` gives a table of the matches, as a record of columns. Here `p` finds runs of digits:" + "Natively, patterns use [fancy-regex syntax](https://docs.rs/fancy-regex/latest/fancy_regex/#syntax), including lookaround and backreferences. Exceeding the engine's backtracking limit raises `DOMAIN`. In the browser they use [JavaScript's](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions). `text •r pattern` gives a table of the matches, as a record of columns. This pattern finds runs of digits:" ] }, { @@ -81,8 +81,7 @@ } ], "source": [ - "p←•r \"[0-9]+\"\n", - "p.matches \"abc123def45\"" + "\"abc123def45\" •r \"[0-9]+\"" ] }, { @@ -128,10 +127,10 @@ } ], "source": [ - "m←(•r \"é|🐈+\").matches \"aé🐈🐈z\"\n", + "m←\"aé🐈🐈z\" •r \"é|🐈+\"\n", "m.position\n", "≢¨m.text\n", - "(•r \"é|🐈+\").matches \"abc\"" + "\"abc\" •r \"é|🐈+\"" ] }, { @@ -159,8 +158,7 @@ } ], "source": [ - "p←•r \"([a-z]+)([0-9]+)\"\n", - "(p.matches \"ab12 cd3\").groups" + "(\"ab12 cd3\" •r \"([a-z]+)([0-9]+)\").groups" ] }, { @@ -168,7 +166,7 @@ "id": "7d563b63", "metadata": {}, "source": [ - "`template p.replace text` replaces each match with `template`. In a native template, `$0` is the whole match and `$$` is a literal `$`. An unknown or unmatched capture expands to `\"\"`. In the browser, templates use JavaScript's syntax, where `$` names a group:" + "`text •replace [pattern template]` replaces each match of `pattern` with `template`. In a native template, `$0` is the whole match and `$$` is a literal `$`. An unknown or unmatched capture expands to `\"\"`. In the browser, templates use JavaScript's syntax, where `$` names a group:" ] }, { @@ -188,8 +186,7 @@ } ], "source": [ - "p←•r \"(?P[a-z]+)([0-9]+)\"\n", - "\"${word}:$2:$$\" p.replace \"ab12\"" + "\"ab12\" •replace [\"(?P[a-z]+)([0-9]+)\" \"${word}:$2:$$\"]" ] }, { @@ -217,8 +214,7 @@ } ], "source": [ - "p←•r \"(?i)cat\"\n", - "{≢(p.matches ⍵).text}¨\"Cat cat\" \"dog\"" + "{≢(⍵ •r \"(?i)cat\").text}¨\"Cat cat\" \"dog\"" ] }, { @@ -255,9 +251,8 @@ } ], "source": [ - "p←•r \"\"\n", - "(p.matches \"é🐈\").position\n", - "'-' p.replace \"é🐈\"" + "(\"é🐈\" •r \"\").position\n", + "\"é🐈\" •replace [\"\" \"-\"]" ] }, { diff --git a/nbs/scripts.ipynb b/nbs/scripts.ipynb index 7a8a26b6..c028e476 100644 --- a/nbs/scripts.ipynb +++ b/nbs/scripts.ipynb @@ -1140,6 +1140,44 @@ "xₛ" ] }, + { + "cell_type": "markdown", + "id": "e2eef28d", + "metadata": {}, + "source": [ + "`ₐ` (approximate) converts to real floats. After an array, a complex number gives its real part at its parts' width, an exact number or a Boolean gives a float of the default width, and a float keeps its width:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8498dcc7", + "metadata": {}, + "outputs": [], + "source": [ + "(3j4)ₐ\n", + "1r3ₐ" + ] + }, + { + "cell_type": "markdown", + "id": "dbe71e6f", + "metadata": {}, + "source": [ + "After a function, `ₐ` converts the results to real floats of the width that the arguments' float promotion gives, never a width chosen from values. Outside the real domain a result is the real part of the principal value. `√ₐ¯4` is 0, the real part of `0j2`. `¯8*ₐ÷3` is 1, the real part of the principal root 1+i√3. `√ₐ`, `⍟ₐ` and `*ₐ` each run as one kernel. Under [`∂`](glyphs/partial.qmd), their slopes are the real part's. [Numbers](numbers.qmd#approximate) gives the rules:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da3f4820", + "metadata": {}, + "outputs": [], + "source": [ + "√ₐ¯4 9\n", + "¯8*ₐ÷3" + ] + }, { "cell_type": "markdown", "id": "0181955a", diff --git a/nbs/sidebar.yml b/nbs/sidebar.yml index 2e7fd22e..7001c732 100644 --- a/nbs/sidebar.yml +++ b/nbs/sidebar.yml @@ -25,6 +25,8 @@ website: - overview.ipynb - href: glyphs-study.qmd text: Glyphs Study Guide + - foundations.qmd + - performance.qmd - principles.ipynb - arrays.ipynb - indexing.ipynb @@ -40,7 +42,6 @@ website: - keyed.ipynb - autodiff.qmd - index-notation.qmd - - performance.qmd - section: Working with data contents: - data.ipynb diff --git a/nbs/study/05_searching-indexing.ipynb b/nbs/study/05_searching-indexing.ipynb index 82cd1d68..06aad51c 100644 --- a/nbs/study/05_searching-indexing.ipynb +++ b/nbs/study/05_searching-indexing.ipynb @@ -2277,7 +2277,7 @@ "id": "db71454a", "metadata": {}, "source": [ - "Plain assignment to a repeated position keeps its last replacement:" + "Plain assignment can repeat a position. Which of its values the position keeps isn't promised, so here they agree:" ] }, { @@ -2297,7 +2297,7 @@ } ], "source": [ - "v.[[0 0 $n]]←7 8 99\n", + "v.[[0 0 $n]]←7 7 99\n", "v" ] }, @@ -2497,7 +2497,7 @@ "id": "058d380b", "metadata": {}, "source": [ - "Repeated positions are allowed on unkeyed axes. A selection that repeats a key is a Domain error, because an axis cannot have duplicate keys." + "An assignment can repeat a position, or a key, and which value it keeps there isn't promised. A read that repeats a key is a Domain error, because an axis holds each key once." ] }, { diff --git a/nbs/syntax.qmd b/nbs/syntax.qmd index e3b21064..757be786 100644 --- a/nbs/syntax.qmd +++ b/nbs/syntax.qmd @@ -68,6 +68,8 @@ BPL reads a statement in three steps: So `a+b × c+d` is `(a+b)(×)(c+d)`, and `1 2+3` is `(1 2)+3`, not `(1)(2+3)`. Spaces around a function between two runs decide the grouping. `a×b + c` is `(a×b)(+)(c)`, the sum of `a×b` and `c`. `a×b +c` is `(a×b)(+c)`, a strand of two subjects. `a×b+ c` is `(a×b+)(c)`, the train `a×b+` applied to `c`. +A call's result binds where a subject would, whatever it returns, and so do a field read from a call's result, a subscript's selection and a scope's result: `(f 0) 1 2` is a strand even where `f 0` returns a function. [Evaluation](evaluation.qmd#binding) shows how to call a function that a call returns. + A run that ends in a dyadic operator takes the next run as its right operand: `⌊@ 10× x` is `(⌊@)(10×)(x)`, where `10×` is the operand of `@`. A run that starts with an operator takes the previous run as its left operand: `≢⊢ ⌸x` is `(≢⊢)(⌸x)`, where `≢⊢` is the operand of `⌸`. [Evaluation](evaluation.qmd) explains operands. ```bpl diff --git a/nbs/system-functions.qmd b/nbs/system-functions.qmd index 77319cb4..18ff173c 100644 --- a/nbs/system-functions.qmd +++ b/nbs/system-functions.qmd @@ -13,7 +13,8 @@ In Python, the workspace object `bpl` has each system function under its name wi | [`•c`](system/c.qmd) | Case | | [`•ucs`](system/ucs.qmd) | Unicode | | [`•normalize`](system/normalize.qmd) | Unicode normalization | -| [`•r`](system/r.qmd) | Regular expressions | +| [`•r`](system/r.qmd) | Regular expression matches | +| [`•replace`](system/replace.qmd) | Regular expression replacement | ## Data and files diff --git a/nbs/system/distribution.qmd b/nbs/system/distribution.qmd index 3f129c6c..6f0bb57d 100644 --- a/nbs/system/distribution.qmd +++ b/nbs/system/distribution.qmd @@ -2,7 +2,7 @@ `params •distribution name` returns the distribution called `name`, with parameters `params`. `•distribution name` uses the distribution's standard parameters, listed in the table below. A distribution is a keyed vector of four functions: -- `sample shape` draws random values from the session's stream, as [`¿`](../glyphs/inverted-question.qmd) does. `g sample shape` draws them from a generator made by `•rand`. +- `sample shape` draws random values from the session's generator, as [`¿`](../glyphs/inverted-question.qmd) does. `g sample shape` draws them from a generator made by `•rand`. - `density x` gives the probability density, or the probability mass for a discrete distribution. - `cdf x` gives P(X ≤ x). - `quantile p` inverts the CDF. `cdf⁻¹` is `quantile`. `quantile⁻¹` is `cdf`. diff --git a/nbs/system/explain.qmd b/nbs/system/explain.qmd index 514a5952..2051e724 100644 --- a/nbs/system/explain.qmd +++ b/nbs/system/explain.qmd @@ -1,23 +1,21 @@ # `•explain` — Explain -`•explain text` runs `text` in the caller's scope, as `⍎` does, computes its result, and gives a record of that result and of what happened while it ran and computed it. Code that another thread runs meanwhile records into the same report. Computing a pending result at the end makes no `forced` row: +`•explain text` runs `text` in the caller's scope, as `⍎` does, computes its result, and gives a record of that result and of what happened while it ran and computed it. Where `text` makes the same call on the same arrays as a pending value that a name holds, the two share that value's pending array, and computing the result computes the value too. Code that another thread runs meanwhile records into the same report. A left argument holds options. With `"sites":$t`, the `site` column of each `forced` and `stopped` row names the functions in BPL's own code that computed the items or stopped the trace, innermost first, as in `"search::first_matches ← primitive::search::index_of ← …"`. Without the option, `site` is empty. Computing a pending result at the end makes no `forced` row: - `value` is the result of `text`. -- `fused` has a row for each fused group that ran, as [`•prefs`](prefs.qmd) describes fusion. `glyphs` lists the glyphs of the group's calls in the order they ran. A fold that reads through the group, without building it, adds its glyph last. `items` is the group's item count. `blocks` counts the blocks of items that the group ran in turn. `runs` counts the pieces of work that those blocks split into. A block splits where it crosses the end of a long repeat or cycle in an input, as in adding a row of 5,000 items to every row of a matrix. More runs than blocks means the group ran in smaller pieces. `copied` counts the items copied out of inputs before the calls read them, as a reversed read needs. A `copied` of 0 means the group read every input in place. `compiled` is `$t` where the group ran compiled code: machine code made for the group's calls and types. Calls that have machine operations giving their kernels' results exactly, such as `+`, `-`, `×`, `⌈` and `⌊` on floats, compile together where two or more come one after another, or where they are all of the group's calls. The other calls run on their usual kernels. A group compiles where a group of the same calls on the same types ran before, or where it has about 3 million items or more. The browser build compiles nothing. +- `fused` has a row for each fused group that ran, as [`•prefs`](prefs.qmd) describes fusion. `glyphs` lists the glyphs of the group's calls in the order they ran. A fold that reads through the group, without building it, adds its glyph last. `items` is the group's item count. `blocks` counts the blocks of items that the group ran in turn. `runs` counts the pieces of work that those blocks split into. A block splits where it crosses the end of a long repeat or cycle in an input, as in adding a row of 5,000 items to every row of a matrix. More runs than blocks means the group ran in smaller pieces. `copied` counts the items copied out of inputs before the calls read them, as a reversed read needs. A `copied` of 0 means the group read every input in place. `compiled` is `$t` where the group ran compiled code: machine code made for the group's calls and types. Calls that have machine operations giving their kernels' results exactly, such as `+`, `-`, `×`, `⌈` and `⌊` on floats, compile together where two or more come one after another, or where they are all of the group's calls. The other calls run on their usual kernels. A group compiles where a group of the same calls on the same types ran before, or where one run of its items repays compiling it. The browser build compiles nothing. - `compiles` has a row for each piece of a group's code that compiled. `glyphs` lists the group's calls, and `microseconds` is the compile's time, about 250 for a group of a few calls. -- `stored` has a row for each pending array that a fused group read as an input after computing it on its own. `glyphs` gives the glyph of the call that computed it, which a copy, such as a reversal, doesn't have. `items` is its item count. `reason` says why the group didn't compute it as one of its calls: - - `"held outside the group"`: it's a copy that a name or another array also holds. A group computes any other pending array that a name or another array holds as one of its outputs, storing the array's items in the same pass, and gives it no row. A dfn's local, `⍵` or `⍺` stops holding its array at the dfn's last read of it. It holds the array until the dfn returns where a nested dfn or an error guard's handler mentions the name, or where the dfn reads names by text, as `⍎` does. +- `stored` has a row for each pending array that a fused group read as an input after computing it on its own. `glyphs` gives the glyph of the call that computed it, which a copy, such as a reversal, doesn't have. `items` is its item count. A group computes a pending array that a name or another array holds as one of its outputs, storing its items in the same pass, and gives it no row. A pending copy that a name holds stays pending: the group reads the copy's source through it. A dfn's local, `⍵` or `⍺` stops holding its array at the dfn's last read of it. It holds the array until the dfn returns where a nested dfn or an error guard's handler mentions the name, or where the dfn reads names by text, as `⍎` does. `reason` says why the group didn't compute the array as one of its calls: - `"kept by a fold"`: a fold left its calls in it, to compute later. - `"a kind that the group doesn't hold"`: its items are of a type that a group doesn't hold, such as complex numbers. -- `forced` has a row for each call or assignment that computed items of pending arrays. Computing a pending array in full ends its fusion. `function` is the call's function, or `←` for an assignment that computed one by writing into it, displaying it with `⎕←` or destructuring it. `line` and `column` give its position in `text`, and `items` counts the items computed. A small selection from a pending copy reads its source without storing the copy. The first small read of a pending elementwise result calculates only the selected items, even if a name holds it. The next read that needs source items computes and caches the whole result. Empty or all-fill selections do not count as reads. Small reads of pending copies remain selective on repeated reads. -- `fast` has a row for each [fast combination](../terms.qmd#term-fast-combination) taken, for each fold of [windows](../glyphs/up-down-arrow.qmd) that slides over their source without building them, and for a contraction that executed as a matrix product. `combination` names it, and `function` is the function that took it. For a combination that the form rewriter puts in place, `function` is one glyph for each kind: `"↑⍋"` for every selection, and `"f/⊢⌸"` for every group fold. The names are `"partial grade"`, `"partial sort"`, `"ranks"`, `"group counts"`, `"group folds"`, `"determinant"`, `"permanent"`, `"masked select"`, `"sliding fold"` and `"contraction"`. +- `forced` has a row for each call or assignment that computed items of pending arrays. Computing a pending array in full ends its fusion. `function` is the call's function, or `←` for an assignment that computed one by writing into it, displaying it with `⎕←` or destructuring it into items. Destructuring a pending array of rank 2 or more takes each major cell as a view of it, which computes nothing until a call reads its items. `line` and `column` give its position in `text`, and `items` counts the items computed. `computed` lists the steps that computed them, each with its items, as in `"× 100000"` or `"copy 30752"`. A step is named by its function's glyph, or as `convert`, `copy`, `gather`, `picks`, `join` or `draw`. With the `"sites"` option, `site` gives the functions that computed the first of them. `reason` is `read as items` where the call computed with them, or `read as values` for an argument whose items the function reads as values, such as `#`'s counts or `↑`'s lengths, which the call computes before it runs. It is `an effect` for the pending arrays alive when an effect runs, such as output or a file write, which computes them first, in order. The row names the call that made the effect, or `•explain` where no call did, as in `⎕←1`. It is `a trace's input` for a pending argument that a trace computes to make its input, and `a trace's end` for each pending array that a trace's recording keeps as a constant, which it computes when the trace ends. A call that computed items for several reasons has a row for each. Rows read as values or at an effect mark no slow path. A small selection from a pending copy reads its source without storing the copy. The first small read of a pending elementwise result calculates only the selected items, even if a name holds it. The next read that needs source items computes and caches the whole result. Empty or all-fill selections do not count as reads. Small reads of pending copies remain selective on repeated reads. +- `fast` has a row for each [fast combination](../terms.qmd#term-fast-combination) taken, for each fold of [windows](../glyphs/up-down-arrow.qmd) that slides over their source without building them, and for a contraction that executed as a matrix product. `combination` names it, and `function` is the function that took it. For a combination that the rule table puts in place, `function` is its rule's glyphs: `"↑⍋"` for every selection, `"⍟1+"` for log1p, `"¯1+*"` for expm1, and the marked function for a mark rule, such as `"÷ₓ"`. The names are `"partial grade"`, `"partial sort"`, `"ranks"`, `"determinant"`, `"permanent"`, `"masked select"`, `"sliding fold"`, `"contraction"`, `"accuracy rule"`, which names log1p and expm1, the rule table's accurate forms of `⍟1+x` and `¯1+*x`, and `"mark rule"`, which names `÷ₓ`, `⌊ₓ` and `⌈ₓ` computed by one exact kernel in place of the call and the floor. - `accumulations` has a row for each sum that added items at positions an index gave them, as the gradient of a selection that repeats positions does under [`∂`](../glyphs/partial.qmd), and for each modified assignment to a selection, as `z.[p]+←1` makes. Positions in one ascending run add a block and make no row. `items` is how many items it added, and `size` is its result's item count. The gradients of reversal, rotation, take, drop, transpose, windows that move by 1, and `⌈` and `⌊` folds read the cotangent instead, and make no row. `path` names how the items were added: - `"in place"` adds into the array, with nothing to undo. - `"logged"` keeps each target's old item, to restore the array if a target fails. Exact integers take this path, because a sum can overflow. - `"on a copy"` converts the array first, as for 16-bit floats, or for Booleans that get numbers. Exact integers that get floats stay exact beside them, so they take `"rounds"` or `"one call"`. - `"rounds"` makes one call for each repeat of a position. - `"one call"` makes one call for targets at distinct positions. - - `"item by item"` calls the function once for each target, as a dfn does. - `fallbacks` has a row for each [map](../terms.qmd#term-map) that called its operand once for each cell after its [batched call](../evaluation.qmd#functions-and-effects) fell back, or without a batched call because an earlier one stopped. `items` is the number of cells, and `reason` says why: - `"an effect"`: the operand writes output, reads input, formats a cell's value with `⍕`, or runs a system function that acts, such as `•nget` or `•time`. A batched call would do each of these once for all the cells. - `"a write"`: the operand writes to a name outside the map, or a side of a guard on a cell's value writes to a name outside its dfn, or the operand appends to a value that differs from cell to cell, or writes into one so that it would be held in mixed storage. @@ -34,25 +32,22 @@ - `"item pairs"`: a pervasive function called itself on each pair of items of mixed storage or of a nested array. - `"batched items"`: code with no rule for batches read a batch's items one at a time, even where the batched call then succeeded. - `"∂ items"`: code with no rule for [`∂`](../glyphs/partial.qmd) read the items of a value that `∂` tracks one at a time. - - `"traced items"`: code building a program read traced items one at a time rather than as a whole array. - - `"accumulation"`: a sum added items one at a time, as the `accumulations` path `"item by item"` does. - - `"item lanes"`: a fold or scan by a function with no kernel for its arguments, such as a primitive on mixed storage or a dfn on a lane that no batched map took, called the function on each pair of items in turn. + - `"item lanes"`: a fold or scan by a primitive with no kernel for its arguments, such as one on mixed storage, called the primitive on each pair of items in turn. A fold or scan by a dfn runs as a loop instead, which `loops` reports. - `"number parts"`: monadic [`∨`](../glyphs/vee.qmd) or [`∧`](../glyphs/wedge.qmd) took the parts of integers or exact numbers one at a time, where floats and complex numbers take one pass. - - `"nested labels"` and `"label ids"`: a selection from a nested array labelled or placed its items one at a time. - - `"key groups"`: [Key](../glyphs/quad-equal.qmd) called its operand once for each group, where no fast combination covered it. - `"batch cells"`: a batched map ran a system function, or a deal by [`¿`](../glyphs/inverted-question.qmd), once for each cell in turn. - `"inverse scan"`: the inverse of a scan by a function with no kernel called the function's inverse on each pair of neighbouring items in turn. -- `routes` groups successful fast paths by `route`, with their call and item counts. It includes tracing, fusion, batching, vector and mapped kernels, sliding and mapped folds, a mapped fold that reads its items where they lie (`"in place"`), a matrix product that reads a view larger than its result through its map without building it (`"mapped product"`), and fast combinations. Routes with no item count report zero. -- `stopped` groups interpreter fallbacks from `∂` by `reason`. `calls` includes calls that reuse an earlier stopped trace. `items` totals the right-argument items. Reasons include `"an effect"`, `"a free value that changes on every call"` and `"a function called on each item"`. A changing free value is an outside name or bound array used as positions, a count or a guard whose value repeatedly changes. Per-item calls include a fold by a dfn or Each through a guard on a traced array. +- `routes` groups successful fast paths by `route`, with their call and item counts. It includes tracing, fusion, batching, vector and mapped kernels, sliding and mapped folds, a mapped fold that reads its items where they lie (`"in place"`), a matrix product that reads a view larger than its result through its map without building it (`"mapped product"`), a fold or scan of each of [Key](../glyphs/quad-equal.qmd)'s groups that reads a vector of values by each value's class (`"key classes"`), and fast combinations. Routes with no item count report zero. +- `stopped` has a row for each reason that a trace stopped for. [What stops a trace](../autodiff.qmd#what-stops-a-trace) lists the reasons. `calls` includes calls that reuse an earlier stopped trace. `items` totals the right-argument items. With the `"sites"` option, `site` gives the functions where this run's first stop for the reason happened. A stop kept from an earlier call gives the site recorded where it first stopped, if that `•explain` had the option. - `declined` groups optional optimizations that code considered and did not take by `route` and `reason`. `items` totals the items concerned. Another fast path can handle a declined optimization. The routes are: - `"fusion"`: a call that could have joined a fused group ran on its own, because of `"an exact argument"` or `"an argument of another kind"`. A call whose result's type depends on its items, such as one on ordinary integers under dynamic kinds, never joins a group, and makes no row. - `"fused fold"`: a fold computed its argument instead of reading through its group, because the argument is `"a copy that no call moves under"`, or because of `"a fold that its group can't run"`. A fold of a pending array that a name also holds stores the array's items in the same pass. - A fast combination's name, as in `"partial grade"`, with `"arguments that don't suit it"`: the general path ran instead. - - `"batching"`: a map didn't try a batched call, because `"the batch pref is off"`, because of `"a label level, or a ∂ call inside a batched run"`, or because of `"an argument too large to lay over the frame"` or `"an argument that isn't compact"`. + - `"batching"`: a map didn't try a batched call, because `"the batch pref is off"`, because of `"a ∂ call inside a batched run"`, or because of `"an argument too large to lay over the frame"` or `"an argument that isn't compact"`. - `"program"`: a call could not attempt a program. `"a call under another level"` means `∂` ran inside another level, such as a batched map. `"an argument that programs can't hold"` means a program cannot take one of its arguments. - `"in place"`: a kernel copied an argument's items into a buffer instead of reading them where they are, because of `"repeats of each item"`, `"a cycle that wraps"`, `"steps other than a forward run"` or `"a repeated item"`. - `"contraction"`: a fold of elementwise products could not run as a matrix product. - `programs` has a row for each [`∂`](../glyphs/partial.qmd) that ran from a program, which holds the whole call, its value and its derivative together. `event` is `"built"` where the call's trace built the program first, and `"replayed"` where the call found one. `steps` counts the program's steps. `blocks` counts the block stages that the call ran: each run of elementwise steps on values of one item count, with a fold that reads them, runs as one block stage, a block of items at a time. `compiled` counts the stages that held compiled code after the call. A program's first reuse compiles each stage whose calls can compile, as `fused`'s `compiled` describes. Block stages compute 32-bit and 64-bit floats, Booleans, and integers that wrap. A program's sums can differ in their last bits from the steps run one at a time, and from one call to the next. `listing` shows the program, one line for each value. It shows a size mark in the dfn as a `convert` step. A call whose trace stopped has a `stopped` row instead. +- `loops` has a row for each loop by a function: `⍣`, and a fold or scan by a dfn. `function` is the text of the loop's function, and `iterations` counts its steps. `way` is `"replay"` where the steps after the first replayed the body's recording, which the first step's trace built, and `"ordinary"` where they ran the body as ordinary code, for the reason in `why`. A body that only computes item by item gives `"an elementwise body"`, and its steps fuse with each other instead. `repicks` counts the times a replay stopped applying, as when the loop's state took lengths that the recording doesn't hold, so that the loop traced its body again. A loop inside a body that a trace records runs as one step of that body's program, and each run of that step adds a row. A loop that the program can't hold as one step adds its steps to the program one at a time, for the reason that its row's `why` gives, such as `"a kind that depends on values, in a loop's body"`, `"an effect"` or `"a history, whose length the data decides"`. Each event record is a table of columns, as `•csv` returns. `(•explain t).routes.route` lists successful routes. `.fast.combination` lists fast combinations. `•prefs ["fusion":$t]` writes each forcing call and assignment as it happens. Event recording is disabled outside a capture. @@ -64,7 +59,7 @@ e.forced.function ⍝ ["⍕"] e.fused.glyphs ⍝ ["+×-"] (•explain "2×⌽y+1").fused.copied ⍝ [1000000]ₓ t←y-0.25 ⋄ (•explain "t×1-t").fused.glyphs ⍝ ["--×"] -q←1000005↑1⌽y ⋄ (•explain "+/q+1").stored ⍝ ["glyphs":[""] "items":[1000000]ₓ "reason":["held outside the group"]] +q←1000005↑1⌽y ⋄ e←•explain "+/q+1" ⋄ [≢e.stored.items ; (•storage q).pending] ⍝ [0ₓ $t] (•explain "5↑2×y+1").forced.items ⍝ [5]ₓ s←2×y+1 ⋄ (•explain "5↑s").forced.items ⍝ [5]ₓ (•explain "5↑s").forced.items ⍝ [1000000]ₓ diff --git a/nbs/system/memory.qmd b/nbs/system/memory.qmd index d0ab220d..eecb3e56 100644 --- a/nbs/system/memory.qmd +++ b/nbs/system/memory.qmd @@ -2,7 +2,7 @@ `•memory x` gives a record of what this thread holds, and of what `x` keeps alive. `•memory 0` is the usual spelling for the counts alone. -- `records` counts the live record nodes: the linear calls that reverse mode records while `∂` runs. Each `∂` frees its record when it returns, so the count is 0 between programs. +- `records` counts the live linear nodes: the tangent calls that reverse mode builds while `∂` runs. Each `∂` frees them when it returns, so the count is 0 between programs. - `pending` counts the live pending arrays, whose items wait for their first read. - `bytes` counts the bytes that `x` keeps alive: its items, the base of a view, the arrays that a pending array reads, the parts of a value that `∂` is differentiating, and the nodes of a record. An allocation that `x` reaches twice counts once. diff --git a/nbs/system/prefs.qmd b/nbs/system/prefs.qmd index c9e5c68e..68ec58d4 100644 --- a/nbs/system/prefs.qmd +++ b/nbs/system/prefs.qmd @@ -45,7 +45,7 @@ With `batch` on, a map calls its operand once on all its cells together where it `seed` restarts the session's random numbers from the seed given, so the same seed gives the same draws of [`¿`](../glyphs/inverted-question.qmd) and of a distribution's `sample`. The settings that `•prefs` returns leave `seed` out, because the stream moves on with each draw. Restoring saved settings therefore never reseeds it. -`fusion` shows where large computations stop waiting. Calls of these glyphs on arrays of 65,536 numbers or more run together, block by block, reading each array once: monadic `+ - ÷ | * π ⊼ ⊽ ≤ ≥`, and dyadic `+ - × ÷ | ⌈ ⌊ π` and the comparisons. Under dynamic kinds, `√` and `⍟` join them where earlier calls keep their argument from being negative or ¯0, as in `√|y` and `⍟1+*y`, and so do dyadic `*` on such a base, as in `0.5*y`, and dyadic `⍟` on two such arguments. So do calls on Booleans, such as `∧` and `~`, and calls on integers where integers wrap: under static kinds, and at a fixed width under either kinds. A width or fixed mark, or `•float`, on a waiting result runs with its calls, and stores only the converted items. A comparison runs with a later call that reads it, as in `y×x>0`, unless a name also holds it. Structural functions such as `⌽`, `⍉`, `↑`, `↓` and `↕` wait too on arrays of that size and of any kind, holding only where each item of their result comes from in their argument. Each call waits until something reads its result. A call that would make a chain of waiting calls hold more than 64 calls, or keep alive more than 8 times its largest array, first computes the waiting results it reads. Fused calls, reductions, other pervasive calls and `+.×` read a waiting structural result from its argument, without building it. A read of a few items computes only those items. Any other reader computes the whole result first, such as `⍋`, display, and Python conversions other than NumPy views of structural results. With `fusion` set to `$t`, the innermost call that computes items of a waiting result writes a line naming its function and position, such as `fusion ends at ⍋ (:1:46)`. A reduction such as `+/` of a waiting result runs with the calls and writes nothing. [Performance](../performance.qmd) explains how waiting calls, compiling and static kinds make code fast. +`fusion` shows where large computations stop waiting. Calls of these glyphs on arrays large enough to wait, about 210 KB of result on Apple silicon ([Performance](../performance.qmd)), run together, block by block, reading each array once: monadic `+ - ÷ | * π ⊼ ⊽ ≤ ≥`, and dyadic `+ - × ÷ | ⌈ ⌊ π` and the comparisons. Under dynamic kinds, `√` and `⍟` join them where earlier calls keep their argument from being negative or ¯0, as in `√|y` and `⍟1+*y`, and so do dyadic `*` on such a base, as in `0.5*y`, and dyadic `⍟` on two such arguments. So do calls on Booleans, such as `∧` and `~`, and calls on integers where integers wrap: under static kinds, and at a fixed width under either kinds. A width or fixed mark, or `•float`, on a waiting result runs with its calls, and stores only the converted items. A comparison runs with a later call that reads it, as in `y×x>0`, unless a name also holds it. Structural functions such as `⌽`, `⍉`, `↑`, `↓` and `↕` wait too on arrays of that size and of any kind, holding only where each item of their result comes from in their argument. Each call waits until something reads its result. A call that would make a chain of waiting calls hold more than 64 calls, or keep alive more than 8 times its largest array, first computes the waiting results it reads. Fused calls, reductions, other pervasive calls and `+.×` read a waiting structural result from its argument, without building it. A read of a few items computes only those items. Any other reader computes the whole result first, such as `⍋`, display, and Python conversions other than NumPy views of structural results. With `fusion` set to `$t`, the innermost call that computes items of a waiting result writes a line naming its function and position, such as `fusion ends at ⍋ (:1:46)`. A reduction such as `+/` of a waiting result runs with the calls and writes nothing. [Performance](../performance.qmd) explains how waiting calls, compiling and static kinds make code fast. [`•explain`](explain.qmd) records the same events. In the first example, the comparison runs with the arithmetic and `+/`. In the second, monadic `⌊` ends the fusion of `y×2`. In the third, `|` keeps the argument of `√` from being negative, so `√` runs with the other calls: diff --git a/nbs/system/r.qmd b/nbs/system/r.qmd index ea741931..9c17560f 100644 --- a/nbs/system/r.qmd +++ b/nbs/system/r.qmd @@ -1,13 +1,11 @@ # `•r` — Regular expressions -`•r pattern` compiles a regular expression. It returns a keyed vector of two functions that share the pattern. `p.matches text` gives a table of the matches, as a record of columns: `text`, the matched text; `position`, its position in characters from 0; and `groups`, the text of each group, which is empty for a group that took no part. `template p.replace text` replaces each match with `template`, expanded with the match's groups. +`text •r pattern` gives a table of the matches of the regular expression `pattern` in `text`, as a record of columns: `text`, the matched text; `position`, its position in characters from 0; and `groups`, the text of each group, which is empty for a group that took no part. [`•replace`](replace.qmd) replaces matches. BPL keeps the 16 patterns it compiled last, so a loop that uses one pattern compiles it once. -Natively the pattern uses [fancy-regex's syntax](https://docs.rs/fancy-regex/latest/fancy_regex/#syntax), including lookaround and backreferences. Flags go in the pattern, as in `(?i)`, and templates use `$1`, `${name}` and `$$`. A search that exceeds the engine's backtracking limit raises `DOMAIN`. In the browser the pattern is a JavaScript regular expression, with [JavaScript's syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions), and templates use `$1`, `$` and `$$`. [Regular expressions](../regex.ipynb) covers each function. +Natively the pattern uses [fancy-regex's syntax](https://docs.rs/fancy-regex/latest/fancy_regex/#syntax), including lookaround and backreferences. Flags go in the pattern, as in `(?i)`. A search that exceeds the engine's backtracking limit raises `DOMAIN`. In the browser the pattern is a JavaScript regular expression, with [JavaScript's syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions). [Regular expressions](../regex.ipynb) covers matching and replacing. ```bpl -p←•r "([a-z]+)([0-9]+)" -p.matches "ab12 cd3" ⍝ ["text":["ab12" "cd3"] "position":[0 5]ₓ "groups":("ab" "12"⋄"cd" "3")] -"$2:$1" p.replace "ab12 cd3" ⍝ "12:ab 3:cd" +"ab12 cd3" •r "([a-z]+)([0-9]+)" ⍝ ["text":["ab12" "cd3"] "position":[0 5]ₓ "groups":("ab" "12"⋄"cd" "3")] ``` Errors: `DOMAIN` for invalid patterns and non-text arguments; `SYNTAX` for the wrong valence; `LIMIT` for oversized results. diff --git a/nbs/system/rand.qmd b/nbs/system/rand.qmd index 3c42d52a..a00e15a1 100644 --- a/nbs/system/rand.qmd +++ b/nbs/system/rand.qmd @@ -1,18 +1,18 @@ # `•rand` — Generator -`•rand seed` returns a generator: a keyed vector of two functions that draw from one stream of random numbers. The seed is a nonnegative whole number. The same seed gives the same draws. +`•rand seed` returns a generator: a keyed vector of two functions that draw random numbers. The seed is a nonnegative whole number. The same seed gives the same draws. - `roll Y` works like `¿Y`. - `X deal Y` works like `X¿Y`. -A distribution's `sample` takes a generator on its left, as in `g d.sample 3`. Copies of a generator share its stream. Drawing from one copy moves every copy on. +A distribution's `sample` takes a generator on its left, as in `g d.sample 3`. A generator counts its draw calls, and each call draws from a stream of its own, picked by the seed and the call's count. So the n-th call from a generator draws the same numbers however many numbers the calls before it drew. Copies of a generator share its count: drawing from one copy moves every copy on. -In a [map](../terms.qmd#term-map), the cells take the generator's numbers in turn. A [batched call](../evaluation.qmd#functions-and-effects) takes every cell's numbers for one draw before its next draw. A map whose cells each draw once gives the same numbers either way. A guard that splits the cells is the exception: each side of the guard draws for its own cells in turn. Here the two cells take the stream's next four numbers: +In a [map](../terms.qmd#term-map), the cells draw from streams that the map's count and each cell's place pick, and the map takes one count from the generator, whatever its cells draw. A [batched call](../evaluation.qmd#functions-and-effects) and its cells run one at a time draw the same numbers, also under a guard that splits the cells. Here the map takes one count, so the calls after it draw what the calls after one draw do: ```bpl -g←•rand 3 ⋄ a←{g.roll ⍵⍴6}¨1 3 ⋄ h←•rand 3 ⋄ ((0⊃a),1⊃a)≡h.roll 4⍴6 ⍝ $t +g←•rand 3 ⋄ a←{g.roll ⍵⍴6}¨1 3 ⋄ h←•rand 3 ⋄ b←h.roll 6 ⋄ (g.roll 2⍴6)≡h.roll 2⍴6 ⍝ $t ``` -`¿` draws from the session's own stream, which [`•prefs ["seed":n]`](prefs.qmd) seeds. +`¿` draws from the session's own generator, which [`•prefs ["seed":n]`](prefs.qmd) seeds. Errors: `DOMAIN` for a seed that is not a nonnegative whole number, or a left argument to `sample` that is not a generator; `LENGTH` or `RANK` for more than one seed; `SYNTAX` for a dyadic call to `•rand`. diff --git a/nbs/system/replace.qmd b/nbs/system/replace.qmd new file mode 100644 index 00000000..e81f8c94 --- /dev/null +++ b/nbs/system/replace.qmd @@ -0,0 +1,10 @@ +# `•replace` — Replace regular expression matches + +`text •replace [pattern template]` replaces each match of the regular expression `pattern` in `text` with `template`. Natively the template uses `$1` for a group, `${name}` for a named group, `$0` for the whole match and `$$` for `$`. In the browser it uses JavaScript's `$1`, `$` and `$$`. Patterns follow [`•r`](r.qmd), and so does the keeping of compiled patterns. + +```bpl +"ab12 cd3" •replace ["([a-z]+)([0-9]+)" "$2:$1"] ⍝ "12:ab 3:cd" +(•replace↢["[0-9]+" "#"])¨"a1" "b22" ⍝ "a#" "b#" +``` + +Errors: `DOMAIN` for invalid patterns, non-text arguments, and a right argument that isn't a pattern and a template; `SYNTAX` for the wrong valence; `LIMIT` for oversized results. diff --git a/nbs/system/storage.qmd b/nbs/system/storage.qmd index d9cdaa4f..f1a4ef8c 100644 --- a/nbs/system/storage.qmd +++ b/nbs/system/storage.qmd @@ -2,13 +2,15 @@ `•storage Y` gives a record of how the items of `Y` are held. It reads no items, and a pending call stays pending. -- `kind` names the storage: `"bool"`, `"integer"`, `"float64"`, `"float32"`, `"float16"`, `"complex128"`, `"complex64"`, `"character"` or `"mixed"`. An atom gives its own kind. That can also be `"rational"`, for an exact rational or an integer beyond 64 bits, or `"function"`. Compact storage holds items of one kind. `"integer"` covers integers of every width. Mixed storage keeps each item's kind. Boxed display marks the same storage on its bottom edge. [Storage](../numbers.qmd#storage) describes each storage. +- `kind` names the storage: `"bool"`, `"integer"`, `"float64"`, `"float32"`, `"float16"`, `"complex128"`, `"complex64"`, `"character"`, `"mixed"` or `"segmented"`. An atom gives its own kind. That can also be `"rational"`, for an exact rational or an integer beyond 64 bits, or `"function"`. Compact storage holds items of one kind. `"integer"` covers integers of every width. Mixed storage keeps each item's kind. Segmented storage holds arrays of one rank and kind as parts of one array, its leaf, as Rank's `⊂` rows and partitions give them. Boxed display marks the same storage on its bottom edge. [Storage](../numbers.qmd#storage) describes each storage. +- `leaf` names the storage of a segmented array's leaf, and is `""` for every other storage. +- `groups` names how a segmented array's items divide its leaf. It is `"ranges"` where each item is a range of the leaf, and `"classes"` where the items are Key's groups of a vector of values, held in the values' own order with each value's class. It is `""` for every other storage. - `pending` is `$t` while fusion hasn't yet run the call that computes the items. - `view` is `$t` when `Y` reads another array's items, as a reshape does. - `active` counts the levels of `∂` whose tags `Y` carries. Under `∂`, `kind` names the storage of the value being differentiated. ```bpl -•storage 1.5 2.5 ⍝ ["kind":"float64" "pending":$f "view":$f "active":0ₓ] +•storage 1.5 2.5 ⍝ ["kind":"float64" "leaf":"" "groups":"" "pending":$f "view":$f "active":0ₓ] (•storage 2 3⍴⍳6ₓ).view ⍝ $t y←1.5+¿200000⍴0 (•storage y).pending ⍝ $t diff --git a/nbs/target.qmd b/nbs/target.qmd index e12c1f99..11b4180f 100644 --- a/nbs/target.qmd +++ b/nbs/target.qmd @@ -8,15 +8,19 @@ Measure for every step, diff and review in the one-graph release. A diff moves t - Fast code: static kinds; branches and updates by `⍚`, `f@g`, `f@[p]`, `x.[p]←`, never dfn guards on traced values. Code written without that care: adequate speed. Dynamic kinds: correct, at most 4× the same code under static kinds at the same size, except where Jeremy agrees (a larger gap where a reasonable rewrite is fast). A guide, "Writing fast BPL", ships with the release: static kinds with explicit sizes on literals and data, `⍚`, `f@g`, `f@[p]`, selective assignment into a preallocated cache (`K.[p]←x+.×Wk`), index notation, shapes kept the same across calls and iterations, `fₐ` where values may leave the real domain; and what ordinary code costs. - Results agree only within the documented accuracy (4 ulps), across spellings, routes, sizes and threads. - "The bar" (below) sets the criteria; it changes only with Jeremy's OK. Done: "Acceptance". +- `fib` or similar recursive functions do not need to be fast at all +- √ et al do not need to be fast, but √ₐ do +- ÷ on integers doesn't need to be fast, but ÷ₓ does ## Non-goals (this release) - Speed for dfn guards on traced values. - Speculated widths for ordinary integers (ranges instead; decision 16). +- Speculated kinds (part 2 reads a kind that depends on values at the call instead; `meta/speculation.md` keeps the design and how to add it back). - Identical bits across routes, sizes, threads or spellings; accuracy past the documented bounds. - Reusing a graph across calls from a Python loop or a recursive dfn's calls (decision 5): each call rebuilds. - Recording through `⍎` or an error guard. -- Symbolic lengths that let 1 recording serve every length (decision 18): buckets are a key policy (part 10). +- Symbolic-length expressions in engine types (decision 18): plans refill concrete descriptions and maps from run-time counts (part 10). - Complex and f16 steps in block stages (main's decision 13); complex numbers under `∂` until a gradient convention is chosen. - The GPU backend, array files, an external backend, bf16 (part 23). - Range inference through `•index ∞` (decision 11). @@ -30,95 +34,119 @@ Measure for every step, diff and review in the one-graph release. A diff moves t - A new kind of thing = data in a table, not arms across match sites. A variant that needs arms in many matches → the enum lacks a shared shape: fix the enum. - Rust closures never stand in for BPL functions or bodies. A body is a function value or a recording. - No cache beside the program cache (recordings) and the JIT's code table (compiled code), each its fact's 1 home; no 2nd copy of items. -- A recording, or a forced graph's cached plan, assumes only its key, its captured free values and its speculation boxes (a speculated kind, and each leaf range fact its plan relies on: part 2). -- Data errors: kernels and executors set the session's error slot, which keeps the 1st error and the node that set it; forcing records and never fails; a run that sets the slot replaces no node downstream of the failing one with a result, so those stay pending and a later read runs them again and raises again; a failed kernel writes values inside its node's declared range and reads nothing past its operands, so no later step reads out of bounds; clearing a masked case's error reruns the whole read (parts 13, 14), so an error the masked case hid is still recorded; the executor raises at its next checkpoint: a host read deciding structure (a count, a shape, a guard), an effect, an error guard's exit (leaving an error guard forces the work built inside it, so the guard catches its errors), display, the statement's end. When and how an error surfaces isn't promised. +- A recording, or a forced graph's cached plan, assumes only its key and range boxes (each leaf range fact its plan relies on: part 2). All captured free data are call-time inputs, never separately reread or compared facts (part 10). +- Errors: construction, shape, type, name and host-I/O errors remain checked in both modes. Graph-numeric data errors are checked under dynamic kinds; static graph-numeric execution does not record them. Host-kind arithmetic remains checked outside the graph. Invalid static numeric indexed reads use the fill, including from empty arrays; invalid static writes skip those positions. Other invalid static numeric results are unspecified. Every kernel stays within its operands' bounds and its declared result range, with fallback values included by the existing range rules. Valid results, exact results and documented finite accuracy are unchanged. The mode comes from each call's origin, never the session at force time. Rustymath owns the error slot held by basedpl's session. Checked kernels and executors on any thread record the first error and increment its count; each runner leaves results downstream of an error pending. Forcing records without raising. The executor raises at checkpoints: a host read deciding structure (a count, shape or guard), an effect, an error guard's exit, display and the statement's end. Leaving an error guard forces the work built inside it. When and how an error surfaces isn't promised. - 1 mode (tinygrad): every call builds a node and waits; a read forces what it needs and runs its graph's plan from the 1 cache, scheduling only on a miss. Each stage has a budget ("Cost model"); a stage over budget → a cheaper stage, never a 2nd route. - basedpl and rustymath never hold 2 machineries for 1 task. -Yes, your outline matches what I'd do. I'd add two steps before the subsystem forks start, plus explicit guards against call-site spread. - ## The build, in order -1. **Interfaces as code first.** One fork writes the target's interfaces as real Rust declarations in their home files, with bodies left as `todo!()`: `Desc`, each `Step` variant's fields, `CallPlan`, `Program`, `Plan` with `refill`, `Node`, `Level`, `FunctionNode` and `OperatorKind::info`, `Rule`, `Programs::serve` with `Key`, and `Session::error`/`raise`. I review it against the target. Prose interfaces leave each fork to invent details, and those inventions collide at compile time as glue at call sites. -2. **A short design note per subsystem before its code.** Each subsystem fork first writes its data structures, its algorithms, and an estimate per operation against its cost-model budget, and I review that. For example, nodes interned by structure already hash themselves when built, so a read's cache key could come from the root's hash rather than a fresh pass over every node. -3. **Subsystem forks write the end state.** Up to five at a time, each owning whole files and writing its subsystem as the target describes, against the declared interfaces. They delete the old code outright, using their part's "Not" list as the checklist. Nothing is compiled. -4. **Compile integration, in dependency order.** A mismatch at a boundary means one side departs from the declared interface, and that side gets fixed. No adapter goes in between. -5. **Correctness, then speed.** First the reference files and "Failing cases". Then each stage's instruction counts against its budget, where a stage over budget gets a cheaper implementation, never a second route. + +This process is a RULE. Exceptions require Jeremy's explicit words "I accept that change to the build rule". + +Steps 1–3 are an iterative work cycle, not global barriers. Write independent pieces in parallel where useful. Keep tightly connected decisions with one owner. The file map records current work allocation, not permanent architectural boundaries. + +1. **Declare the interfaces needed next.** Write shared interfaces as real Rust declarations in their target homes before their consumers need them. Do not wait for every future interface to be settled. The implementation owner chooses local API details and notifies consumers and main of meaningful contract changes. Main settles changes to semantics, architecture, ownership or substantive costs, not routine accessor names. +2. **Plan the next coherent chunk.** Start from the file map and short design notes covering structures, algorithms, cost estimates and retained code. Reuse the existing notes when ownership changes. Settle only the open details that block the next chunk; `meta/step1-interfaces.rs` records the remaining ones. +3. **Write the end state.** Up to five forks work at once, each with exclusive ownership of whole files. A ready chunk proceeds without waiting for other notes or future interfaces. Delete the old machinery using its parts' "Not" lists. If a dependency blocks it, report the specific need and advance another independent piece; do not invent an adapter or a temporary route. +4. **Compile integration, in dependency order.** Main starts this step when the end-state pieces are written. Fix a boundary mismatch in the side that departs from the declared contract, not with an adapter. No compiles, checks, tests or timings run in steps 1–3. +5. **Correctness, then speed.** First the reference files and "Failing cases". Then each stage's instruction counts against its budget. A stage over budget gets a cheaper implementation, never a second route. The acceptance gates below still apply in full. + +### Step 2: current ownership and working notes + +- The file map assigns every calculation-source file in both repositories to one current owner and lists its target parts. Parts whose implementation homes share a file stay together. Unchanged editor/UI assets and tooling remain outside the rewrite. +- Main can merge, split or transfer assignments as coupling and progress change. Stop the old owner's edits, hand over the current code, decisions and next action, then update the map and active roster before the new owner writes. One file has one owner at a time. A connected change stays with one fork or its successor. +- Notes support implementation, not another approval ceremony. Existing notes and keep lists remain usable across handoffs. Update them for a changed mechanism or meaningful decision; do not write a new design document for a reassignment or routine API choice. + +### Step 3: ready chunks and milestone review + +The owning fork pins the declarations needed by its next chunk, writes that chunk's end state and reports a coherent milestone. Main reviews the approach and result against the target: one home per fact, deleted old routes, interface fan-out, net source lines, costs and unresolved departures. Mechanical inspection goes to a fork when it resolves meaningful uncertainty. A second broad review of every note, helper or API proposal is not required. + +Main keeps a whole-system view, finds independent work and changes ownership when a boundary causes repeated coordination. It does not keep five forks busy for its own sake or act as an approval gate for local details. ### Keeping call sites from spreading again -- **No threading of cross-cutting concerns.** The target already keeps lengths, errors, levels and settings each in one home, out of engine call sites. Engine code sees only concrete lengths, errors go to the session slot rather than through `Result`s everywhere, and steps carry the settings they read. -- **Changes outside a fork's files only through the declared interfaces.** A fork that needs a new parameter on a widely called function stops and reports. That's an interface change, so it comes to me, and to you if it changes the design. -- **A count in every diff review:** the functions outside the fork's own files that the diff touches. Any number above zero needs a reason. Bounded extents' roughly 90 call sites would have shown up in the first such review. -- **The isolation test applies to everything:** complexity is acceptable only where it's fully isolated behind its home. +- Lengths, errors, levels and settings each have one home. Engine code sees concrete lengths, checked errors use the session slot rather than `Result`s threaded everywhere, and origins keep the settings they read. +- Other owners consume declared interfaces. Publish meaningful changes to their consumers and main before dependent edits. If a change repeatedly crosses owners or spreads a concern through callers, move the connected work together or fix its home; do not add glue. +- At milestones, report changes outside the previous ownership boundary and the reason. Complexity must remain isolated behind its home. ### Existing code: reference, never parts -- Each subsystem's design note carries a keep list: every existing function the fork keeps or calls, each with the target part that names it as the home. Everything else in the fork's files goes. Main reviews the keep list against the target before any code. -- Code outside a fork's own files is reached only through the declared interfaces. A missing interface is an interface change, which goes to main. -- Deletion is safe by construction: code outside a declared interface has no caller outside its subsystem, so a fork deletes its old machinery outright; integration finds only callers that should use an interface. -- Each diff review checks calls into pre-existing code against the fork's keep list. A call off the list is a departure. +Existing code supplies behaviour to preserve, not the architecture. Keep lists name every existing function retained or called and the target part that homes it. Reuse and update those lists as work moves; there is no separate main-approval round for each entry. Delete obsolete machinery outright, rather than leave it for another fork. Milestone review checks retained calls against their target homes and the "Not" lists. Existing callers, privacy or names do not justify a second route, cache or compatibility layer. ## Flow -call → levels (`transform::call`: newest level's rule) → row plan → 1 builder: every call builds a node and waits, so every pending value has its origin (a read replaces a node with its result, except downstream of a data error); nothing in the graph computes at a call, and the reads a call makes of its own operands force them (part 8). A read forces what it needs: its graph's plan from the 1 cache (keyed by structure, leaves as descriptions; a lone node is a graph of 1); a miss schedules (gather → rewrite → order → stages → compile? → plan); refill where lengths changed → run. Data errors: the session's slot, raised at checkpoints. Outside a recording an effect runs at its call, in order among effects (a token); an effect forces only what it reads. A trace is no mode of dispatch: it records the graph built anyway; its end = the scheduler up to the plan, kept as a recording. Replay = the plan run on new arguments, skipping dispatch. +call → semantic forms (part 5) → levels (`Level::rule`: newest level's rule) → roles and operand reads → row-plan builder → pending node. The origin keeps what replay and refill need (part 4). A read walks live nodes and inputs (`Live::of`), looks up its structure key in the 1 cache (part 10), checks range boxes, and schedules only on a miss: `Program::of` → rewrite → order → stages → plan. A hit refills changed lengths or count parameters. Run preparation makes cheap choices from actual counts, then executes each stage or compiles its selected version when that pays (parts 8–10). Checked errors go to the session's slot and are raised at checkpoints. Outside a recording an effect runs at its call, in order among effects, forcing what it reads. A trace records the graph built anyway; its end uses the same scheduler. Replay runs the plan on new inputs, skipping dispatch. ## Role models - tinygrad: 1 lazy graph of interned nodes (`UOp`); every call builds and waits, only a read computes (`realize`); running a graph separate from reading its result (`realize()`, `numpy()`); a rule matcher indexed by root op with early rejection (`PatternMatcher`); stage ends by its indexing rule (`schedule/indexing.py`); compiled kernels cached by structure; recordings from real runs replayed (TinyJit), effects as ordered nodes where TinyJit runs them only while capturing. -- JAX: 1 entry, `Primitive.bind`, interpreted by the innermost level (its trace stack: `Derivative` as `JVPTrace`, `Batched` as `BatchTrace`); derivative rules applied as calls run, linear nodes and their transpose (`jvp`, `linearize`, `transpose`); batching rules beside each primitive (`vmap`); loops carrying batching, JVP and transpose rules beside the node (`scan`, `while_loop`); a select's input gate (`cond` under `grad`); ordered effects (a token threaded through effects only); explicit random state; a cache keyed by signature (`jit`, on the function object; BPL keys a dfn by its definition, part 10) and 1 compiled program per primitive and signature (eager `apply_primitive`), which a read's cached plan follows; older levels' values entering as opaque inputs (lift). Not needed: JAX's buckets and padding for shapes, which exist because XLA recompiles for each shape; BPL's kernels take counts at run time and compiled code is cached by structure, so a new shape costs at most a plan, never a compile. -- Also: XLA (speed rules after the transforms; a cheap producer recomputed in each consumer), Dex (`for` loops, reverse mode through them), DuckDB (ranges as statistics; segments as list vectors), Dynamo (a retrace limit of 8). +- JAX: 1 entry, `Primitive.bind`, interpreted by the innermost level (its trace stack: `Derivative` as `JVPTrace`, `Batched` as `BatchTrace`); derivative rules applied as calls run, linear nodes and their transpose (`jvp`, `linearize`, `transpose`); batching rules beside each primitive (`vmap`); loops carrying batching, JVP and transpose rules beside the node (`scan`, `while_loop`); a select's input gate (`cond` under `grad`); ordered effects (a token threaded through effects only); explicit random state; a cache keyed by signature (`jit`, on the function object; BPL keys a dfn by its definition, part 10) and 1 compiled program per primitive and signature (eager `apply_primitive`), which a read's cached plan follows; older levels' values entering as opaque inputs (lift). JAX/XLA normally specialize on exact shapes; users pad inputs into buckets to reuse compilations. BPL reuses structural plans by length class, refills concrete descriptions and maps, and passes counts to kernels at run time (part 10). A changed length alone does not require different compiled code. +- Also: Futhark (run-time work estimates choose sequential or parallel versions; BPL compiles a selected useful version on demand); XLA (speed rules after the transforms; a cheap producer recomputed in each consumer), Dex (`for` loops, reverse mode through them), DuckDB (ranges as statistics; segments as list vectors), Dynamo (a retrace limit of 8). - Not taken: tinygrad's gradient after the fact (a read before the loss would cut it); JAX's abstract tracers (BPL traces real runs, decision 6.4) and its eager evaluation (BPL is lazy throughout). ## Parts Each: job; callers; not (what must not exist). -1. Values (`array.rs`, `array/storage.rs`). `Value`: atom | array (`ArrayData`: compact buffer; segmented: 1 leaf buffer + an entry (offset, length) per item; mixed: host kinds only; pending: a node) | pending atom | carried (a level's value, dual or batch: `Value::Carried`, 1 home). A view of a stored array: a layout over its buffer, no node; of a pending value: a `Copy` node. Segmented storage is built where made (the gather finish applies the compact rule below); writes keep it. Every host read of a node's items passes 1 chokepoint (`ArrayData::storage`, `Node::into_storage`): stops traces, refuses linear reads. A partial read of a pending value (an item, a part) is a `Copy` node read through the chokepoint; the scheduler computes only what it reads, as tinygrad pushes a shrink into its kernel. Kinds in the graph: Boolean, fixed and ordinary integers, floats of 16, 32 and 64 bits, complex, characters; on the host: rationals, big integers, arrays mixing characters with numbers or exact with inexact, functions, records as containers. A record's field or a host array's item that is a compact array enters the graph as a leaf when read. A nested array may hold a pending atom as an item, as a pytree holds a tracer; compact storage resolves one. Segmented: an entry is a contiguous (offset, length) run of 1 leaf buffer, no stride (strided cells are copied into the leaf), with each item's shape for items of rank 2 or more; items of 1 shape need no entries (stacked); a leaf is an ordinary array value (stored, a view, pending) and can itself be segmented; entries are an integer operand, which a recording holds as 1 input node; an empty segmented array keeps its prototype item. Made by the compact rule: every item an array of 1 rank and storage kind, no keys or names on its axes; any other nested array stays mixed, and the walk writes through it level by level (part 16). Pervasive calls between segmented arrays: segmented agreement (each item's lengths checked when the call runs; length-1 items expand; LENGTH ERROR names the call). Frequent readers read segments directly (Each, enlist, mix, pick, first, depth, match, search, unique, sorts, display, Python, `•csv`, `•json`); ragged cells compare through 1 comparator in rustymath `search` (a leaf slice with its shape: equality, hash, `array_order`'s order). A leaf buffer: host memory the allocator owns; later a mapped file or GPU memory (part 23). Not: caches holding items twice; packing per producer; reads past the chokepoint; carriers in storage (`Data::Carried`); level code in storage. -2. Descriptions (`program::Desc`; JAX abstract eval). Kind, atom flag, layout, range. Computed only by `Step::describe` from operands' descriptions. Kinds: Boolean; fixed integer of a width; ordinary integer; float of 16, 32 or 64 bits; complex of either width; character; weak or strong. Joining kinds: 1 home, `Kind::join` (`Storage::compact`'s width for ordinary integers joins through it). Under static kinds, results computed apart (writes, compacting selects, map assembly, `⍣` histories) join to 1 compact kind (`Widening::of_results`), as JAX gives 1 dtype to `vmap`, `where` and `scan` outputs; strands and rows keep each item's kind; dynamic kinds keep each kind. Wider accumulators come only from seeds and conversions (`0ₓ₈+/u`, `(aₓ₄)+.×bₓ₄`); no accumulator field. Kinds that depend on values (dynamic kinds): `√ * ⍟ ○` on reals (complex), `÷` of exact integers (rational, a host kind), `⌊ ⌈` and monadic `×` of floats (exact): each speculated at its call by 1 mechanism, inside and outside traces alike (part 10's speculation). Arithmetic on ordinary integers can widen too, and isn't speculated: ordinary integers stay in the graph where ranges prove a width, at that width; otherwise a host call (part 5; decision 16). Keyed and named layouts are graph values: in keyed agreement a key missing on one side reads as the fill (selecting a missing key is an INDEX error: part 20); keyed agreement is a node whose maps and length come from the keys at run time; a cache key compares key sets by identity before contents; a layout's renderer travels in descriptions. Ranges (DuckDB statistics): each numeric node's range comes from its object's interval rule over its operands' ranges; a leaf's when made, or unknown. Ranges replace rustymath's `nonnegative`, `total_on` and `keeps` facts and descriptions' `nonnegative` mark. They keep `√` and `⍟` real with no speculation where arguments are in range, keep ordinary integers in the graph where they can't overflow, drop exact folds' `fits` checks, and drop a gather's bounds check where its index range lies inside its axis. A plan that relies on a leaf's range (a sign, an integer width, an index inside its axis) keeps each such fact as a speculation box, checked on every hit against the leaf's carried range and the run's lengths; a leaf of unknown range fails the box, under speculation's policy (part 10). An ordinary integer's proved storage width is part of its kind, so keys hold it. A loop carry's range widens to unknown at the loop's head unless the body provably keeps it (abstract interpretation). A fold's range over n items uses its bucket's bound for n (part 10). Not: a description from a value's `layout()` in a trace; parallel description helpers; a 2nd shape rule (`Step::extents`); `narrows`. -3. Graph (`graph.rs`; tinygrad `UOp`). `graph::Node`: a step, its operands each read through a rustymath `Mapping`, its `Desc`; or a leaf: an input (a trace argument or a write root: a key position, no step) or a computed buffer. Interned by structure (`keeping`), leaves included. A view = a `Copy` node composed into its readers' maps. Forced → `result`: a read replaces the node with its result and drops its step and operands (tinygrad's `realize`), so later graphs read it as a leaf and no rule matches across it; a run that sets the error slot replaces no node downstream of the failing one (Measures). Live graph → `program::Program` by 1 conversion; the scheduler, rules, recordings and `verify` read only `Program`. A view is stored, at 1 copy, only for a reader whose kernel can't read through a map. A chain of copies composes into 1 map by 1 home (`Mapping::composed`; `Steps::through` composes by the inner array's axes where the maps don't compose affinely); 1 test decides whether a reader reads a copy's source through its map in place (`reads_through`). A shared pending copy whose readers' maps don't compose: each reader reads the source through both maps; nothing stores it. Not: node builders outside dispatch; scheduling the live graph directly. -4. Steps (`program::Step`): the IR. `Map Convert Copy Gather Fold Product Select At Picks Join Call Draw Effect Loop Index` (`Index`: an affine index step). A variant: `describe`, `run::step`, `glyph`, `operands!`. A step carries its parameters and keeps its origin (primitive, parameters), from which a recording refills its descriptions and maps at replay (part 10); a route is a step, never a builder. A new job takes an existing step where one serves. Batching, JVP and transpose rules: rows, or beside `Loop`; never in steps. Index steps: `⍳` and `•index` give affine index steps, generated, never stored; `+ -`, negation and `×` by a constant keep an index affine; selection plans its `Steps` from the coefficients (`Stride`, `Rotated` for wrapping, `Padded` with `•edge`'s modes), so clamped and reflected reads aren't gathers; `Table::affine` stays only for index arrays that are data; a fill is a parameter of a `Padded` map. `Call` runs a planned primitive that no block stage runs (a sort, a grade, a search, `⌹`) on forced operands, in a stage of its own. `Effect` reads the token of the effect before it and its own operands, so effects keep their order among themselves and nothing else waits on them (JAX's ordered effects); the scheduler ends a stage at it. A step that reads a numeric setting (a comparison's tolerance, Mathish mode) carries it as a parameter, so a later `•prefs` change can't change a pending node. `Join` is the gather from several sources: each item read from 1 source at a position; positions from running the moving primitive on ids; its transpose adds each item's cotangent back into its source; it serves strands, `,`, `⍪`, compaction, Under's writes and joins' transposes. -5. Dispatch (`Operation::call`; JAX `Primitive.bind`; tinygrad's lazy `UOp`). 1 entry for every call of an operation on values: primitives (pairing axis, matrices, at), folds and products by primitives, selects, loops, strands and array notation (`Join`), conversions, draws, effects. Order: the newest level among the args takes it (its rule calls dispatch for its parts) → the row's plan, once: operand roles (data | read at the call: a parameter, values deciding a length; each a read, part 8; `Facts::reads`; a kind that depends on values is speculated, part 2, never read at the call), the step (`Map Copy Gather Fold Product At Join Picks Select Convert Draw Effect Loop Index`, else `Call`: the row's own code runs at the node), result length and bytes → 1 builder (`fusion::built`; `Step::describe` gives its `Desc`, computed only here): every call builds a node and waits, so every pending value has its origin; nothing in the graph computes at a call (tinygrad); a read forces what it needs, the reads a call makes of its own operands included (part 8's list). A host kind (rationals, big integers, mixed arrays, functions, records) isn't in the graph: a call on host values runs its row's code on them. A build too costly for small calls is fixed by a cheaper build (the cost model's dispatch budget), never by a 2nd route. Data errors aren't a reason to compute: executors set the session's error slot (Measures). Engine code calls dispatch, never a builder. A read of a pending value's item or part: a `Copy` through the same builder. Check: dispatch and a read against the cost model's budgets ("Cost model"). A fold of a pending array to an atom gives a pending atom; a call reading it reads its node. Not: a 2nd entry (today `call_axis` beside `Operation::call`'s `node`); a call computed without its node (eager and traced as 2 modes, `fusion::builds`); a force policy choosing per call (`fusion::gate`'s now or wait; the cost rule, `fusion::waits`, `wait_limit`); gating or building in own code (`structural::copying`, `pervasive_apply`'s `defer`, `Operation::node`'s arm per kind, `fusion::node` from `operators.rs`, `derivative.rs`, `distribution.rs`); own code beside a step's executor (`pervasive_apply`, `structural.rs` beside `run::step`'s `Map`, `Copy`); rewrites at dispatch (`taken_right`, the selecting form's copy for the walk); rule marks; pending atoms read by special case (`views`, `pending_atom`): roles replace them. -6. Rows (`primitive.rs`) and rustymath objects (`facts.rs`, `recipe.rs`, `structure.rs`, `derivative.rs`). Row: its object, plan (kind rule; shape rule over known lengths), batching rule (`cells`), Under selection, linear map and its transpose, fold identity, cost, row facts (`passes`, `alternates`, `first_axis`). Object: 1 row holding its formula (once), partials, ranges, fold properties; kernels instantiate the formula over their `Ops` (recipe); an instantiation slower than a hand-written kernel is fixed in the formula or its `Ops`, never by a 2nd writing. Defined forms (monadic `≤ ≥ ⊼ ⊽`) need no rules: their code calls other primitives, which build nodes. Not: rules kept by kind across files; an object's facts in parallel tables (`rules!`, `Partials`, recipe bodies); a name check standing in for a fact; a 2nd writing of an object's math (`lowered_op`, kernels' scalar code beside a formula). -7. Rewrite engine (`program/rules.rs`; tinygrad `PatternMatcher`). 1 table: BPL pattern rows and structural rules (fold of a copy, contraction, sliding fold, copy commute). A rule: a root key (data: step kind, root primitive) that rejects; 1 match giving bindings; 1 builder from them. Runs in the scheduler's pipeline on `Program`s after `∂` and batching, a read of a lone node included (part 8). Every pending value has its origin, so a rule matches in every spelling: `⍟1+x` → `log1p` from 1 row, across statements, bindings, dfns and trains; a read replaces a node with its result (part 3), so a rule doesn't match across a read of its intermediate. Speed rules run after `∂` and batching (XLA's simplifier after JAX's transforms), so the hidden specials they introduce (partial sorts, ranks, `log1p`, `expm1`) need only plans and kernels; a pattern also matches its primitives' batched forms, which carry an axis. A rule may change last bits, never leave the documented accuracy bounds (kmeans' cancelling rule is out: decision 14); each rule has a reason that depends on no model, and a reference case. The table: selections of a grade or sort (`k↑⍋⍵`, `↑⍋⍵`, `i⌷⍋⍵`, `i⊃⍋⍵`, for `⍋ ⍒ < >`) as partial sorts; `⍋⍋⍵`, `⍋⍒⍵` as ranks; `⍟1+⍵` as `log1p` and `¯1+*⍵` as `expm1` (accuracy); mark rows, each a conversion node over its call's node, matched on nodes like any row (`fₐ` builds `(f args)ₐ`, part 21): `(√⍵)ₐ (⍟⍵)ₐ (⍺*⍵)ₐ (⍺○⍵)ₐ (⍺○⁻¹⍵)ₐ` as real-form specials, `(⌊⍵)ₓ (⌈⍵)ₓ (⍺÷⍵)ₓ` as exact forms; a `+` fold of `×` whose operands read through affine maps as 1 product (index notation's level 3); a fold of a copy that repeats items as a fold of its source; a dfn fold whose body is 1 associative primitive (`{⍺+⍵}/`) as that primitive's fold; `J+.×v` as a JVP and `u+.×J` as a VJP, where `J` is a Jacobian `∂` built and nothing has computed. Not rules: a combination of 1 call matched by its function's structure is its operator's own algorithm (monadic `-.×`, `+.×`: `primitive_expansion`); Key's counts and folds by group come from batching over segments (part 12). Not: a test written twice (a pre-check beside the match); a rule tried from 2 places; 1 rule in 2 directions written as 2 rules; marks and their machinery (`marks`, `UNMARKED`, `FunctionData.needs`, `call_marked`, `rules::pairs`, `rules::at_once`); mark rows bound to function values by spelling (`FunctionData.rows`, `Function::new`'s row filling, `operators::marked`, `spelled`, `marker`). -8. Scheduler (forcing: `graph.rs`, `program.rs`, `program/run.rs`). 1 pipeline, `schedule(Program) -> Plan` then run: gather, rewrite, order, stage ends (tinygrad `indexing.py`), outputs, cheap chains recomputed, folds sharing stages, select lowering (masked | compacting, from descriptions), compile decision, run. The same pipeline serves forcing, a trace's end and replay planning. Forcing records data errors in the session's error slot and never fails. A read runs its graph's plan from the 1 cache (part 10), keyed by the graph's structure, literals by value (tinygrad's `CONST`) and other leaves as descriptions (a length class and bucket per axis) and each length the data decides inside it by its class and bucket (part 10), with the plan's speculation boxes checked on each hit (part 2), and refilled where lengths changed; only a miss schedules, so a read of a graph seen before costs a key, a probe and the run (cost model). A lone node is a graph of 1 (JAX's 1 compiled program per primitive and signature; tinygrad's kernels cached by structure). Stage ends (tinygrad): a node whose length the data decides ends its stage, its count read before the steps after it are laid out; a value with 1 consumer fuses with it; consumers that index a value alike fuse with it; consumers that index it differently store it, unless it's recomputed. Recomputed in each consumer's stage: an elementwise chain over inputs and recomputed values, each node at most 1 copy in its row's `cost` (XLA's producer fusion, AOTAutograd's recomputation); reverse mode's residuals take the same rule. A value's life past the graph: reference counts, exact by releasing each name and temporary at its last use. An uncompiled stage stores outputs in its own pass. A fold whose rows hold more than 1 item adds each row into column totals (NumPy `sum(axis=0)`), a compiled kernel into its piece's totals; `inner == 1` keeps totals in registers. Compile decision: a kernel already compiled, or items × passes saved × saving per item × expected runs (loop counts, a recording's reuse) above the compile cost. Forcing reads, the 1 list: display, conversion to Python or JavaScript, writers (`•json⁻¹`); an observing read (a guard's predicate, a count or length, an item read one at a time, `⍎`'s text); a read a call makes of its own operands (part 5's roles: a parameter, values deciding a length; selective assignment's positions, part 16; a call on host values, part 5; an ordinary-integer call with no proved width, part 2); an effect, which outside a recording runs at its call (what it reads, after the effects before it); the memory limit, in bytes: a pending graph whose leaves kept alive (leaf buffers that only the pending graph still references) pass `MAX_KEPT` (8) times its largest array, or whose own nodes pass 1 MiB (about 7,000 nodes; one core's L2), never a count of calls. A top-level statement forces nothing at its end; destructuring forces nothing. Running a graph and reading its result are separate steps (tinygrad's `realize()`, `numpy()`): on a GPU a run submits work and a read waits for its readback. A fold whose totals feed later steps runs with them cell by cell where a cell fits the stage buffer (`fusion::BUFFERED`): each cell folds, then the later steps read its totals and its items from cache; the plan chooses this schedule, never the compile decision. Not: eager executors beside it (`fusion::fold`); a step routed around it to its kernel by kind; lone-call paths beside the pipeline (`Node::compute`'s, `copy_of`, `own_passes`, `fold_pass`); a plan cache beside the 1 cache (`PLANS`); a session registry forcing pending work at effects (`graph::barrier`); executor arms calling own code (`pervasive_apply`, `Operation::run`, `folded_by`). -9. Block stages, JIT (`program/blocks.rs`, `jit.rs`, rustymath `gather.rs`, `agreement.rs`, `reduce.rs`). A stage runs its steps a block of items at a time; it compiles where items × passes saved × saving per item exceeds the compile cost (1 formula, the plan's). Reads go through `Mapping`s by 1 rustymath read plan (periods, tiling, runs), whose consumer passes its run length, read by rustymath's kernels, folds and block stages alike. Lowering reads the object's formula. Compiled code: `jit::KERNELS` owns it (identical kernels compile once); a stage holds a shared handle (`Arc`) fetched once at plan time, a reference, not a 2nd copy, so no lookup per run. Block stages read Booleans, fixed integers, ordinary integers at a proved width, f32, f64, and characters as `i32` code points (`characters::as_codes`, no copy). A step giving characters, f16 steps and complex steps run on rustymath's whole-array kernels outside block stages (main's decision 13); a chain doesn't fuse across them. Every step rounds to its type in a stage or a compiled kernel, as when it runs alone (f16 computes in f32: rustymath's DEV.md); compiled code may reorder a fold's or a product's additions and fuse `a×b+c`, never change integer results, kinds, rounding points, NaN or infinity behaviour, or a dfn fold's order. The JIT generates code with Cranelift, natively only: the browser runs block stages uncompiled until the GPU stage; LLVM is never a target, and the built-in JIT works without an external backend (part 23). Not: a stage's own cache of compiled code (`Compiling.code`); kernel sightings as a compile rule (`Kernel::seen`); basedpl copies of rustymath's read planning; thresholds per consumer; a primitive → machine-op table (`lowered_op`); fold admission re-derived beside row facts. -10. Traces, recordings, cache (`program/trace.rs`, `program.rs` `Programs` and `tacit`, `program/run.rs`). Trace: no mode of dispatch: the graph every call builds anyway, recorded at its end, with capture of free values and opaque inputs (enter unforced; no producer facts). Recording: the scheduled program, `Key`, captured free values, speculation boxes (kinds; leaf range facts, part 2). 1 cache: 1 store keyed by function structure: a dfn by its `Definition`, which every closure of it shares (where its `Programs` live today), never by a closure instance, so a dfn made again on each evaluation (`{⍵+1}¨x` inside a function) reuses its recordings, where a cache keyed by the function object (JAX's `jit`) traces again; its captured free values are recording facts, checked at replay; its entries go when its `Definition` goes (a weak reference); entries no function value owns (a forced graph's plan) are evicted least recently used past 256 entries (a plan is a few kilobytes; a miss costs a schedule); 1 entry per function value, holding recordings, stops, select plans and loop bodies; a forced graph's plan sits in the same store under its graph's structure, leaves as descriptions (part 8). A transformed call is its own function value (part 19: `f∂`, framed, JVP step, VJP step), so its recordings sit in its own entry, never `f`'s, and `Key` needs no transform flags. A call under a level is that level's rule, recorded as the transformed function's run; values of older levels enter as opaque inputs (JAX's lift). Planning run: a case built on a stand-in for its argument (a leaf holding only a description, no items: a read of it ends the run), never forced, ending at its 1st stop; 1 route, inside and outside traces. Key: per argument axis, a length class (0, 1, more: length 1 changes broadcasting) and a power-of-two bucket B (lengths above B/2), a cache-key policy only; a trace records at concrete lengths. Replay checks key, free values and boxes; at entry it refills descriptions and maps from each step's origin (part 4) at the run's concrete lengths, through the row-plan builders the plan used (1 home for map derivation), and reuses the bucket's plan; engine code sees only concrete lengths. A free value read as structure is captured, and a mismatch retraces; a count from the arguments (`n↑`, `n⍴`, a window size) refills partway through the replay; a guard on a traced value stops. Refill runs only where lengths differ from the plan's last refill, and is a subset of planning (descriptions and maps only: no gather, rewrite, order, stage ends or compile decision; "Cost model": 130 against 270 instructions a node). Buckets serve plan reuse only: a new shape costs at most a plan, never a compile, since kernels take counts at run time and compiled code is cached by structure (JAX buckets and pads because XLA recompiles for each shape). Buckets, not a plan per exact shape: a plan's cost-based choices (the compile decision, the cheaper of 2 reads, recomputing a cheap chain) stay within 2× across a bucket, and a growing carry refills each step (265·N) where a plan per shape would schedule each step (405·N). A replay failing its check inside forcing runs the body as ordinary code, never as an error. Recorded bodies: a loop's body, a map's operand on framed values, `∂`'s operand; other code builds its graph unrecorded, its reads running plans from the same store (parts 5, 8). A trace runs on real values (decision 6.4). The key also records which axes' lengths were equal, and the axis names the trace drew, numbered by first appearance across constants, arguments and free values (`•index` draws a fresh name per call; a replay substitutes the current names, keys included). A trace builds a nested argument from its leaves' input values: each leaf is traced. Speculation (decision 3), 1 mechanism inside and outside traces: a node whose kind depends on values takes the likely kind (its range's where that decides it; else, in a recording, the 1st run's; else the real or exact kind: outside recordings nothing remembers a run, since the 1 cache holds outcomes only for recordings) and its kernel checks it (`√` flags a negative); a read whose check fails gives that node the other kind (a host call where that kind is a host kind), and every pending node reading it, in that read's graph or another, goes back through dispatch from its origin when a read reaches it (forcing re-enters dispatch for these nodes alone); the nodes after it are then planned again; in a recording a different kind retraces, counting toward the retrace limit; a stop that a failed speculation causes keeps no outcome for its signature; a replay failing its check runs that call as ordinary code and keeps its entry, a 2nd failure in a row drops the entry and traces again, a replay resets the count (`FAILS` = 2); once a trace has recorded an effect it no longer speculates. Retrace limit: a definition recording past its limit (more signatures in a row than it keeps, no replay between) keeps no new recording; kept ones serve the calls they fit, others run as ordinary code (Dynamo's 8). A recording stops at: a host read of a value that depends on the arguments and decides control flow or a structure other than a length (a guard's predicate, a rank, an axis or a permutation taken from items, an item read one at a time); `⍎`; an error guard (`::`); recursion; a write to a name outside the body (under `∂` a differentiated value written to an outer name is an error, as JAX raises when a tracer escapes); a host-kind value from the arguments; an effect that changes bindings or settings or gives a value read as structure (part 17); a drawn name that becomes characters. No stop: shapes and kinds (in the key); a length the data decides (`⍸`, `#` by a mask, `∪`, monadic `⊤`, Key's groups, a compacting select's counts, a count taken from items: `n↑`, `n⍴`, a window size, `•index n`): a replay computes up to its node and reads the count, which then keys the steps after it as an argument's length does: the plan records its length class, bucket and equalities with other lengths, and the replay compares them as at entry; a match refills the steps after it from their origins (`Plan::refill` from that step), and a mismatch schedules the rest from that node and keeps that plan under the key extended by the count's class and bucket; GPU memory bounds for such lengths wait for the GPU stage, given by users as JAX's `size=` is; a guard on a framed value (a select, part 14); output effects and draws (nodes, part 17). A plan choice that depends on an exact length (an unrolled short fold, a one-item read chosen by a repeat's length, grouping by equal lengths) is never made on a bucketed axis: refill derives descriptions and maps only. A buffer that a growing carry writes is allocated at its bucket's bound, so an append stays in place. Not: 2 recording stores (a dfn's `Programs`, `program::tacit`'s map); a trace as a dispatch mode (`in_trace`, `fusion::builds` branches); trace state in `transform.rs`; a 2nd cache; a cache keyed by a closure instance; a 2nd route for kinds that depend on values (read at the call outside traces, speculated inside); plans rebuilt per call; recordings assuming producer facts; transform flags in `Key` or `serve` (`frame`, `step`, `held`); a lookup declined under a level; bounded extents (extent nodes, map templates, pins, rows' `bounded` flags, `fixed()` guards, rustymath's generic `Traced` maps); a bounded rule per primitive; re-planning per shape; shape rules generic over a lengths type (`Lengths`, `Known`, `Bounded`) where every caller passes known lengths. -11. Levels (`transform.rs`; JAX traces). 2 kinds: `Derivative` (duals, tags), `Batched` (framed values). A call goes to the newest level among its arguments; a level's rule builds through dispatch. Levels nest, any kind inside any, as JAX's traces do: a map inside a batched run or inside a `∂` begins its own `Batched` level, as does a framed body (part 13). The running batched level is the innermost; a batch's level is found by its id. A level's rule takes a value of an older level as an unbatched operand, and its own calls pass that value on to its level (JAX's lift). Guard cases split the innermost run's cells. A nested level's extra rule pass costs only a map's 1st run, which the cache records. Not: `Trace`, `Record`, `Label`; engine code calling into a level; carriers beyond dual and batch; widening a running run (`Entry::Widen`, a `Case` with no definition, `batch::widened`, `Levels::widened`), a 2nd route for a map inside a map that nesting serves and correctness doesn't need; a refusal to begin a batched run inside one (`Kind::admits_batch`), which sends a map inside a `∂` inside a map to a call per cell. +1. Values (`array.rs`, `array/storage.rs`). `Value`: atom | stored array (`ArrayData`: compact, segmented or mixed) | pending (`Value::Pending`, a node's output whose description gives its atom flag) | carried (a level's value, dual or batch: `Value::Carried`, 1 home). A view of a stored array: a layout over its buffer, no node; of a pending value: a `Copy` node. Segmented storage is built where made (the gather finish applies the compact rule below); writes keep it. Every host read of a node's items passes `Node::into_storage`, the chokepoint in part 10. It refuses unbound linear reads and judges other reads by the input roles they reach. `ArrayData::storage` reads stored arrays, which are not trace inputs. A partial read of a pending value (an item, a part) is a `Copy` node read through the chokepoint; the scheduler computes only what it reads, as tinygrad pushes a shrink into its kernel. Kinds in the graph: Boolean, fixed and ordinary integers, floats of 16, 32 and 64 bits, complex, characters; on the host: rationals, big integers, arrays mixing characters with numbers or exact with inexact, functions, records as containers. A record's field or a host array's item that is a compact array enters the graph as a leaf when read. A nested array may hold a pending value as an item, as a pytree holds a tracer; compact storage resolves one. Segmented: an entry is a contiguous (offset, length) run of 1 leaf buffer, no stride (strided cells are copied into the leaf), with each item's shape for items of rank 2 or more; items of 1 shape need no entries (stacked); a leaf is stored compact data (a buffer or a view over one), or itself segmented. Entries, shapes and prototype items are stored too. A segmented array with a pending part is `Value::Pending`, described by segments (part 2). Entries enter a recording as 1 integer input. An empty segmented array keeps its prototype item. Made by the compact rule: every item an array of 1 rank and storage kind, no keys or names on its axes; any other nested array stays mixed, and the walk writes through it level by level (part 16). Pervasive calls between segmented arrays: segmented agreement (each item's lengths checked when the call runs; length-1 items expand; LENGTH ERROR names the call). Frequent readers read segments directly (Each, enlist, mix, pick, first, depth, match, search, unique, sorts, display, Python, `•csv`, `•json`); ragged cells compare through 1 comparator in rustymath `search` (a leaf slice with its shape: equality, hash, `array_order`'s order). A leaf buffer: host memory the allocator owns; later a mapped file or GPU memory (part 23). Not: caches holding items twice; packing per producer; reads past the chokepoint; carriers in storage (`Data::Carried`); level code in storage. +2. Descriptions (`program::Desc`; JAX abstract eval). Kind, atom flag, layout, range, segments (items' rank, whether they share 1 shape, the leaf's own segments). A step's description comes from its origin through the row-plan builder's describe, in 1 kind, shape and range pass over operand descriptions and parameter values before the step is chosen. `Desc::leaf` describes a leaf once as it enters the graph. Kinds: Boolean; fixed integer of a width; ordinary integer; float of 16, 32 or 64 bits; complex of either width; character; weak or strong. Logical kinds and their join have 1 home, `number::Kind` and `number::Kind::join`; there is no `program::Kind`. Canonical rustymath machine-dtype promotion remains shared. The single stored-result joining policy in `array/storage.rs` serves assembly and graph descriptions through that logical join; there is no `program::Widening`. Under static kinds, results computed apart (writes, compacting selects, map assembly, `⍣` histories) join to 1 compact kind, as JAX gives 1 dtype to `vmap`, `where` and `scan` outputs; strands and rows keep each item's kind; dynamic kinds keep each kind. Wider accumulators come only from seeds and conversions (`0ₓ₈+/u`, `(aₓ₄)+.×bₓ₄`); no accumulator field. Kinds that depend on values (dynamic kinds): `√ * ⍟ ○` on reals (complex) and `÷` of exact integers (rational, a host kind); where ranges don't decide such a kind, the call reads the operands it depends on (part 5's roles), inside and outside traces alike (part 10); the primitive real forms in part 21 and `÷ₓ` (floor division to an exact integer) resolve before the unmarked call's kind rule and reads; their kinds never depend on values. `⌊ ⌈` of floats give ordinary integers, saturating at the 64-bit range (`⌊∞` the largest, `⌊¯∞` the smallest; NaN's result unpromised); monadic `×` of floats gives an ordinary integer. Arithmetic on ordinary integers can widen too: ordinary integers stay in the graph where ranges prove a width, at that width; otherwise a host call (part 5; decision 16). Keyed and named layouts are graph values: in keyed agreement a key missing on one side reads as the fill (selecting a missing key is an INDEX error: part 20); keyed agreement is a node whose maps and length come from the keys at run time; a cache key compares key sets by identity before contents; a layout's renderer travels in descriptions. Ranges (DuckDB statistics): each numeric node's range comes from its object's interval rule over its operands' ranges; a leaf's when made, or unknown. Ranges replace rustymath's `nonnegative`, `total_on` and `keeps` facts and descriptions' `nonnegative` mark. They keep `√` and `⍟` real with no read at the call where arguments are in range, keep ordinary integers in the graph where they can't overflow, drop exact folds' `fits` checks, and drop a gather's bounds check where its index range lies inside its axis. Ranges may prove the object's repair predicate (part 6) false and remove its per-item test and repair branch. This uses that same predicate, not another threshold or admission policy. Any range assumed by this proof belongs to the plan's range boxes and is validated on cache hits and refill before execution. A read's kind and range include every numbered source it may select, including its chosen fill, from descriptions without reading their data. This covers an invalid static read's fallback wherever validity is not proved. A plan that relies on a leaf's range (a sign, an integer width, an index inside its axis) keeps each such fact as a range box, checked on every hit against the leaf's carried range and the run's lengths; a leaf of unknown range fails the box, under part 10's box policy. An ordinary integer's proved storage width is part of its kind, so keys hold it. A loop carry's range widens to unknown at the loop's head unless the body provably keeps it (abstract interpretation). A fold's range uses a proved bound for its actual item count or stays conservative across the lengths the plan accepts. Refill must validate any range or storage-width assumption before execution. A changed kind or failed range box follows part 10's normal miss policy. Not: a description from a value's `layout()` in a trace; parallel description helpers; a 2nd shape rule (`Step::extents`); `narrows`. +3. Graph (`graph.rs`; tinygrad `UOp`). `graph::Node`: a step and its origin's operand list, each read through a rustymath `Mapping`, with a description per output (a final carry, stacked states and residual stacks can differ); or a leaf: an input (a key position, role and, for a free value, its name; no step) or a computed buffer. Interned by the structural origin facts in part 4 (`keeping`), leaves included. A view = a `Copy` node composed into its readers' maps. Forced → `result`: a read replaces the node with its result and drops its step and operands (tinygrad's `realize`), so later graphs read it as a leaf and no rule matches across it; a run that sets the error slot replaces no node downstream of the failing one (Measures). `Live::of` walks a read's live nodes, hashes its key and gathers its inputs. `Program::of` converts them to a program only on a cache miss; the scheduler, rules, recordings and `verify` read only `Program`. A view is stored, at 1 copy, only for a reader whose kernel can't read through a map. D's single numbered-source read planner composes copy chains without materializing them. Single-source affine maps use `Mapping::composed`; `Steps::through` handles composition by inner axes where affine maps do not suffice. Fill routes remain reads of scalar sources (part 4). One capability test (`reads_through`) decides whether a kernel consumes the resulting read plan or needs storage. Readers of a shared pending copy compose their own reads without storing that copy. Not: node builders outside dispatch; scheduling the live graph directly. +4. Steps (`program::Step`): the IR. `Map Convert Copy Gather Fold Product Select At Picks Join Call Draw Effect Loop Index` (`Index`: an affine index step). A variant has `run::step`'s arm, `glyph` and map derivation (`Step::maps`). Its operands are its origin's list, named by position; no variant holds another list. The origin keeps the operation; function values; operands as reads in call order; all captured free data and operator data, snapshotted and flattened at the call with `Trees` (part 10); roles; literal marks; mode (static or dynamic kinds, Mathish); argument trees for flattened calls; the call's source site. Internal calls preserve that site. Host operations key the source base needed for relative paths, not diagnostic ranges or source text. Cached execution binds current sites. Numeric keys omit source sites. Refill derives descriptions and maps through the same builder, including rule outputs and lowered steps. Numeric values a kernel reads, such as tolerance and a loop's convergence tolerance, are operands with parameter roles named by the row's `settings`. Mode is bits, not a parameter read. Callable origins also capture the numeric defaults needed to interpret deferred bodies. A scoped activation uses those setting inputs and the origin's mode. Activation never changes process defaults; an explicit preferences effect updates defaults and active contexts through the same settings home. Pending nodes keep their call's settings. A route is a step, never a builder. A new job takes an existing step where one serves. Batching and JVP rules belong to rows, or beside `Loop`; transposes are the pass in part 15, keyed on linear facts. Index steps: `⍳` and `•index` are affine generators, never stored; `+ -`, negation and `×` by a constant keep them affine. Selection plans strides, wrapped and padded reads from their coefficients (`Stride`, `Rotated`, `Padded`). Clamped and reflected reads use `Padded` maps with `•edge` modes, not gathers. `Table::affine` stays for index arrays that are data. A chosen numerical fill is scalar data, described by kind and range without reading it. An options record supplies its structure as parameters but extracts its fill field as a data input; settings, counts, ranks and tolerances remain parameters. `Copy` takes its base and optional fill as numbered sources; `Copy` and `Join` share rustymath's `ReadPlan`, whose route gives `(source, position)`. Runtime views bind those source numbers. A fallback reads another scalar source; rustymath mappings contain no BPL `Value`, and binding never mutates a cached map. `Call` runs a planned primitive outside block stages (sort, grade, search, `⌹`) on forced operands in its own stage. `Effect` reads the preceding effect's token and its own operands; a stage ends at it. `Join` uses that same numbered-source read plan, with positions found by running the moving primitive on ids. It serves strands, `,`, `⍪`, compaction, Under writes and joins' transposes. Read-back distributes cotangents to every source through part 15's one mechanism. +5. Dispatch (`Operation::dispatch(Origin)`; JAX `Primitive.bind`; tinygrad's lazy `UOp`). 1 entry for operations on values: primitives (pairing axis, matrices, at), primitive folds and products, selects, loops, strands and array notation (`Join`), conversions, draws, effects. Semantic forms resolve first: a defined form marked by its row (monadic `≤ ≥ ⊼ ⊽`) runs the row's code, whose calls return to dispatch; the marked-form kind entry resolves part 21's listed primitive forms before the unmarked producer's kind rule, operand reads or level handling. All captured free data and operator data join the call's ordered inputs before level choice (part 10). Then the newest level's rule takes the call and calls dispatch for its parts. Otherwise `Operation::roles` gives operand roles before any read: data, or read at the call as a parameter, a length or a value-dependent kind (part 2). Reads use part 8's policy and key by part 10's read kind. Describe, step selection and maps share one borrowed call view over the origin, bound functions, parameter values and operand descriptions. The builder describes once before `Operation::plan` chooses the step (else `Call`, running the row's code), then derives maps through `Step::maps`. There is no owned `CallPlan` or duplicate resolved-call record. The 1 builder (`fusion::built`) builds a node that waits. A host call runs its row's code on host values outside the graph. Expensive small builds require a cheaper builder, never an eager route. Executors apply the origin's checking mode (Measures); errors do not justify computing at a call. Engine code calls dispatch, never a builder. A read of a pending item's part is a `Copy` through this builder. A fold to an atom stays pending. Check dispatch and reads against the cost model. Not: a 2nd entry (`call_axis` beside dispatch); eager and traced modes (`fusion::builds`); per-call force policies (`fusion::gate`, `fusion::waits`, `wait_limit`); builders in consumers (`structural::copying`, `pervasive_apply`'s `defer`, `Operation::node`'s kind arms, `fusion::node` from operators, derivatives or distributions); executors beside `run::step` (`pervasive_apply`, `structural.rs`'s copies); speed rewrites at dispatch (`taken_right`, selecting-form copies); rule marks; special pending-atom readers (`views`, `pending_atom`): roles replace them. +6. Rows (`primitive.rs`) and rustymath objects (`facts.rs`, `recipe.rs`, `structure.rs`, `derivative.rs`). Row: its object; plan (kind and shape rules over known lengths); batching (`cells`: operation, wholes and frame count in, a dispatch call out, including a loop's batched form); inverse; Under form (structural, or computational through the inverse); linear facts (linear operands, jointly or each, and zero-tangent calls, as JAX's `deflinear` and `defbilinear`); identity; cost; row facts (`passes`, `alternates`, `first_axis`). The row's identity reads its object's `identity` where one exists; structural functions such as `,` hold their own. There is no transpose per row (part 15). Object: its common formula, optional separately named repair predicate and repair routine, partials, ranges, numeric validity conditions and fold properties, each once. The repair predicate is itself an Ops formula returning one Boolean per item, shared by native and JIT execution. It owns width-dependent limits, including trig's large-reduction limits of 2^23 for f64 and 2^20 for f32. `Object::test` remains a Boolean-result formula for comparisons, not this repair predicate. Native SIMD computes the common formula, then repairs flagged items with the object's scalar routine before consumers use them. A proved-false predicate needs no test or branch (part 2). Shared formulas over `Ops` are the default for elementwise arithmetic. A substantial algorithm can instead be one object-owned `Call` through the same builder and runner. Its common computation is never duplicated in a formula and a legacy kernel. Checked numeric validity is an Ops Boolean predicate paired with its error kind, shared by native and JIT execution; no predicate means total. Structural validity belongs to the operation's row. The builder derives a step's potential to record a data error from these facts, its mode, descriptions and existing range proofs. Unknown means potentially raising. A case or loop plan combines this property over its steps; there is no separate validity evaluator. Native and compiled kernels instantiate the same width-specific formula over their `Ops` (recipe). The numeric Kernel interface preserves every supported machine kind for each operation, independent of block/JIT admission: Boolean, fixed integers, ordinary integers at a proved width, f16/f32/f64 and native complex. f16 works in f32. Host exact arithmetic remains outside this machine interface and uses the same canonical mathematical facts, not the legacy kernel engine. The numerical algorithms below specify the shared bodies. Defined forms call other primitives through dispatch (part 5). Not: the old `Kernels`/probe engine as a fallback for missing new bodies; kind-specific rule files; parallel facts tables (`rules!`, `Partials`, recipe bodies); name checks replacing facts; duplicated math (`lowered_op`, duplicated common scalar math beside a formula). +7. Rewrite engine (`program/rules.rs`; tinygrad `PatternMatcher`). 1 table: BPL pattern rows and structural rules (fold of a copy, contraction, sliding fold, copy commute). A rule: a root key (data: step kind, root primitive) that rejects; 1 match giving bindings; 1 builder from them. Runs in the scheduler's pipeline on `Program`s after `∂` and batching, a read of a lone node included (part 8). Every pending value has its origin, so a rule matches in every spelling: `⍟1+x` → `log1p` from 1 row, across statements, bindings, dfns and trains; a read replaces a node with its result (part 3), so a rule doesn't match across a read of its intermediate. Speed rules run after `∂` and batching (XLA's simplifier after JAX's transforms), so the hidden specials they introduce (partial sorts, ranks, `log1p`, `expm1`) need only plans and kernels; a pattern also matches its primitives' batched forms, which carry an axis. A rule may change last bits, never leave the documented accuracy bounds (kmeans' cancelling rule is out: decision 14); each rule has a reason that depends on no model, and a reference case. The table: selections of a grade or sort (`k↑⍋⍵`, `↑⍋⍵`, `i⌷⍋⍵`, `i⊃⍋⍵`, for `⍋ ⍒ < >`) as partial sorts; `⍋⍋⍵`, `⍋⍒⍵` as ranks; `⍟1+⍵` as `log1p` and `¯1+*⍵` as `expm1` (accuracy); a `+` fold of `×` whose operands read through affine maps as 1 product (index notation's level 3); a fold of a copy that repeats items as a fold of its source; a dfn fold whose body is 1 associative primitive (`{⍺+⍵}/`) as that primitive's fold; `J+.×v` as a JVP and `u+.×J` as a VJP, where `J` is a Jacobian `∂` built and nothing has computed. Not rules: mandatory marked-form semantics (parts 5, 21); a combination of 1 call matched by its function's structure is its operator's own algorithm (monadic `-.×`, `+.×`: `primitive_expansion`); Key's counts and folds by group come from batching over segments (part 12). Not: a test written twice (a pre-check beside the match); a rule tried from 2 places; 1 rule in 2 directions written as 2 rules; marks and their machinery (`marks`, `UNMARKED`, `FunctionData.needs`, `call_marked`, `rules::pairs`, `rules::at_once`); mark rows bound to function values by spelling (`FunctionData.rows`, `Function::new`'s row filling, `operators::marked`, `spelled`, `marker`). +8. Scheduler (forcing: `graph.rs`, `program.rs`, `program/run.rs`). 1 pipeline, `schedule(&Program, &Running, from, runs) -> Plan`, then run preparation and execution. Scheduling gathers, rewrites, orders and fixes stage ends (tinygrad `indexing.py`), outputs, legal cheap-chain recomputation, folds sharing stages, and select lowering (masked | compacting, from descriptions). The cached plan holds this structure and kernel recipes. Run preparation uses actual counts to decide compilation, contraction's cheaper read, and recomputation where producer/consumer multiplicity affects its cost. These are cheap choices within the plan, outside item loops. A choice that changes an assumed kind or structure uses the normal guard/miss path (part 10). No second scheduler or general multiversioning framework. The same pipeline serves forcing on a miss, a trace's end and replay planning for the rest after `Ran::Rest`. `from` is 0 or the rest's first node; the one `Running` state supplies bound inputs, callable/template bindings and already-computed prefix values, including when scheduling the rest; `runs` is expected reuse for the compile decision. A hit at unchanged lengths copies no description or map. Forcing records checked errors in the session's slot and never raises them itself. A read uses `Live::of` before the cache lookup (part 3) and runs its graph's plan from the 1 cache (part 10), keyed by structure and part 10's input read kinds, with range boxes checked on each hit (part 2) and refill where lengths or count parameters changed; only a miss schedules, so a read of a graph seen before costs a key, a probe and the run (cost model). A lone node is a graph of 1 (JAX's 1 compiled program per primitive and signature; tinygrad's kernels cached by structure). Stage ends (tinygrad): a node whose length the data decides ends its stage, its count read before the steps after it are laid out; a value with 1 consumer fuses with it; consumers that index a value alike fuse with it; consumers that index it differently store it, unless it's recomputed. Eligible for recomputation in each consumer's stage: an elementwise chain over inputs and recomputed values, each node at most 1 copy in its row's `cost` (XLA's producer fusion, AOTAutograd's recomputation). Where item counts cancel, this is structural. Otherwise run preparation compares actual producer/consumer multiplicity. Reverse mode's residuals take the same rule. A value's life past the graph: reference counts, releasing each name and temporary at its last use. The plan chooses masked or compacting execution before running (parts 13, 14). Data errors never restart a read. Descriptions remain immutable. There is no masked-error retry state, revised-origin decision, forwarding, protected retry root or carry, or replay of completed effects and random counters. An uncompiled stage stores outputs in its own pass. A fold whose rows hold more than 1 item adds each row into column totals (NumPy `sum(axis=0)`), a compiled kernel into its piece's totals; `inner == 1` keeps totals in registers. Run preparation uses a compiled kernel when available. Otherwise it compiles the selected recipe when actual items × passes saved × saving per item × expected runs (loop counts, a recording's reuse) exceeds the compile cost. Until then the same stage executor runs its blocks uncompiled. Forcing reads, the 1 list: display, conversion to Python or JavaScript, writers (`•json⁻¹`); an observing read (a guard's predicate, a count or length, an item read one at a time, `⍎`'s text); a read a call makes of its own operands (part 5's roles: a parameter, values deciding a length or a kind that depends on values; selective assignment's positions, part 16; a call on host values, part 5; an ordinary-integer call with no proved width, part 2); an effect, which outside a recording runs at its call (what it reads, after the effects before it); the memory limit, in bytes: a pending graph whose leaves kept alive (leaf buffers that only the pending graph still references) pass `MAX_KEPT` (8) times its largest array, or whose own nodes pass 1 MiB (about 7,000 nodes; one core's L2), never a count of calls. A top-level statement forces nothing at its end; destructuring forces nothing. Running a graph and reading its result are separate steps (tinygrad's `realize()`, `numpy()`): on a GPU a run submits work and a read waits for its readback. A fold whose totals feed later steps runs with them cell by cell where a cell fits the stage buffer (`fusion::BUFFERED`): each cell folds, then the later steps read its totals and its items from cache; the plan chooses this schedule, never the compile decision. Not: eager executors beside it (`fusion::fold`); a step routed around it to its kernel by kind; lone-call paths beside the pipeline (`Node::compute`'s, `copy_of`, `own_passes`, `fold_pass`); a plan cache beside the 1 cache (`PLANS`); a session registry forcing pending work at effects (`graph::barrier`); executor arms calling own code (`pervasive_apply`, `Operation::run`, `folded_by`). +9. Block stages, JIT (`program/blocks.rs`, `jit.rs`, rustymath `gather.rs`, `agreement.rs`, `reduce.rs`). A stage runs its steps a block of items at a time and compiles by part 8's formula. Rustymath's one numbered-source `ReadPlan` serves kernels, folds and block stages alike (periods, tiling, runs). It routes `(source, position)` through positional mappings and binds runtime source views once, including scalar fallbacks; the consumer supplies its run length. Lowering reads the object's common formula and repair predicate (part 6). Unless ranges proved the predicate false, the JIT branches only when a vector has flagged lanes, calls the same object-owned scalar repair only for those lanes, and makes the corrected result available before downstream fused operations. No JIT-specific repair threshold or mathematical implementation. `jit::KERNELS` owns compiled code, keyed by emitted structure, types and modes. Counts are run-time arguments, not code-cache keys. Run preparation selects the useful recipe from actual counts and compiles it on demand under part 8's formula. It does not compile every possible version. Different counts share code unless they select genuinely different emitted code. A selected version holds its shared handle (`Arc`) after the first lookup. Plans hold references to that code, never another code cache or copy. Block stages read Booleans, fixed integers, ordinary integers at a proved width, f32, f64, and characters as `i32` code points (`characters::as_codes`, no copy). A step giving characters, f16 steps and complex steps run on rustymath's whole-array kernels outside block stages; a chain doesn't fuse across them. Kernels preserve kinds, explicit conversions, exact results and the documented accuracy bounds. They may reassociate fold or product additions and use FMA for `a×b+c`, including different overflow near the range limits. Last bits, infinities and NaNs, and zero's sign need not agree across routes. A dfn fold retains its specified order. The JIT generates code with Cranelift, natively only: the browser runs block stages uncompiled until the GPU stage; LLVM is never a target, and the built-in JIT works without an external backend (part 23). Not: a stage's own cache of compiled code (`Compiling.code`); kernel sightings as a compile rule (`Kernel::seen`); basedpl copies of rustymath's read planning; thresholds per consumer; a primitive → machine-op table (`lowered_op`); fold admission re-derived beside row facts. +10. Traces, recordings, cache (`program/trace.rs`, `program.rs` `Programs` and `tacit`, `program/run.rs`). Trace: no mode of dispatch: the graph every call builds anyway, recorded at its end, with opaque inputs (enter unforced; no producer facts). Every call snapshots all captured free data, including stored scalars/arrays and operator data, into ordered origin inputs after its own operands, flattened with existing `Trees`. Captured records flatten the same way. Dispatch's level choice, reach, interning and keys see these inputs. Free and Argument roles retain their read semantics. The original callable binds from the current call's inputs, with no per-call frozen function or record allocation. Deferred execution and replay never reread these values from the session. Functions made inside a recording or a rule use part 19's one structure-only template/rebuild and positional data dependencies; cached programs and plans hold no live closure or data producer. Recording: the scheduled program, `Key` and range boxes (leaf range facts, part 2). 1 cache: 1 store keyed by function structure: a dfn by its `Definition`, which every closure of it shares (where its `Programs` live today), never by a closure instance, so a dfn made again on each evaluation (`{⍵+1}¨x` inside a function) reuses its recordings, where a cache keyed by the function object (JAX's `jit`) traces again; its entries go when its `Definition` goes (a weak reference); entries no function value owns (a forced graph's plan) are evicted least recently used past 256 entries (a plan is a few kilobytes; a miss costs a schedule); 1 entry per function structure, holding recordings, stops, select plans and loop bodies; a forced graph's plan sits in the same store under its graph's structure and the input key rules below. A transformed call is its own function value (part 19: `f∂`, framed, JVP step, VJP step), so its recordings sit in its own entry, never `f`'s, and `Key` needs no transform flags. A call under a level is that level's rule, recorded as the transformed function's run; values of older levels enter as opaque inputs (JAX's lift). Planning run: a case built on a stand-in for its argument (a leaf holding only a description, no items: a read of it ends the run), never forced, ending at its 1st stop; 1 route, inside and outside traces. If a select case or loop body cannot plan its result, the call reads its data operands as values deciding a length, then traces on their values to obtain descriptions, a recording and effects. An outer trace judges these reads normally. If this trace also stops, the call runs ordinary code at the call (part 13). The read chokepoint judges a read by the inputs it reaches. Inputs include arguments, free values, planning stand-ins, write roots and `∂` seeds, each with a role; a free value also has its name. The reader names the read (count, structure or value), not the input's role. A read reaching a stand-in ends planning. A count records a length the data decides. Other reads reaching arguments or write roots stop recording; those reaching only free inputs key the call-time snapshot by the existing `ReadKind`, without a separate captured-facts path. Unbound linear reads are refused. Key: the structural origin facts in part 4, then literals by value (their marks distinguish literal atoms from named values), function values by structure, data by description under the length policy below, parameters by read kind: counts (`n↑`, `n⍴`, window sizes, `•index n`) by length class; kind reads by the kind decided; other parameters (axis, permutation, rank, rotation amount, sign, array) by value. Each data-input axis has length class 0, 1 or more (length 1 changes broadcasting). There are no size buckets. Kinds, ranks, layout and structural facts retain their normal key and guard rules. A trace records at concrete lengths. Replay binds the current call's inputs and checks its key and boxes; at entry it refills descriptions and maps from each step's origin (part 4) at the run's concrete lengths, through the row-plan builders the plan used (1 home for map derivation), and reuses the structural plan while its assumptions hold. Engine code sees only concrete lengths. Structural free reads key their snapshotted values by `ReadKind`; a key miss traces normally; a count from the inputs (`n↑`, `n⍴`, a window size) refills partway through the replay; a guard on a traced value stops. Refill runs only where lengths or count parameters differ from the plan's last refill. It derives concrete descriptions and maps through the existing builders without gathering, rewriting, ordering or placing stage ends ("Cost model": 130 against 270 instructions a node). Run preparation makes part 8's cheap actual-count choices. The plan retains rewrites, order, stage/fusion structure and kernel recipes. Length classes do not waive assumptions about kinds, ranges, layouts, structural parameters or axis equalities. If refill invalidates one, the existing guard/miss path applies before executing the plan. A changed ordinary-integer width or count-dependent range proof follows the same rule (part 2). A new length within these rules refills the plan without another trace or schedule. Compiled code remains parametric in counts and is shared by emitted structure, types and modes (part 9). A replay failing its check inside forcing runs the body as ordinary code, never as an error. Recorded bodies: a loop's body, a map's operand on framed values, `∂`'s operand; other code builds its graph unrecorded, its reads running plans from the same store (parts 5, 8). A trace runs on real values (decision 6.4). The key also records which axes' lengths were equal, and the axis names the trace drew, numbered by first appearance across constants, arguments and free values (`•index` draws a fresh name per call; a replay substitutes the current names, keys included). A trace builds a nested argument from its leaves' input values: each leaf is traced. Range boxes (part 2): a replay whose box fails runs that call as ordinary code and keeps its entry, a 2nd failure in a row drops the entry and traces again, a replay resets the count (`FAILS` = 2). No kind is speculated: a kind that depends on values is read at the call where ranges don't decide it (part 2). Retrace limit counts genuine signature changes, never a changed length alone. A definition recording past its limit (more signatures in a row than it keeps, no replay between) keeps no new recording; kept ones serve the calls they fit, others run as ordinary code (Dynamo's 8). A recording stops at: a host read of a value that depends on the arguments and decides control flow, a kind that depends on values (part 2) or a structure other than a length (a guard's predicate, a rank, an axis or a permutation taken from items, an item read one at a time); `⍎`; an error guard (`::`); recursion; a write to a name outside the body (under `∂` a differentiated value written to an outer name is an error, as JAX raises when a tracer escapes); a host-kind data operand used by the call; an effect that changes bindings or settings or gives a value read as structure (part 17); a drawn name that becomes characters. No stop: shapes and kinds (in the key); a length the data decides (`⍸`, `#` by a mask, `∪`, monadic `⊤`, Key's groups, a compacting select's counts, a count taken from items: `n↑`, `n⍴`, a window size, `•index n`): a replay computes up to its node and reads the count, which then keys the steps after it as an argument's length does: the plan records its length class and equalities with other lengths, and the replay compares them and the plan's guards as at entry. A match refills the steps after it from their origins (`Plan::refill` from that step). A mismatch schedules the rest using the same `Running` prefix and keeps that plan under the count-extended key in the same cache; GPU memory bounds for such lengths wait for the GPU stage, given by users as JAX's `size=` is; a guard on a framed value (a select, part 14); output effects and draws (nodes, part 17). An exact-length assumption that affects rewrite or stage legality must be proved or guarded under the normal rules. It cannot be inferred from the class "more". Cost choices use actual counts outside item loops (part 8), without another scheduler or plan cache. A growing carry uses geometrically grown buffer capacity independently of plan keys. An append stays in place while that capacity suffices. Not: 2 recording stores (a dfn's `Programs`, `program::tacit`'s map); a trace as a dispatch mode (`in_trace`, `fusion::builds` branches); trace state in `transform.rs`; a 2nd cache; a cache keyed by a closure instance; a 2nd route for kinds that depend on values (read at the call outside traces, speculated inside); plans rebuilt per call; recordings assuming producer facts; transform flags in `Key` or `serve` (`frame`, `step`, `held`); a lookup declined under a level; bounded extents (extent nodes, map templates, pins, rows' `bounded` flags, `fixed()` guards, rustymath's generic `Traced` maps); a bounded rule per primitive; re-planning per shape; shape rules generic over a lengths type (`Lengths`, `Known`, `Bounded`) where every caller passes known lengths. +11. Levels (`transform.rs`; JAX traces). 2 kinds: `Derivative` (duals, tags), `Batched` (framed values). After part 5's semantic forms resolve, a call goes to the newest level among its arguments; a level's rule builds through dispatch. Levels nest, any kind inside any, as JAX's traces do: a map inside a batched run or inside a `∂` begins its own `Batched` level, as does a framed body (part 13). The running batched level is the innermost; a batch's level is found by its id. A level's rule takes a value of an older level as an unbatched operand, and its own calls pass that value on to its level (JAX's lift). Guard cases split the innermost run's cells. A nested level's extra rule pass costs only a map's 1st run, which the cache records. Not: `Trace`, `Record`, `Label`; engine code calling into a level; carriers beyond dual and batch; widening a running run (`Entry::Widen`, a `Case` with no definition, `batch::widened`, `Levels::widened`), a 2nd route for a map inside a map that nesting serves and correctness doesn't need; a refusal to begin a batched run inside one (`Kind::admits_batch`), which sends a map inside a `∂` inside a map to a call per cell. 12. Maps (`batch.rs`, 1 map entry; JAX `vmap`). Each, Rank, outer product, stencil, `f.g`, Key, Partition, ragged Each: 1 entry runs the operand once on framed values under `Batched`; rows' batching rules apply; ragged cells and Key run over segmented arrays; draws take their place in a map from the levels (part 17). Under `Batched` a framed value keeps only the frame axes it depends on (JAX's `not_mapped`): its whole keeps each frame axis in order, length 1 where it doesn't vary; a value whose cells must stay apart (a draw, a compacting select's mask) is laid over the whole frame first. Segmented maps: elementwise steps run on the leaf buffer (DuckDB's `list_transform`); folds and scans run by segment; a primitive with no segmented rule runs once per segment and joins the results; Key keeps each row's class number, so folds and counts by group reach `fold_groups` and `class_counts` with no gather. A map whose body returns an axis name it drew runs cell by cell, with a nested result. Not: evaluators per operator (Rank's shortcuts, Key by spelling); own-code arms by name in `batch::call`; per-cell fallback other than a stopped run. -13. Loop node (`Step::Loop`; Dex `for`, JAX `scan`, `while_loop`). `⍣`, dfn folds, scans. Body: a function value, replayed from its recording in the cache. Runs as an in-item kernel, a one-item kernel, or the host runner replaying the plan. Rules beside the node: batching, JVP, transpose (residuals: a host stack of per-iteration values, its length known when the forward loop ends; the reverse runner replays the VJP step once per iteration; recomputing, `Until::Rerun` today, only as checkpointing past the memory budget), substitution. Each builds a loop node whose body is the transformed function value, recorded on its 1st run, as JAX's rules bind a new `scan`/`while` on `batch_jaxpr`/`jvp_jaxpr`/the transposed jaxpr: batching → `f` framed (a nested `Batched` run over the leading frame axes); JVP → `f`'s JVP step; transpose → its VJP step. BPL re-runs functions where JAX transforms jaxprs (decision 6.4), so a transformed body is a function value (an operator applied to `f`, part 19: 3 internal operator kinds, framed, JVP step and VJP step; a kind's parameters are structure only: frame axes, packing, direction; values such as a fixed argument and its tangent enter as arguments or captured free values). The next level's rule then takes a transformed body like any body: `∂` of a fold over batched lanes (core 3526), `∂` under a map, nested `∂`. A loop under levels takes the newest level's rule, nothing else. A framed body begins a nested `Batched` level (part 11). Masked exit (batched predicate, `⍣∞`; JAX batches every carry): every lane steps until all exit; a finished lane keeps its carry; a masked loop that sets the error slot clears it and reruns the whole read, the loop starting again from its initial carry dropping finished lanes each step (refill), which raises only for live lanes; admitted wherever the body is effect-free. Inputs: the initial carry, values constant across iterations (`⍣`'s left argument), a fold's or scan's items; outputs: the final carry, the stacked states of a history or a scan. Exits: a count; `∞` once |new−old| ≤ t×max(|new|,|old|) at `Numeric::convergence()`'s tolerance; a predicate graph on the new and old states. A count array runs 1 loop per sign up to its largest count, storing the state at each requested count; negative counts and `¯∞` loop over `f⁻¹`'s body. A history is regular when the carry's shape is fixed, segmented when it changes. The in-item kernel takes an elementwise body (pervasive in the carry, other arrays read item by item, no folds, structural steps or effects) with a count or `∞` exit; under `∞` each item stops when it converges. A loop compiles by the scheduler's rule on its total work; with a count the data decides, once its iterations so far cost more than compiling (LuaJIT, V8). A failed kind check flags its iteration: a one-item kernel exits with the iteration and its carry, a host loop stops there, and the host reruns that iteration on real data or retraces for the new kind; an elementwise kernel's whole loop reruns from its initial carry. A body with effects runs only on the host runner, its effects in order on every iteration. A body that can't record (recursion; an inverse that reads values on the host: `⍸⁻¹ ↕⁻¹`, `,⁻¹` along an axis, `⊥⁻¹`) runs as ordinary code on every iteration on the host runner. An exact integer carry under dynamic kinds stays in the loop where its widened range proves a width; otherwise its arithmetic runs on the host every iteration. Checkpointing: past the memory budget, keep every k-th carry, drop residuals, and recompute each segment's residuals from its 1st carry (Futhark's stripmining: about √N carries stored, 1 extra forward run); a count fixed at the start sets k; with a count the data decides, drop residuals when the stack 1st fills the budget, then keep every other stored carry each time it fills. Not: several steppers; a Rust step (`Stepper`, `items_loop`'s `frame` branch) as a body; a frame or linearization point threaded through the runner and its helpers as data; host loops beside the runner; per-caller variants of the loop's JVP or transpose. -14. Selects (`Step::Select`). `⍚`, `f@[p]`, guards on framed values. Case plans from descriptions by 1 planning route (part 10), kept in the case's cache entry, inside and outside traces alike. Masked where every case reads only its own item, has no effect and gives 1 layout and kind (dynamic kinds: no value-dependent kind); a masked case that sets the error slot (which names the node) clears it and reruns the whole read with that select compacting, which raises only for items that picked the case and records any error the masked case hid; compacting otherwise (effects, reads across items, differing layouts). Under `∂`, a masked case reads each differentiated input it reads, closure included, through the input gate `select(mask, x, stop_gradient(x))` (JAX's double-`where` fix: no zero cotangent times an infinite slope). A mask with fewer axes than its argument (1 Boolean per row) selects by gathering whole rows. Compaction gathers each case's cells, runs the case and scatters the results; it transposes through the gather from several sources and its scatter-add. On a GPU a select is a per-lane branch. Not: admission of cases by spelling; planning on every call; a 2nd planning route outside traces; range narrowing for admission (`Desc.narrows`); compaction as its own executor beside `Step::Select`'s lowering. -15. `∂` (`derivative.rs`, `linear.rs`; JAX `jvp`, `linearize`, `transpose`). `Derivative` level and tags; tangents are linear nodes, arrays of their primal's kind and width under the usual promotion (no dual storage kind; exact primals differentiate exactly, `{⍵×⍵}∂3ₓ` → `6ₓ`; a seed is 1 of its argument's kind); JVPs from rustymath partials through rows; transpose = 1 pass over linear nodes, 1 arm per step kind, keyed on row facts; a Jacobian = the linearized function (part 19's JVP or VJP step, a function value) mapped over unit seeds by the 1 map entry; loops through the loop node's rules. Tangents: linear nodes over a seed leaf; forward mode binds the seed and the scheduler computes tangents with the primal; reverse mode leaves it unbound and transposes. A linear node holds its `∂` call's level (on the node, never in a `Desc` or a key); nothing forces it; the chokepoint refuses reading it; it may hold an older level's value as a constant operand. Kept: tags, derivatives stripped from escaping values, `f∂←g` as a JVP rule that reverse mode transposes, `•detach`, rustymath's partials recorded as primitive calls. A call whose result has a zero tangent (a grade, ranks, positions, a comparison) runs on its arguments' primals: 1 home, in `∂`'s dispatch. A map needs no rules: its calls are batched primitives; a shared input reaches each call spread over the frame (`agreement::spread`), whose transpose sums over the frame. A copy's transpose is `At` with `+` into zeros through the copy's map (JAX's 1 scatter-add); its plan picks the kernel from the run's map (a fold along step 0, a copy through an inverted table, a scatter-add). A read's cotangent: 1 home (`linear::read_back`: a gather where each source item is read at most once, else a scatter-add). A selection transposes by name through the same selection plan as its forward gather (JAX transposes a gather with its dimension numbers). Joins transpose through the gather from several sources. A loop's JVP is a loop over its body's JVP step, whose exit tests read primal values only. Reverse: partial evaluation splits the body by the linear mark; the primal part runs forward and stacks each iteration's residuals beside its carry (only nonlinear steps save inputs); the reverse loop runs the transposed linear body from the last iteration to the 1st, carrying the cotangents of values constant across iterations; a fold's or scan's items get cotangents on each iteration, a history's add in at their iteration; `⍣∞` and `⍣` with a condition stack every iteration (decision 6.3). Choices that depend on lengths (forward mode for 1 number; a Jacobian by rows or by columns, whichever count is smaller) are made at the trace, for its bucket. Complex numbers stay UNSUPPORTED under `∂` until a gradient convention is chosen. Not: an evaluator of its own; reverse loops per item; Jacobians built entry by entry; closures as map operands. -16. Writes (`selection.rs`, `eval/assign.rs`, compiled targets in `eval/binder.rs`, `eval/tree.rs`). 1 walk (`selection::selecting`) composes `g`'s nodes back to the root, through a leaf per item type for nested values; every write is 1 `At` node, inside and outside traces, written in place where the scheduler chooses. Direct paths (`⌷ .[] ⊃` chains) are compositions inside the walk. Key extension (`m.k←v`, a missing key): the walk reaches the container; the `At` writes the container joined with the new item. Positions (`f@[p]`, a mask): a selection by `p`, composed into the walk. A `g` that computes rather than selects writes through `g⁻¹`. A `g` that adds items (`{⍵,0}`) raises "cannot assign to an item that isn't from the original"; a catenation of selections works (`10@{(1↑⍵),¯1↑⍵}`, `((1↑a),¯1↑a)←8 9`). A selection into a segmented array takes 2 kinds of position: an item's entry and a leaf item's offset. At a repeated position, which value a replacing write keeps isn't promised, keyed or not (decisions 6.2, 15); an accumulating write (`y.[dst]+←v`) adds every repeat. Selective assignment checks every position before it changes anything: an error leaves the name unchanged; positions force, values may stay pending; in a trace the `At` node checks its positions when it runs, before it writes. An item whose kind stays on the host is a target taken whole only, found by identity. Not: a direct route beside the walk; labels; write loops per path or per target kind; write executors beside `At`'s (`write_targets`, `pervasive::combine_at`); appends chosen by spelling; host writes beside the `At` node (today `At` only when `fusion::builds`). -17. Effects, draws. Effects and draws are always nodes, inside and outside traces. Every effect is an ordered node (output, `⍕` of a carried value, system effects, time, input `⎕`); effects run in order among themselves, each taking the token of the effect before it, and an effect forces only what it reads (JAX's ordered effects; when other pending work runs isn't promised). Outside a recording every effect runs at its call, after the effects before it; a call whose body holds an effect is itself an ordered effect. An effect that changes bindings or settings, or gives a value read as structure (input `⎕`, `•load`, `⍎`, settings changes), ends a trace's recording; pending nodes keep the settings they were built with (part 4). A draw is a `Draw` node, computed when a read reaches it. Generators' state is a graph input, 1 home; draws use place-based counter streams (rustymath `random.rs`); map nesting comes from the levels. Effect kinds: `Acts` (output, input `⎕`, `⍕` of a carried value, system functions that read or write files, the network, the clock or the terminal, or make something new on each call: `•delay •nput •fetch •nget •time •load •js •uuid`), `Catches` (an error guard, which forces the work built inside it at its exit; a call on prototypes), `Writes` (a write to a binding outside a level's frames). An effect node takes the previous effect's token; a stage ends at each effect node; a training loop that prints its loss stays 1 cached loop that prints on every iteration. A `•rand` generator's state and the session generator's that `¿` draws from are graph inputs: a generator's key joins the recording's key and its count is an input when a replay starts, moved on by the body's number of draws, so every replay draws new numbers; `•prefs` with a seed changes the key. A `Draw` step draws at stream number c + k (c the count, k its place among the body's draws). A batched draw is 1 roll over the whole frame, each cell reading its own stream: a cell's 1st item is word p×w of 1 shared stream (p the cell's place, w the words a cell draws). Not: host draws or effects beside their nodes (`distribution::drawing`, `trace::acting`, `transform::acts`' host run); a session registry forcing every pending value at an effect; draws stopping traces; replays repeating draws; a 2nd home for a generator's count (`Recording::counts`); a draw context beside the levels (`MAPS`); a 2nd seeded generator (`SplitMix64`). +13. Loop node (`Step::Loop`; Dex `for`, JAX `scan`, `while_loop`). `⍣`, dfn folds and scans, and a recorded body's call at count 1 (a map's operand on framed values or `∂`'s operand). A call builds a node whose recording runs when read. Primitive scans such as `+\v` are fold steps. Body: a function value, replayed from its recording in the cache. Runs as an in-item kernel, a one-item kernel, or the host runner replaying the plan. Rules beside the node: batching, JVP, transpose (residuals: a host stack of per-iteration values, its length known when the forward loop ends; the reverse runner replays the VJP step once per iteration; recomputing, `Until::Rerun` today, only as checkpointing past the memory budget), substitution. Each transforms the body as a function value; executable loop nodes follow part 15's forward/reverse split. The body is recorded on its 1st run, as JAX's rules bind a new `scan`/`while` on `batch_jaxpr`/`jvp_jaxpr`/the transposed jaxpr: batching → `f` framed (a nested `Batched` run over the leading frame axes); JVP → `f`'s JVP step; transpose → its VJP step; substitution, the inverse rule → `f⁻¹` (`Applied(Inverse, f)`) at the positive count, for negative counts and `¯∞`. BPL re-runs functions where JAX transforms jaxprs (decision 6.4), so a transformed body is a function value (an operator applied to `f`, part 19: 3 internal operator kinds, `Framed { axes, atoms }`, `JvpStep { packing }` and `VjpStep { direction }`; a kind's parameters are structure only: frame axes and per-argument-leaf atom flags, packing (all carry, noncarry and captured leaves relative to the current derivative level), direction (forward, the primal step giving residuals; backward, the transposed step); values such as a fixed argument and its tangent enter as arguments or captured free values). The next level's rule then takes a transformed body like any body: `∂` of a fold over batched lanes (core 3526), `∂` under a map, nested `∂`. A loop under levels takes the newest level's rule, nothing else. Cell-atom flags preserve Each's atoms and Rank's rank-0 arrays without inspecting a first cell. The internal Once exit has no count operand or exit setting. A framed body begins a nested `Batched` level (part 11). Masked exit (batched predicate, `⍣∞`; JAX batches every carry): every lane steps until all exit; a finished lane keeps its carry; the plan uses masked exit for effect-free bodies under static graph-numeric execution, and under checked dynamic execution only where the body and exit cannot record a data error (part 6). Otherwise it drops finished lanes before running the next step, from the outset. No iteration or whole read is retried. Inputs: the initial carry, noncarry values constant across iterations (`⍣`'s left argument), a fold's or scan's items and captured data from the origin; outputs: the final carry, the stacked states of a history or a scan. Exits: a count; `∞` once |new−old| ≤ t×max(|new|,|old|) at `Numeric::convergence()`'s tolerance; a predicate graph on the new and old states. A count array runs 1 loop per sign up to its largest count, storing the state at each requested count; negative counts and `¯∞` loop over `f⁻¹`'s body. A history is regular when the carry's shape is fixed, segmented when it changes. The in-item kernel takes an elementwise body (pervasive in the carry, other arrays read item by item, no folds, structural steps or effects) with a count or `∞` exit; under `∞` each item stops when it converges. Run preparation applies part 8's compile rule to a loop's total work. With a count the data decides, it compiles the selected version once its iterations so far cost more than compiling (LuaJIT, V8). A body with effects runs only on the host runner, its effects in order on every iteration. A body that can't record (recursion; an inverse that reads values on the host: `⍸⁻¹ ↕⁻¹`, `,⁻¹` along an axis, `⊥⁻¹`) runs as ordinary code on every iteration on the host runner. An exact integer carry under dynamic kinds stays in the loop where its widened range proves a width; otherwise its arithmetic runs on the host every iteration. Checkpointing: past the memory budget, keep every k-th carry, drop residuals, and recompute each segment's residuals from its 1st carry (Futhark's stripmining: about √N carries stored, 1 extra forward run); a count fixed at the start sets k; with a count the data decides, drop residuals when the stack 1st fills the budget, then keep every other stored carry each time it fills. Not: several steppers; a Rust step (`Stepper`, `items_loop`'s `frame` branch) as a body; a frame or linearization point threaded through the runner and its helpers as data; host loops beside the runner; per-caller variants of the loop's JVP or transpose. +14. Selects (`Step::Select`). `⍚`, `f@[p]`, guards on framed values. Case plans from descriptions by 1 planning route (part 10), kept in the case's cache entry, inside and outside traces alike. Masked where every case reads only its own item, has no effect and gives 1 layout and kind (dynamic kinds: no value-dependent kind). Under checked dynamic execution every case must also be unable to record a data error (part 6); otherwise the select compacts from the start. Static graph-numeric cases need no data-error admission check. Effects, host-kind calls, reads across items and differing layouts also compact from the start. There is no error-mask propagation or masked-error retry route. Under `∂`, a masked case reads each differentiated input it reads, closure included, through the input gate `select(mask, x, stop_gradient(x))` (JAX's double-`where` fix: no zero cotangent times an infinite slope). A mask with fewer axes than its argument (1 Boolean per row) selects by gathering whole rows. Compaction gathers each case's cells, runs the case and scatters the results; it transposes through the gather from several sources and its scatter-add. On a GPU a select is a per-lane branch. Not: admission of cases by spelling; planning on every call; a 2nd planning route outside traces; range narrowing for admission (`Desc.narrows`); compaction as its own executor beside `Step::Select`'s lowering. +15. `∂` (`derivative.rs`, `linear.rs`; JAX `jvp`, `linearize`, `transpose`). `Derivative` level and tags. Differentiation and tangents use real-floating kinds and widths under normal float promotion, with no dual storage kind. A seed is 1 of its argument's promoted float kind. At the AD boundary, exact numeric leaves in the differentiated-input structure promote to floats through existing float-width promotion and defaults. Nonnumeric structure and non-differentiated parameters are preserved. JVPs from rustymath partials through rows; transpose = 1 pass over linear nodes, 1 arm per step kind, keyed on row facts; a Jacobian = the linearized function (part 19's JVP or VJP step, a function value) mapped over unit seeds by the 1 map entry; loops through the loop node's rules. Tangents: linear nodes over a seed leaf; forward mode binds the seed and the scheduler computes tangents with the primal; reverse mode leaves it unbound and transposes. A linear node holds its `∂` call's level (on the node, never in a `Desc` or a key); nothing forces it while its seed is unbound; the chokepoint refuses such a read; it may hold an older level's value as a constant operand. Kept: tags, derivatives stripped from escaping values, `f∂←g` as a JVP rule that reverse mode transposes, `•detach`, rustymath's partials recorded as primitive calls. A call whose result has a zero tangent (a grade, ranks, positions, a comparison) runs on its arguments' primals: 1 home, in `∂`'s dispatch. A map needs no rules: its calls are batched primitives; a shared input reaches each call spread over the frame (`agreement::spread`), whose transpose sums over the frame. One read-back mechanism (`linear::read_back`) transposes a numbered-source read plan into cotangents for every source, using D's generic read/scatter kernels: a gather where each source item is read at most once, otherwise accumulation. This serves copies, joins, selections and scalar fallback sources without a fill-specific AD rule. Selection names use the forward read plan. A loop's JVP uses its body's JVP step; exit tests read primals only. Bound forward seeds may use one joint primal-and-tangent loop. Reverse linearization partially evaluates the transformed body relative to the current derivative level before building executable loops: a primal/residual loop and a separate linear loop. The normal runner executes both; the transpose uses the linear loop. Each node has one level mark, with no partial-realization or per-output-demand executor. Packing covers all carry, noncarry and captured leaves. The primal loop stacks residuals beside its carry (only nonlinear steps save inputs); the reverse loop runs the transposed linear body from the last iteration to the 1st, carrying the cotangents of values constant across iterations; a fold's or scan's items get cotangents on each iteration, a history's add in at their iteration; `⍣∞` and `⍣` with a condition stack every iteration (decision 6.3). Length classes decide forward mode for 1 number. A Jacobian chooses rows or columns from the actual dimensions at the call. The selected JVP or VJP function has its own structure-keyed recording (parts 10, 19). Complex numbers stay UNSUPPORTED under `∂` until a gradient convention is chosen. Not: an evaluator of its own; reverse loops per item; Jacobians built entry by entry; closures as map operands. +16. Writes (`selection.rs`, `eval/assign.rs`, compiled targets in `eval/binder.rs`, `eval/tree.rs`). 1 walk (`selection::selecting`) composes `g`'s nodes back to the root, through a leaf per item type for nested values; every write is 1 `At` node, inside and outside traces, written in place where the scheduler chooses. Direct paths (`⌷ .[] ⊃` chains) are compositions inside the walk. Key extension (`m.k←v`, a missing key): the walk reaches the container; the `At` writes the container joined with the new item. Positions (`f@[p]`, a mask): a selection by `p`, composed into the walk. A `g` that computes rather than selects writes through `g⁻¹`. A `g` that adds items (`{⍵,0}`) raises "cannot assign to an item that isn't from the original"; a catenation of selections works (`10@{(1↑⍵),¯1↑⍵}`, `((1↑a),¯1↑a)←8 9`). A selection into a segmented array takes 2 kinds of position: an item's entry and a leaf item's offset. At a repeated position, which value a replacing write keeps isn't promised, keyed or not (decisions 6.2, 15); an accumulating write (`y.[dst]+←v`) adds every repeat. Checked selective assignment validates every position before changing anything; an error leaves the name unchanged. Static graph-numeric writes skip invalid numeric positions instead. Positions force, values may stay pending; a trace checks positions when its `At` runs. Structural and host-kind errors remain checked (Measures). An item whose kind stays on the host is a target taken whole only, found by identity. Not: a direct route beside the walk; labels; write loops per path or per target kind; write executors beside `At`'s (`write_targets`, `pervasive::combine_at`); appends chosen by spelling; host writes beside the `At` node (today `At` only when `fusion::builds`). +17. Effects, draws. Effects and draws are always nodes, inside and outside traces. Every effect is an ordered node (output, system effects, time, input `⎕`); effects run in order among themselves, each taking the token of the effect before it, and an effect forces only what it reads (JAX's ordered effects; when other pending work runs isn't promised). Outside a recording every effect runs at its call, after the effects before it; a call whose body holds an effect is itself an ordered effect. An effect that changes bindings or settings, or gives a value read as structure (input `⎕`, `•load`, `⍎`, settings changes), ends a trace's recording; pending nodes keep the settings they were built with (part 4). A draw is a `Draw` node, computed when a read reaches it. Generators' state is a graph input, 1 home; draws use place-based counter streams (rustymath `random.rs`); map nesting comes from the levels. `⍕` is pure: a carried argument goes to its level before a step. `∂` formats primals (a zero-tangent call); batching stops and formats cell by cell. Effect kinds: `Acts` (output, input `⎕`, system functions that read or write files, the network, the clock or the terminal, or make something new on each call: `•delay •nput •fetch •nget •time •load •js •uuid`), `Catches` (an error guard, which forces the work built inside it at its exit; a call on prototypes), `Writes` (a write to a binding outside a level's frames). An effect node takes the previous effect's token; a stage ends at each effect node; a training loop that prints its loss stays 1 cached loop that prints on every iteration. A `•rand` generator's state and the session generator's that `¿` draws from are graph inputs: a generator's key joins the recording's key and its count is an input when a replay starts, moved on by the body's number of draws, so every replay draws new numbers; `•prefs` with a seed changes the key. A `Draw` step draws at stream number c + k (c the count, k its place among the body's draws). A batched draw is 1 roll over the whole frame, each cell reading its own stream: a cell's 1st item is word p×w of 1 shared stream (p the cell's place, w the words a cell draws). Not: host draws or effects beside their nodes (`distribution::drawing`, `trace::acting`, `transform::acts`' host run); a session registry forcing every pending value at an effect; draws stopping traces; replays repeating draws; a 2nd home for a generator's count (`Recording::counts`); a draw context beside the levels (`MAPS`); a 2nd seeded generator (`SplitMix64`). 18. Binder, executor (`eval/binder.rs`, `eval/tree.rs`). The binder compiles each statement before it runs, emitting instructions only; `tree.rs` is the 1 statement evaluator and holds the value constructors. The compiler may build values from constants through those constructors (constant folding: `⍤1` derived once): construction, not evaluation. A role change the compiler can't see is an error (decision 10), never a restart. Binding rules: (1) a call's result is a value to the binder, and so is a field read from it; a subscript's selection counts as a call: `+/÷(f 0)` is an expression; `g←f 0 ⋄ +/÷g` names a returned function; `1⍚fs x` calls a selected one. (2) A role change the compiler can't see is an error: a write that changes the role of a name outside the writing function's frame (raised at the write), and a read, later in a statement, of a name whose role that statement's write changed (raised at the read); an array binds as a function when any item is a function; `Session::assign`, the 1 home of assignment, compares old and new categories. (3) `⍎` changing a name's role while a statement that reads it runs is an error (decision 9); an unbound name binds as an array (`x+⍎"x←3"` works). An earlier assignment in the statement (`x+(x←3)`) gives the name the assigned expression's role for the rest of it. Roles are read again at each statement's start. The compiler binds ahead: dot access on a name (the field's role from `T`'s value, a recorded fact); a scope `T.(expr)` (from `T`'s keys and field roles; a computed `T` compiles its body on entry, as `⍎`'s text does); superscripts; numeric subscripts by their operand's role (`(⊃fs)₁` selects); an absent `⍺` (compiled per valence, or checked at the statement's start). A recording checks the roles of the names it reads at the start of the statement and of each group or run. A top-level statement compiles, then runs once. A call that returns no result fails when it runs. Not: evaluation inside the binder; resume machinery (`Again`); a 2nd node evaluator (`Session::resolve`); a 2nd reader of the grammar (`Pattern::read`, `dot_path`). -19. Function values (`eval.rs` `FunctionNode`; tinygrad `UOp(op, src, arg)`, JAX `JaxprEqn(primitive, params, invars)`). Leaves: primitive, system function, dfn, late-bound name, function array. Every operator application is 1 variant: its operator (an `OperatorKind`, or a dfn operator's closure) and 1 or 2 operands; `/ ⌿ \ ⍀`, `⍠`, `⁻¹`, `ᵇ` and marks are operator kinds in it. Internal kinds, for part 13's loop rules: framed (a nested `Batched` run over leading frame axes), JVP step, VJP step; their parameters are structure only (frame axes, packing, direction); values (a fixed argument, its tangent) enter as arguments or captured free values, never as key parts. A train: 1 variant, its tines. Attached rules (`f⁻¹←g`, `f∂←g`): a field of the function's data beside `environment`, as JAX's `custom_jvp` holds its rule beside its function; display, parts and calls see `f`. Operand traversal (`visit_operands`, `rebuilt`, `equal_by`, owns, environment) written once over operands. Per-kind behaviour (call, spelling, inverse, `discards`, Python name): the operator kind's entry, keyed on the kind as `Primitive::info()` keys rows. A new operator or wrapper kind = 1 kind entry, 0 node arms. Attached rules' readers: `⁻¹`, Under (`discards`, inverse), `∂`; rules aren't operands (no traversal) and count for `environment`. No needs mask: rules match on nodes (part 7). Train tines: functions or arrays (an array tine is a constant, a data input in programs). A dot read on a function (`f.name`, `f.[I]`): an operator kind, its field or index the parameter. Operator values: a primitive operator (`OperatorKind`), a dfn operator (closure), a bound operator (its right operand bound); `f₁` is a bound `⍠`, no variant of its own. Not: a variant per wrapper kind (today `Fold`, `Axis`, `Inverse`, promoted by `Function::new` out of `Modified`/`Composed`, which already hold the kind); `Modified`, `Composed` and `Derived` as 3 application variants; attached rules as a node (`Ruled`); host closures as function values; a 2nd call route for trains (tines called in place). -20. Index notation and positions (`⍳`, `•index`, rustymath selection planning). Level 2: regular selectors as strides, in rustymath's selection planning; level 3: contractions as 1 rule (part 7). Positions count from 0, negative positions from the end; no selector fills outside its axis: a position past an axis's length or a missing key raises INDEX ERROR, and only a NaN position reads the fill (keyed agreement's fill: part 2). `.[]` takes no `⍠`; a fill comes through `⌷⍠["fill":v]`. Drawn names: part 10's key; a map whose body returns one: part 12. Axis order of `g×z`: where every axis of the left operand pairs and the right has more axes, the right's order; otherwise the left's axes first. Not: range inference (`•index ∞`, decision 11); strides decided from 1 run's index values inside a recording. -21. Scripts and marks (`number.rs` `MARKS`, `syntax.rs`). Marks, 1 table (`MARKS`): `ₓ` exact, `ₓ₁ ₓ₂ ₓ₄ ₓ₈` fixed widths, `ₕ ₛ ₚ` floats of 16, 32 and 64 bits, `ₐ` real; after a function each is an operator kind (part 19), after a value a conversion node (`Mark::conversion`). A mark after a function converts each result as it's stored, in its producer's stage: `⌊ₓ₁`, `-ₓ₁`, `÷ₛ` and `⍳ₓ₁` take no 2nd pass. A marked literal group, and a superscript after a literal number (`3²`), is constant folding through the same conversion or call (part 18), never computed by the lexer or parser (today `syntax.rs`'s `mark_literals` and `strands`, under `Numeric::current()`). `ₐ`: real floats at the arguments' float width, never chosen from values (a fixed integer takes its type's float promotion: `ₓ₁ ₓ₂` float32, `ₓ₄ ₓ₈` float64; a weak literal takes its partner's; arguments with no compact join take the default width); outside the real domain the real part of the principal value (`√ₐ¯4` → `0`, `⍟ₐ¯1` → `0`, `¯8*ₐ÷3` → `1`); `fₐ` ≡ `(f args)ₐ`, a static kind under both kinds. `fₐ` builds `(f args)ₐ`, a conversion node over `f`'s node; mark rows (part 7) rewrite such a pair on nodes: `√ * ⍟ ○` on reals to specials backed by rustymath objects (`RealSqrt`, `RealLn`, `RealPow`, `RealCircle`'s codes, whose table of codes is a rustymath object, not basedpl's `with_circle!` and `with_real_circle!`), each with a real form, a complex form giving the real part, a formula (`Facts::formula`), a range rule and the real part's slopes (`RealLn` 1/x; `RealPow` below 0 those of |x|^y cos πy; `RealSqrt` and codes whose real part is constant there, 0 outside the domain), with JAX's double `where` where a case's slope is infinite; `⌊ ⌈ ÷` with `ₓ` to exact forms; any other `fₐ` converts `f`'s result. Speculation stays (decision 3); `fₐ` is the spelling guaranteed fast. Superscript digits after a function: Rank, in a separate build (`open.md`, "Agreed, left"); this release keeps today's meanings (a digit after a function repeats it, `⁻¹` inverts). Letter superscripts: `ᵀ` after an array transposes it, `ᵘ` after a function makes a scalar holding it; either on the other role is an error. +19. Function values (`eval.rs` `FunctionNode`; tinygrad `UOp(op, src, arg)`, JAX `JaxprEqn(primitive, params, invars)`). Leaves: primitive, system function, dfn, late-bound name, function array. Every operator application is 1 variant: its operator (an `OperatorKind`, or a dfn operator's closure) and 1 or 2 operands; `/ ⌿ \ ⍀`, `⍠`, `⁻¹`, `ᵇ` and marks are operator kinds in it. Internal kinds, for part 13's loop rules: framed (`Framed { axes, atoms }`, a nested `Batched` run over leading frame axes), JVP step (`JvpStep { packing }`), VJP step (`VjpStep { direction }`); their parameters are structure only (frame axes, cell-atom flags, packing, direction); values (a fixed argument, its tangent) enter as ordered data inputs, never embedded in the operator's structural parameters; structural reads key them only by part 10's `ReadKind`. A train: 1 variant, its tines. Attached rules (`f⁻¹←g`, `f∂←g`): a field of the function's data beside `environment`, as JAX's `custom_jvp` holds its rule beside its function; display, parts and calls see `f`. Operand traversal (`visit_operands`, `rebuilt`, `equal_by`, owns, environment) is written once. One structure-only template/rebuild represents functions made in recorded bodies or rules, with positional dependencies for all data operands. Cached `Program`/`Plan` store these templates and input positions, never a live closure, current environment or data producer; the original callable binds from current inputs. Per-kind behaviour (call, spelling, inverse, `discards`, Python name): the operator kind's entry, keyed on the kind as `Primitive::info()` keys rows. A new operator or wrapper kind = 1 kind entry, 0 node arms. Attached rules' readers: `⁻¹`, Under (`discards`, inverse), `∂`; rules aren't operands (no traversal) and count for `environment`. No needs mask: rules match on nodes (part 7). Train tines: functions or arrays (an array tine is a constant, a data input in programs). A dot read on a function (`f.name`, `f.[I]`): an operator kind, its field or index the parameter. Operator values: a primitive operator (`OperatorKind`), a dfn operator (closure), a bound operator (its right operand bound); `f₁` is a bound `⍠`, no variant of its own. Not: a variant per wrapper kind (today `Fold`, `Axis`, `Inverse`, promoted by `Function::new` out of `Modified`/`Composed`, which already hold the kind); `Modified`, `Composed` and `Derived` as 3 application variants; attached rules as a node (`Ruled`); host closures as function values; a 2nd call route for trains (tines called in place). +20. Index notation and positions (`⍳`, `•index`, rustymath selection planning). Level 2: regular selectors as strides, in rustymath's selection planning; level 3: contractions as 1 rule (part 7). Positions count from 0, negative positions from the end; under checked dynamic execution a numeric position outside its axis raises INDEX ERROR; under static graph-numeric execution an invalid numeric position reads the fill, including from an empty axis (Measures). A NaN position reads the fill in either mode. Missing keys and other structural errors resolved at the boundary remain checked (keyed agreement's fill: part 2). `.[]` takes no `⍠`; a fill comes through `⌷⍠["fill":v]`. Drawn names: part 10's key; a map whose body returns one: part 12. Axis order of `g×z`: where every axis of the left operand pairs and the right has more axes, the right's order; otherwise the left's axes first. Not: range inference (`•index ∞`, decision 11); strides decided from 1 run's index values inside a recording. +21. Scripts and marks (`number.rs` `MARKS`, `syntax.rs`). Marks, 1 table (`MARKS`): `ₓ` exact, `ₓ₁ ₓ₂ ₓ₄ ₓ₈` fixed widths, `ₕ ₛ ₚ` floats of 16, 32 and 64 bits, `ₐ` real; after a function each is an operator kind (part 19), after a value a conversion node (`Mark::conversion`). A function mark converts each result as it is stored in its producer's stage: `⌊ₓ₁`, `-ₓ₁`, `÷ₛ` and `⍳ₓ₁` take no 2nd pass. A marked literal group, or superscript on a literal number (`3²`), folds through the same conversion or call (part 18), never in the lexer or parser (`syntax.rs`'s `mark_literals` and `strands`, under `Numeric::current()`). `ₐ` gives real floats at the arguments' float width, never a width chosen from values: `ₓ₁ ₓ₂` promote to float32, `ₓ₄ ₓ₈` to float64; a weak literal takes its partner's width; no compact join takes the default width. Outside the real domain it gives the principal value's real part (`√ₐ¯4` → `0`, `⍟ₐ¯1` → `0`, `¯8*ₐ÷3` → `1`). Result semantics: `fₐ` ≡ `(f args)ₐ`. The marked-form kind entry resolves the primitive forms `√ₐ *ₐ ⍟ₐ ○ₐ` and inverse-circle real forms to rustymath's real operations before the unmarked producer's kind rule, operand reads or level handling (part 5). These use `RealSqrt`, `RealLn`, `RealPow` and `RealCircle`'s code table, not basedpl's `with_circle!` or `with_real_circle!`. Each object holds its real form, the real part of its complex form, formula (`Facts::formula`), range rule and real-part slopes: `RealLn` has 1/x; `RealPow` below 0 differentiates |x|^y cos πy; `RealSqrt` and circle codes whose real part is constant outside the real domain have slope 0 there. Infinite slopes use JAX's double `where`. The same early resolution handles exact `⌊ₓ ⌈ₓ ÷ₓ`. Resolved operations use normal dispatch and the existing objects and derivative facts; their origins keep the marked call. Other marked functions convert their result. These mandatory semantics are not late speed rewrites. No kind is speculated (part 2); the listed primitive real forms and `÷ₓ` are guaranteed fast. Superscript digits after a function become Rank in a separate build; this release keeps repetition and `⁻¹` inverse. Letter superscripts: `ᵀ` transposes an array; `ᵘ` makes a scalar holding a function; either on the other role is an error. 22. Reports (`explain.rs`, rustymath `events`). `•explain` reports every graph, with no size filter; users filter by `items`. Records: `fused` (stages run); `stored` (nodes stored partway, with a reason); `forced` (pending work that a read forces partway, with a reason: an observing read, a host kind, an effect's operand, a selective assignment's checks); `programs` (every recording: loop bodies, maps' operands, `∂`'s operands; events include "retraced" and "rerun", as JAX's cache-miss explanations and Dynamo's recompile logs report); `stopped` (a row per reason with calls, items and `site`, as Dynamo's `break_reasons`); `fast` (each rule applied); `fallbacks` (every run per item, cell or iteration on the host, with reasons; today's `itemwise` and `loops` records fold into it); `declined`, `accumulations`, `mixed`, `compiles`, `routes`. A reader that takes its argument whole (sorts, grades, searches, `⌹`, products) is classified by `Facts::reads`, 1 home. Route checks and the coverage matrix keep 3 requirements: a mapping stage compiles (`compiled`), a fold reads its argument inside its stage (`fused fold`), a gradient replays (`trace`). `⍝⍝⍝ route` names only these 3; every other route is checked by the absence of slow events (`fallbacks`, `stored`, `forced`, `stopped`), which fail unless allowed. Routes are rustymath's `events`, their 1 home, which basedpl's `•explain` and rustymath's Python `rm.explain()` both capture; the route list holds only routes the target keeps; `stored` reports every value a stage end stores. Reports and diagnostic tools change without agreement. -23. Later stages' requirements on the core (not built in this release). GPU: running a graph separate from reading its result; nodes as plain data; transposes that read rather than scatter where they can; readbacks for results whose shape depends on values; bf16 as a kind (spelling open); WGSL has `bool i32 u32 f32` and `f16`, no f64, i64, complex or bf16 (those stay on the CPU or are emulated: the GPU stage's choice); data errors gathered into 1 flag read once at the end (the session slot's device form; XLA's error bits); allocation at a bucket's bound, so a captured graph's buffers stay fixed; 1 captured graph per key; user bounds for lengths the data decides (JAX's `size=`); kernels that read counts from device memory. Array files: leaves backed by buffers the allocator doesn't own: rustymath `Buffer`'s owner `Owned(Vec)` or `External(Arc, offset, len)`, read through `&[T]`, copied on its 1st write; a region says whether the host can read it; built with array files. External backend (mo): the graph as plain data that a backend writes out. +23. Later stages' requirements on the core (not built in this release). GPU: running a graph separate from reading its result; nodes as plain data; transposes that read rather than scatter where they can; readbacks for results whose shape depends on values; bf16 as a kind (spelling open); WGSL has `bool i32 u32 f32` and `f16`, no f64, i64, complex or bf16 (those stay on the CPU or are emulated: the GPU stage's choice); checked data errors gathered into 1 flag read once at the end (the session slot's device form; XLA's error bits); buffer capacity independent of plan keys, with captured graphs reused while their buffer addresses remain valid and recaptured when required by a capacity change; user bounds for lengths the data decides (JAX's `size=`); kernels that read counts from device memory. Array files: leaves backed by buffers the allocator doesn't own: rustymath `Buffer`'s owner `Owned(Vec)` or `External(Arc, offset, len)`, read through `&[T]`, copied on its 1st write; a region says whether the host can read it; built with array files. External backend (mo): the graph as plain data that a backend writes out. ## Interfaces -Pinned before the build phase; a unit changing one tells main first. Names are targets, not the tree's. +These are target interfaces, not promises to preserve today's names. Pin declarations for the next coherent chunk under the build process above. -- `Desc`: kind, atom flag, layout, range. `Step::describe(&self, operands: &[&Desc]) -> Result`: its only producer. -- `Step`: part 4's variants, each with `describe`, `run::step`'s arm, `glyph`, `operands!`, and its origin (primitive, parameters). `run::step(&Step, operands) -> Value`; a data error sets the session's error slot. -- Row plan: `CallPlan { roles: per operand Data | Values (read at the call: a parameter, values deciding a length; a kind that depends on values is speculated, part 2), step: Step (Call where the row has none), len, bytes }`. 1 builder for every call, `fusion::built(step, operands) -> Value`, its `Desc` from `describe`; the node waits. No force policy: a read forces (part 8). -- Dispatch: `Operation::call(op, args) -> Result`, the only caller of level rules and of the builder. A level rule: `(level, op, args) -> Result`, calling dispatch for its parts. +- `Desc`: logical `number::Kind`, atom flag, layout, range, segments. The origin's row-plan builder describes each output from operand descriptions and parameter values before choosing its step; `Desc::leaf` describes inputs. Result assembly and descriptions use the one stored-result joining policy (part 2). One projection, `Desc::of(&Value)` or `Value::desc`, returns a borrowed-or-owned (`Cow`) description: pending borrows its node description, Batch borrows its stored cell, Dual delegates to its primal, and a stored leaf uses `Desc::leaf`. Both call-binding implementations use that projection. No first-cell or whole-shape inference. +- `Outputs` holds flat leaves and their optional `Trees`. `Description` is `Outputs`. Mapping leaves preserves the trees; a language result assembles once from `Outputs`. Programs retain an effect-token root separately from public data roots. +- `Step`: part 4's variants, each with `run::step`'s arm, `glyph` and `Step::maps`. The origin owns the ordered operand snapshots, trees and refill facts. Copy/Join bind scalar fills and other numbered sources through the shared read plan (part 4). `run::step(step, operation, reads, values) -> Outputs`; a checked error records in the session's slot. +- Row plan: `Operation::roles` gives `Roles` (data, or a call-time read with its `ReadKind`) before any read. Describe, `Operation::plan` and `Step::maps` borrow one call view derived from the origin, bound functions, parameter values and descriptions. Describe precedes step choice; no owned `CallPlan`, duplicate `Role` or duplicate resolved call. One `Origin::call_view(bindings)` projects metadata; `reborrow` uses `with_list` rather than copying the fields again. `fusion::built` is the live-node builder. A read forces; there is no force policy. +- Dispatch: `Operation::dispatch(Origin) -> Result>` is the entry and caller of level rules and the builder. The value convenience entry `call(args, literals) -> Result` constructs the origin and delegates to dispatch. Semantic forms resolve first (part 5). A level rule receives the complete origin and returns `Result>` through dispatch. - Levels: `Level { id, kind: Derivative | Batched }`; `Value::Carried { level: id, .. }`; the innermost running batched level; a level found by its id. -- Graph to runs: `Program::of(roots) -> Program` (the 1 conversion); `schedule(&Program) -> Plan`; `Plan::run(inputs) -> outputs`. Forcing, a trace's end and replay call these alone. A read first looks up `Program::key()` (structure, literals by value, other leaves as descriptions: a length class and bucket per axis, and each length the data decides inside the graph by its class and bucket) in the 1 store, checks the plan's speculation boxes, and schedules only on a miss. -- Rules: `Rule { key: RootKey { kind: StepKind, root: Option }, bind: fn(&[Node], &Step) -> Option, build: fn(&mut Vec, Bound, &Desc, &Shared) -> Option }`; the table indexed by `RootKey` once, run by the pipeline alone (a read of a lone node included). -- Function values: `FunctionNode`: `Primitive | System | Defined | LateBound | Applied { op: Operator, operands } | Train(tines) | Array`; `Operator`: `Kind(OperatorKind)` | a dfn operator's closure; `FunctionData { node, environment, rules: Option }`; `OperatorKind::info()`: call, spelling, inverse, `discards`, Python name. Internal kinds: `Framed { axes }`, `JvpStep`, `VjpStep`. -- Cache: `Programs::serve(&Function, Key) -> Outcome` (a recording, a stop, a select plan) on 1 store keyed by function structure (a dfn by its `Definition` through a weak reference, never a closure instance; entries no function owns evicted least recently used past 256), 1 entry per function value; a forced graph's plan sits in the same store under `Program::key()`. `Key`: argument descriptions (a length class and bucket per axis, part 10), numeric settings, free values read as structure, speculation boxes (kinds; leaf range facts); a length the data decides extends the key from its node (part 10). -- Refill: `Plan::refill(from: usize, &[Desc])` gives a plan, from step `from` on (0 at entry; the step after a length the data decides, partway through a run), its descriptions and maps at a run's concrete lengths, from each step's origin through the row-plan builders; a replay and a read of a cached graph both call it, only where lengths differ from its last refill. -- Loop: `Step::Loop { body: Function, until, carries, data }`; rules `batched`, `jvp`, `transposed`, `substituted`, each giving a `Step::Loop` whose body is `Applied(kind, f)`. -- Errors: 1 slot per session (`Session::error`), holding the 1st error and the node that set it, set by kernels, executors and forcing; `Session::raise() -> Result<()>` at the checkpoints; a run that sets it replaces no node downstream of the failing one; a failed kernel writes in-range values; clearing a masked case's error reruns the whole read. -- Effects: `Step::Effect` takes the token of the effect before it and its own operands, and outside a recording runs at its call; no session registry; a step that reads a numeric setting carries it as a parameter. -- Writes: `selection::selecting(target, root) -> Walk`; a walk writes through 1 `At` node. +- Graph to runs: `Live::of(roots)` gathers the key, inputs and live nodes on every read; `Program::of` converts them on a miss. `schedule(&Program, &Running, from, runs) -> Plan` reads the same input/template bindings and computed prefix state that execution uses; `Plan::run` uses that `Running` state. Rest scheduling starts at its node without reconstructing initial-only bindings. Forcing, trace completion and replay's remaining work use this pipeline. The plan caches rewrites, order, stage/fusion structure and kernel recipes. Hits validate assumptions and refill where needed. Before execution, run preparation makes part 8's cheap actual-count choices. Input keys follow part 10. +- Rules: `Rule` has `RootKey { kind: StepKind, root: Option }`, one match giving `Bound` and one build through the shared program builder. That builder registers nodes, literal inputs and structure-only function templates with positional dependencies through the same bindings, using the row's borrowed call view for step/maps. The table is indexed once by `RootKey` and run by the pipeline alone, including a lone-node read. +- Function values: `FunctionNode`: `Primitive | System | Defined | LateBound | Applied { op: Operator, operands } | Train(tines) | Array`; `Operator`: `Kind(OperatorKind)` | a dfn operator's closure; `FunctionData { node, environment, rules: Option }`; `OperatorKind::info()`: call, spelling, inverse, `discards`, Python name. Internal kinds: `Framed { axes, atoms }`, `JvpStep { packing }`, `VjpStep { direction }`. The one generic binding recipe lives in `eval.rs` and is reused by `Program`. Its canonical `FunctionOperand::{Value { value, literal }, Input { position, literal }}` distinguishes real values from input positions and retains literal provenance without fake placeholder values. One `OperandKind` rule classifies structural operands, ranks included, versus data. One `Function::captured()` order covers every free datum (stored, host, pending or carried), bound data operands and array tines; capture traversal, environment binding and rebuilding use it. Cached templates contain structure and positions, never raw data, live closures or producers. Ordinary calls resolve existing Origin/frame bindings without rebuilding frozen functions; actual function results rebuild from the run's values. There is no separate template IR, registry, `FunctionView` or replay-time `free_value` lookup (parts 10, 19). +- Cache: `Programs::serve(base, current_origin)` checks discovered-read guards in the one function-structure store. One cached `Recording` holds the program, logical output description, effects, itemwise/error properties and discovered reads for ordinary invocation, case planning and loops. A dfn keys by a weak reference to its `Definition`, never a closure instance. A forced graph uses its structure key; unowned entries are evicted least recently used past 256. `Key` uses per-axis 0/1/more length classes, axis equalities and the other description, structural and parameter facts in part 10. All call-time captured-data inputs follow part 10; there is no stored/host free-facts comparison or late session reread. Each plan keeps the leaf range boxes it assumes. +- Refill: `Plan::refill(from, running)` derives descriptions and maps through origins and their row-plan builders, starting at 0 or after a data-determined length. The same bound inputs and prefix values supply parameters and callable bindings through the borrowed call view. A replay and a cached graph read use it when lengths or count parameters changed. It never gathers, rewrites, orders or places stage ends. Refill validates the plan's kind, range and structural assumptions before execution under the existing guard/miss policy. Run preparation then selects part 8's count-dependent choices, including on-demand compilation. +- Loop: `Step::Loop` has a bound body/template, exit, carry and noncarry/captured input positions; rules `batched`, `jvp`, `transposed`, `substituted` (the inverse rule: a negative count or `¯∞` loops over `f⁻¹`, `Applied(Inverse, f)`, at the positive count): `batched`, `jvp` and `substituted` return the rebuilt `Origin>` for the loop whose body is `Applied(kind, f)`; reverse linearization splits before building executable loops (part 15); `transposed` builds the normal reverse loop over the separate linear body. Bound forward JVPs may keep joint outputs. Packing covers carry, noncarry and captured leaves relative to the current level. Every transformed call preserves mode, literal marks, argument trees, explicit-boundary metadata, settings and capture bindings through the origin projection; no helper returns a bare operation/list or reloads session preferences. +- Errors: rustymath owns the first-error slot and count held by `Session::error`; `Session::raise() -> Result<()>` raises at checkpoints. Checking follows the call's origin (Measures). Runners compare counts around steps; no error restarts a read. Static graph-numeric steps remain memory-safe without recording data errors. +- Effects: `Step::Effect` takes the previous effect's token and its operands. Outside a recording it runs at its call. No session registry. Settings belong to origins (part 4). +- Writes: `selection::selecting(target, root) -> Result>`: a walk writes through one `At` node; a computing target uses its inverse; an invalid selection raises. + +## Numerical algorithms + +General positive floating Pow/Root composes the shared Exp/Ln formulas: `exp(y*ln(x))` and `exp(ln(x)/n)`. Preserve cheap constant/integer powers, sqrt/Cbrt, exact arithmetic and zero/negative/parity/domain semantics. Remove general Pow's table/compensation machinery. Characterize float errors and document function-specific exceptions without weakening common-use results or exact arithmetic. Accuracy characterization uses `u*|y ln(x)|` (unit roundoff u) for Pow, not the exponent alone; f32 and f64 need their own bounds. + +Trig keeps short fast reduction and polynomials. Rare out-of-range lanes use the object's scalar platform/native sin/cos/tan repair instead of porting custom Payne–Hanek u128/table arithmetic into Ops. The predicate and its limits have one home (part 6); range-proof elimination belongs to part 2, compiled repair ordering to part 9. Native/wasm provider and ABI selection must preserve the semantic and speed requirements. GPU execution needs device code or portable lowering, not necessarily another mathematical implementation. + +Reuse rustymath's existing optimized elementwise, reduction and contraction drivers through shared interfaces co-designed with BPL. Keep repair and error reporting outside their common hot loops. Do not add new SIMD code or a separate vector execution engine in the foundation-writing and integration stages. Once the foundation works, measure before changing performance code. Preserve non-FMA f32 trig's f64 intermediates through the existing width operations. A retained algorithm that needs table lookup can use the existing scalar loads or platform operations. + +Keep Cbrt's compact fixed algorithm: two f32 or three f64 Newton steps and correction. Incomplete Gamma/Beta convergence computes the answer and stays in canonical object-backed Calls where appropriate. Ordinary Gamma/Beta do not require those convergence loops. Substantial algorithms need neither a general numerical-loop IR nor a blanket vector-call ABI. Host exact arithmetic and native f16/complex retain the coverage in parts 1, 6 and 9; block admission does not decide correctness coverage. + +Shared formulas and object-backed Calls must each have one mathematical implementation. An algorithm's call/staging and lost-fusion costs belong to the normal speed budgets, not an assumed fixed one- or two-copy charge. Preserve the documented finite-result bounds until characterized exceptions are documented. Performance and simplicity outrank accuracy machinery beyond those requirements. ## Cost model -What each stage of the pipeline ought to cost, counted from the work it must do and set beside the role models. Never from today's tree: a stage over its budget is a gap in that stage's code. Units: instructions retired (`instructions`, Apple silicon) per call, node, step or run; work per item in copies (DEV.md). +What each stage of the pipeline ought to cost, counted from the work it must do and set beside the role models. Never from today's tree: a stage over its budget is a gap in that stage's code. Estimates cover a single-output call with at most 2 operands; cost variable operands, output descriptions and argument trees at their actual sizes. Units: instructions retired (`instructions`, Apple silicon) per call, node, step or run; work per item in copies (DEV.md). ### Primitive operations @@ -134,30 +162,30 @@ Assumed, Rust on Apple silicon, small sizes: | Stage | Work it must do | Budget | |---|---|---| -| dispatch, 1 call | innermost level among ≤2 args (10); row by index (5); roles (10); kind, shape, range rules (100); length, bytes (10); node allocated and filled (70); interned: hash, probe (60) | 300 | -| pending node | memory only: about 150 B plus operand references; its leaves stay alive, no intermediate is stored | 0 | -| read of a pending graph of N nodes, plan cached | structure key: each node's step and operand descriptions hashed (30 a node); probe (40); per step: executor call (15), output allocation (40), kernel setup (50), work; each intermediate freed at its last use (30); `Program::of`'s walk (20 a node) and allocation (50); leaves bound to the plan's inputs (20 a leaf); the intern table updated as each node is replaced by its result or dies (30 a node) | 150 + 235·N + work | -| plan, a cache miss | gather (30 a node); rewrite: root-key probe (20 a node), match and build (200 a rule applied); order (20 a node); stage ends (20 a node); descriptions and maps (130 a node); compile decision (20 a stage); plan allocated (50 a node) | 270·N + 200·rules | -| refill | descriptions and maps at the run's lengths through the plan's own builders (130 a node); no gather, rewrite, order, stage ends or compile decision | 130·N, only where lengths differ from the plan's last refill | -| trace | the run's own dispatch and reads, plus free values captured (20 each) and the key (30 an argument) | dispatch + 50 a call | -| replay | key: argument descriptions hashed (30 each), free values compared (20 each), boxes checked; probe (40); refill where lengths changed; run as a read (135 a step + work) | 150 + 135·N + refill + work | -| compile (JIT) | only where its formula repays (part 9); code owned by `jit::KERNELS`, keyed by kernel structure, never by length or bucket | per formula | +| dispatch, 1 call | innermost level among ≤2 args (10); row by index (5); roles (10); captured input binding (20 a captured leaf); kind, shape, range and validity rules (100); node allocated and filled (70); interned: hash, probe (60) | 300 + 20·captured leaves | +| pending node | node and origin: about 150 B for a single-output call, plus variable operand references, output descriptions and argument trees; leaves stay alive, no intermediate is stored | 0 | +| read of a pending graph of N nodes, plan cached | structure key: each node's step and operand descriptions hashed (30 a node); probe (40); per step: executor call (15), output allocation (40), kernel setup including cheap actual-count choices (50), work; each intermediate freed at its last use (30); `Live::of`'s walk (20 a node) and allocation (50); leaves bound to the plan's inputs (20 a leaf); the intern table updated as each node is replaced by its result or dies (30 a node) | 150 + 235·N + work | +| plan, a cache miss | `Program::of` and gather (30 a node); rewrite: root-key probe (20 a node), match and build (200 a rule applied); order (20 a node); stage ends (20 a node); descriptions and maps (130 a node); plan allocated (50 a node) | 270·N + 200·rules | +| refill | descriptions and maps at the run's lengths through the plan's own builders (130 a node); no gather, rewrite, order, stage ends or compile decision | 130·N, only where lengths or count parameters differ from the plan's last refill | +| trace | the run's own dispatch and reads, plus input roles and the key (30 an input); capture binding was paid at dispatch | dispatch + 50 a call | +| replay | key: all input descriptions hashed (30 each), inputs bound (20 each), boxes checked; probe (40); refill where lengths or count parameters changed; run as a read (135 a step + work) | 150 + 135·N + refill + work | +| compile (JIT) | only the selected useful version where its formula repays (part 9); code owned by `jit::KERNELS`, keyed by emitted structure, types and modes, never by counts | per formula | | tree op, dfn call | op 30; name load 10; dfn frame 60 + 20 an argument | — | -Role models for the shape of these costs: JAX's eager mode runs each primitive as a 1-op compiled program cached by primitive and abstract signature (`apply_primitive`); `jit` checks a cache keyed by the function and its arguments' signatures, so a hit costs a key check plus the program. tinygrad builds every op as a lazy `UOp`, caches compiled kernels by their AST, and TinyJit replays the kernels a real run captured. BPL's budgets sit below both: no Python, no device launch before the GPU stage. +Base budgets assume at most two input leaves and no additional captured data; extra leaves incur the binding/hash work listed in each row. Role models for the shape of these costs: JAX's eager mode runs each primitive as a 1-op compiled program cached by primitive and abstract signature (`apply_primitive`); `jit` checks a cache keyed by the function and its arguments' signatures, so a hit costs a key check plus the program. tinygrad builds every op as a lazy `UOp`, caches compiled kernels by their AST, and TinyJit replays the kernels a real run captured. BPL's budgets sit below both: no Python, no device launch before the GPU stage. ### What follows 1. Small calls: a call then a read of its result ≈ 300 + 150 + 235 = 685 + work. Every pending value keeps its node; a call matching no rule pays 1 root-key probe (20). 2. Ordinary code against replay, a body of N calls run again at the same shapes: ordinary ≈ 300·N (dispatch) + 150 + 235·N (read, plan cached by structure) = 535·N + 150; replay ≈ 150 + 135·N. About 4× at tiny sizes; at 10⁶ floats a step's work (about 10⁶) dwarfs both. -3. Lengths: power-of-two buckets, refill on each run whose lengths changed (part 10). - - A plan's cost-based choices depend on item counts: the compile decision (part 9), the cheaper of 2 reads (contraction), recomputing a cheap chain. Across a bucket each is off by at most 2×; a choice that a length makes exactly (an unrolled short fold) is never made on a bucketed axis (part 10); a plan per exact shape would make them exactly, for little gain. - - A growing carry (1 item more each step): buckets ≈ (130 + 135)·N = 265·N a step, a plan at each power of two; a plan per exact shape ≈ (270 + 135)·N = 405·N a step; exact lengths only (a retrace per shape up to the limit, then ordinary code whose reads miss the cache) ≈ (300 + 30 + 270 + 135)·N = 735·N a step. - - Refill is a subset of planning by construction (descriptions and maps only), so it can't reach a plan's cost: no branch waits on a measurement. -4. 1 cache, keyed by structure (part 10): a function value's entry holds its recordings; a forced graph's plan sits under its graph's structure, leaves as descriptions (a length class and bucket per axis); a lone node is a graph of 1. Ordinary code refills as a replay does: a growing value read outside a trace ≈ 300·N + 150 + (235 + 130)·N ≈ 665·N a step. -5. Memory between reads: a pending graph keeps its leaves alive, never an intermediate; a node ≈ 150 B; the memory limit (part 8) forces first. +3. Lengths: per-axis 0/1/more classes, with refill when lengths or count parameters change (part 10). + - Run preparation uses actual counts for compilation, contraction's cheaper read and recomputation when multiplicity matters. These cheap checks belong to kernel setup, outside item loops. The cached structural plan and code cache do not change for a new length alone. + - A growing carry (1 item more each step) reuses its plan within a length class and the plan's guards. Refill plus replay is ≈ (130 + 135)·N = 265·N a step, including kernel setup. Scheduling every exact shape would cost ≈ (270 + 135)·N = 405·N a step. Geometric buffer capacity growth is independent of this key policy. + - Refill derives descriptions and maps without structural scheduling. Run preparation uses the run's actual work estimates under normal speed budgets. +4. 1 cache, keyed by structure (part 10): a function value's entry holds its recordings; a forced graph's plan sits under its graph's structure, inputs keyed by part 10's read kinds; a lone node is a graph of 1. Ordinary code refills as a replay does: a growing value read outside a trace ≈ 300·N + 150 + (235 + 130)·N ≈ 665·N a step. +5. Memory between reads: pending graphs keep leaves alive, not intermediates. Node estimates include origins and variable operands, output descriptions and argument trees. The memory limit reads bytes from descriptions, not a call plan. 6. Scalar runs: a `⍣` loop of scalar ops is a loop node replaying its body (150 + 135·N an iteration); a scalar op outside a loop ≈ 685 (accepted: "The bar", "No cost when unused"). -7. A new length or bucket never compiles: kernels are keyed by structure; counts are run-time parameters. +7. A new length alone does not produce a compiled version. Counts are run-time parameters. A useful recipe that emits different code compiles on its first justified use, then reuses that code (part 9). ### Checking @@ -176,13 +204,11 @@ Futhark, Dex and Julia set the standard. A design at least as good would meet th | One rule set | Each scalar glyph's partial derivatives are written once and serve both modes. Each linear operation has one transpose. A user rule attaches once and works in both modes. | Futhark's `Derivatives.hs`, Dex's `linearizeOp`, JAX's `custom_jvp`. | | Higher orders | `∂∂` is `∂` applied to the function `f∂`, with no confusion between nesting levels. | ForwardDiff's tags. JAX nests transformations. | | No cost when unused | Code that isn't differentiated runs as fast as now, except agreed exceptions. Agreed: always-lazy evaluation gives every call a node and a read, so a lone primitive call costs about 1.5 to 2 times today's eager call (about 685 instructions plus its work: "Cost model"), while dfn calls, loops and scalar recursion cost less. | | -| Errors | When and how an error from deferred work surfaces isn't promised. Leaving an error guard forces the work made inside it, so the guard catches its errors. Effects keep their order among themselves; an effect forces only what it reads. | `meta/ideas.md`, "Block fusion", names late errors as a cost of lazy arrays. | +| Errors | Construction and host errors stay checked; dynamic graph-numeric execution also checks data errors, while static graph-numeric execution follows Measures' non-raising policy. When and how a checked deferred error surfaces isn't promised. Leaving an error guard forces the work made inside it, so the guard catches its errors. Effects keep their order among themselves; an effect forces only what it reads. | `meta/ideas.md`, "Block fusion", names late errors as a cost of lazy arrays. | | Composes with optimisation | The rewrite rules and the scheduler apply to tangent and transpose nodes as to any other nodes. Derivative code fuses, windows and stencils included. The scheduler recomputes a residual that is cheap to compute again, rather than storing it. | XLA simplifies and fuses the programs that JAX's transforms produce. | | Evaluator | The change doesn't repeat evaluator semantics. | | | Build time | After a small edit to one function, the dev build rebuilds about as fast as now. Functions that every call goes through, such as `Primitive::call_axis`, show growth first. | The baseline below this table. | -Exact derivatives for exact inputs, as `⌻` gives now, would be a BPL extra. - Gradients over arrays of thousands of values or more, as in neural-net training and image diffusion, are in scope. `∂` therefore needs reverse mode, and the gradient-cost criterion is a requirement. Build-time baseline: the total time of `cargo build --lib --bins --profile test --timings`, the build that DEV.md documents for inspecting compilation. Each edit wrapped one value in `std::hint::black_box`, which changes the generated code but not the behaviour. Each function was timed after the edit and again after undoing it: @@ -196,11 +222,10 @@ Build-time baseline: the total time of `cargo build --lib --bins --profile test ## Acceptance -Every model in `tests/speed/models.bpl` and in each held-out set (written by fresh agents from the docs alone; implementation forks don't read them) passes under static kinds: (1) it traces whole: its forward pass and `∂` of every scalar loss each become 1 cached graph (`•explain`: "built" once, then "replayed", no "not kept"); (2) its training loop `step⍣N params` is 1 node, traced on the 1st iteration and replayed after; (3) its forward and gradient costs meet first-principles expected costs with no `held` lines, each gradient at most about 3× its forward pass; (4) it passes in at least 2 spellings (tacit and dfn, or index notation and plain) at costs within a small margin. Every rule has a reason that depends on no model, and a reference case outside the models. Under dynamic kinds every model gives correct results, costs recorded; 2 speed cases check the 4× aim: a fused chain through a value-dependent kind (`+/⍟v` or `+/√v` on a float column) and integer date arithmetic (`7|3+d` on day numbers). Also: every deletion in the parts' "Not" lists, and DEV.md's full checks. +Every model in `tests/speed/models.bpl` and in each held-out set (written by fresh agents from the docs alone; implementation forks don't read them) passes under static kinds: (1) it traces whole: its forward pass and `∂` of every scalar loss each become 1 cached graph (`•explain`: "built" once, then "replayed", no "not kept"); (2) its training loop `step⍣N params` is 1 node, traced on the 1st iteration and replayed after; (3) its forward and gradient costs meet first-principles expected costs with no `held` lines, each gradient at most about 3× its forward pass; (4) it passes in at least 2 spellings (tacit and dfn, or index notation and plain) at costs within a small margin. Every rule has a reason that depends on no model, and a reference case outside the models. Under dynamic kinds every model gives correct results, costs recorded; 2 speed cases check the 4× aim: a fused chain through a real form (`+/⍟ₐv` or `+/√ₐv` on a float column) and integer date arithmetic (`7|3+d` on day numbers). Also: every deletion in the parts' "Not" lists, and DEV.md's full checks. ## Use -- A fork's step names the part it moves toward and the departure it removes. -- A review measures a diff against its part, never against neighbouring code. A departure left → a gap row; a departure added → rejected. -- The gap review lists each departure in the tree, by part, with its home in the target. -- After the target is fixed, a change goes in an errata doc: each entry names the part number it replaces and why. +- A coherent milestone names its target parts, removed mechanisms, net source lines and unresolved departures. +- Review against the target, not neighbouring code. Record remaining departures by part and home; reject a new parallel mechanism rather than accommodate it. +- Incorporate agreed design changes directly into this target. Keep no separate amendment document. diff --git a/nbs/web.ipynb b/nbs/web.ipynb index f4162c85..33168978 100644 --- a/nbs/web.ipynb +++ b/nbs/web.ipynb @@ -175,7 +175,8 @@ } ], "source": [ - "(•canvas \"sq\") 50" + "square←•canvas \"sq\"\n", + "square 50" ] }, { @@ -774,7 +775,7 @@ "- `•js` exists only in the browser build.\n", "- `•readdir`, `•metadata`, `•copy`, `•rename`, `•remove`, `•mkdir`, `•path` and `•delay` don't exist. `•nput` writes only to `\"-\"`.\n", "- File reads and `•fetch` are synchronous `XMLHttpRequest`s, relative to the URL you pass to `configure`.\n", - "- `•r` uses JavaScript's `RegExp`, with its syntax and replacement templates.\n", + "- `•r` and `•replace` use JavaScript's `RegExp`, with its syntax and replacement templates.\n", "- `•date⁻¹` has no `locale` option.\n", "- Evaluation nests at most 244 levels deep. A call of a function other than a primitive, a group and each statement of a dfn before its last each count a level. A recursive dfn such as `{⍵=0?0;1+∇ ⍵-1}` takes two levels a call, for 120 calls. A deeper evaluation is a `LIMIT` error.\n", "- Fused computations run on kernels built into the module. The browser build compiles no machine code at run time, where native builds compile large fused computations, as [Performance](performance.qmd) describes.\n", diff --git a/py/src/lib.rs b/py/src/lib.rs index f8dae4dd..10e3a22f 100644 --- a/py/src/lib.rs +++ b/py/src/lib.rs @@ -1,8 +1,7 @@ use basedpl::{ - EvalOptions, Evaluation, Function, InterruptHandle, Number, Operand, Session, Value, buffer::{Buffer, DType, FloatWidth, IntBuf, View}, keyed::{self, Texts} + EvalOptions, Evaluation, Function, InterruptHandle, Number, ReadKind, Session, Value, buffer::{Buffer, DType, FloatWidth, IntBuf, View}, keyed::{self, Texts} }; use num_bigint::BigInt; -use num_rational::BigRational; use pyo3::{ IntoPyObjectExt, exceptions::{PyBufferError, PyKeyError, PyTypeError, PyValueError}, ffi, prelude::*, sync::PyOnceLock, types::{PyBool, PyBoolMethods, PyComplex, PyComplexMethods, PyDict, PyFloat, PyInt, PyList, PyRange, PySlice, PyString, PyTuple, PyType} @@ -52,7 +51,7 @@ fn record(keys: Arc, data: Vec, name: Option>, width: Floa /// `value` with `keys` and `names` on its axes. fn labelled(value: Value, keys: Vec>>, names: Vec>>) -> PyResult { let layout = value.core_layout().clone().with_keys(keys).and_then(|l| l.with_names(names)).map_err(rustymath_buffer::py_err)?; - value.with_core_layout(layout).map_err(value_error) + Session::current(|_| value.with_core_layout(layout)).map_err(value_error) } /// The value of an object that exports a buffer, such as a rustymath array or a `memoryview`, with its dtype and labels. @@ -95,12 +94,6 @@ fn numpy(obj: &Bound<'_, PyAny>, items: &mut Items) -> PyResult> { Value::imported(obj.getattr("shape")?.extract()?, data, prototype).map(Some).map_err(value_error) } -/// Python's `fractions.Fraction`. -fn fraction(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { - static FRACTION: PyOnceLock> = PyOnceLock::new(); - FRACTION.import(py, "fractions", "Fraction") -} - /// The BPL value of `obj` as an item of an array, converted by `items`. A Python float or complex number is weak, a complex number follows the current settings' kinds, and `None` reads as NaN. A `dict` and a data frame give keyed vectors, and a list gives a nested array. fn element(obj: &Bound<'_, PyAny>, items: &mut Items) -> PyResult { let width = items.width; @@ -124,10 +117,7 @@ fn element(obj: &Bound<'_, PyAny>, items: &mut Items) -> PyResult { return record(keys, data, None, width); } if obj.is_instance_of::() || obj.is_instance_of::() || obj.is_instance_of::() { return import(obj, items); } - if obj.is_instance(fraction(obj.py())?)? { - let rational = BigRational::new_raw(obj.getattr("numerator")?.extract()?, obj.getattr("denominator")?.extract()?); - return Number::try_from(rational).map(Value::Number).map_err(value_error); - } + if let Some(rational) = rustymath_buffer::rational(obj)? { return Number::try_from(rational).map(Value::Number).map_err(value_error); } if let Some(value) = numpy(obj, items)? { return Ok(value); } if let Some(frame) = rustymath_buffer::frame::frame(obj)? { let ([rows, cols], [row, col]) = (frame.keys, frame.names); @@ -139,8 +129,17 @@ fn element(obj: &Bound<'_, PyAny>, items: &mut Items) -> PyResult { Err(PyTypeError::new_err(format!("cannot convert {} to BPL", obj.get_type().name()?))) } +fn observed(value: &Value, kind: ReadKind) -> PyResult { + Python::attach(|py| py.detach(|| Session::current(|session| -> Result { + let value = value.read(kind)?; + session.raise()?; + Ok(value) + }))).map_err(value_error) +} +fn array(py: Python<'_>, value: &Value) -> PyResult> { exported(py, &observed(value, ReadKind::Value)?) } + /// `a`'s parts for Python: its `_Array` for compact items, which Python reads through `tolist` or the buffer protocol, or else a dict of its shape, items, prototype, keys and names. -fn array(py: Python<'_>, a: &Value) -> PyResult> { +fn exported(py: Python<'_>, a: &Value) -> PyResult> { fn element(py: Python<'_>, e: &Value) -> PyResult> { Ok(match e { Value::Number(n) => { @@ -153,10 +152,10 @@ fn array(py: Python<'_>, a: &Value) -> PyResult> { else { PyFloat::new(py, n.as_float().unwrap()).into_any().unbind() } } Value::Character(c) => PyString::new(py, &c.to_string()).into_any().unbind(), - a @ Value::Array(_) => array(py, a)?, + Value::Array(_) => exported(py, e)?, Value::Function(f) => Py::new(py, PyFunction { inner: f.clone() })?.into_any(), Value::Operator(op) => Py::new(py, PyOperator { inner: op.clone() })?.into_any(), - Value::Carried(_) => return Err(PyValueError::new_err("a value that a running ∂ carries has no Python value")), + Value::Pending(_) | Value::Carried { .. } => unreachable!("Python exports observed values"), }) } if !a.is_atom() && a.view().is_some() { return Ok(Py::new(py, PyArray::from(a.clone()))?.into_any()); } @@ -193,9 +192,9 @@ fn index_error(error: basedpl::Error) -> PyErr { rustymath_buffer::py_err(rustymath::error::Error::new(e.kind, text)) } -/// Decodes a Python index of `value` once, for reads and writes, through `⌷`'s planner. Python's own kinds, slices, keys, integers and lists of keys, decode without converting. `convert` turns other selectors into arrays, which decode as `⌷` decodes them, except Boolean masks, which select where they hold true. Pick paths allow positions, keys, full slices, None and ... as suffixes, but not partial slices or masks. -fn selection(value: &Value, index: &Bound<'_, PyAny>, convert: &Bound<'_, PyAny>) -> PyResult { - let converted = |obj: &Bound<'_, PyAny>| -> PyResult { Ok(convert.call1((obj,))?.extract::>()?.value()?.clone()) }; +/// Decodes a Python index of `value` once, for a read or, where `writes`, a write, through `⌷`'s planner. Python's own kinds, slices, keys, integers and lists of keys, decode without converting. `convert` turns other selectors into arrays, which decode as `⌷` decodes them, except Boolean masks, which select where they hold true. Pick paths allow positions, keys, full slices, None and ... as suffixes, but not partial slices or masks. +fn selection(value: &Value, index: &Bound<'_, PyAny>, writes: bool, convert: &Bound<'_, PyAny>) -> PyResult { + let converted = |obj: &Bound<'_, PyAny>| -> PyResult { observed(convert.call1((obj,))?.extract::>()?.value()?, ReadKind::Structure) }; let parts = rustymath_buffer::selection::parts(index)?; let values = parts.iter().map(|p| p.as_ref().try_map(|p| if rustymath_buffer::selection::native(p) { Ok(None) } else { converted(p).map(Some) })) .collect::>>()?; @@ -210,7 +209,7 @@ fn selection(value: &Value, index: &Bound<'_, PyAny>, convert: &Bound<'_, PyAny> return Ok(IndexSelection::Rich(paths)); } let layout = value.core_layout(); - value.planned(&values, |i, axis| { + value.planned(&values, writes, |i, axis| { let (IndexPart::Axis(p), IndexPart::Axis(v)) = (&parts[i], &values[i]) else { unreachable!("planned decodes axis parts") }; match v { None => Some(rustymath_buffer::selection::selector(layout, axis, p, || unreachable!("a native selector decodes without converting"))), @@ -226,8 +225,11 @@ fn selection(value: &Value, index: &Bound<'_, PyAny>, convert: &Bound<'_, PyAny> /// The array whose compact items an export lends for `value`, with the offset of `value`'s first item in them and, for a structural copy read through an affine map, each axis's stride in items. A copy that hasn't been built lends its source's items this way. Otherwise `value` lends its own items where they're compact. `None` where neither applies. fn lendable(value: &Value) -> Option<(Value, usize, Option>)> { - if let Some((source, offset, strides)) = value.affine().filter(|(source, ..)| source.view().is_some()) { return Some((source, offset, Some(strides))); } - value.view().map(|_| (value.clone(), 0, None)) + let (_, map) = value.physical()?; + let (offset, strides) = match map { + Some(map) => { let (offset, strides) = map.affine(value.shape())?; (offset, Some(strides)) }, None => (0, None), + }; + Some((value.clone(), offset, strides)) } /// Items that `own` moved out of BPL, which NumPy views writably through `data`, their address, as `dtype`. NumPy can write any bytes into them, so Booleans are held as `u8` and characters as `i32`, which have no invalid values. @@ -285,7 +287,7 @@ impl PyArray { let value = self.value()?; let layout = value.core_layout().clone().with_names(names.unwrap_or_default().into_iter().map(|n| n.map(Arc::from)).collect()) .map_err(rustymath_buffer::py_err)?; - Ok(value.clone().with_core_layout(layout).map_err(|k| PyValueError::new_err(k.to_string()))?.into()) + Ok(Session::current(|_| value.clone().with_core_layout(layout)).map_err(value_error)?.into()) } #[getter] fn keys(&self) -> PyResult>>>> { Ok(basedpl::keyed::key_lists(self.value()?)) } @@ -293,15 +295,15 @@ impl PyArray { let value = self.value()?; let keys = rustymath_buffer::axis_keys(keys.unwrap_or_default())?; let layout = value.core_layout().clone().with_keys(keys).map_err(rustymath_buffer::py_err)?; - Ok(value.clone().with_core_layout(layout).map_err(|k| PyValueError::new_err(k.to_string()))?.into()) + Ok(Session::current(|_| value.clone().with_core_layout(layout)).map_err(value_error)?.into()) } fn __rustymath_array__(&self, py: Python<'_>) -> PyResult<(Py, Py, Vec>)> { if !self.exports()? { return Err(PyTypeError::new_err("only ordinary compact values can cross to Rustymath; convert exact or mixed values explicitly")); } - let value = self.value()?; + let value = observed(self.value()?, ReadKind::Value)?; let owner = match value.widened_for_export() { - Some(items) => Owned::new(items, value.shape()).into_py_any(py)?, None => self.copy()?.into_py_any(py)? + Some(items) => Owned::new(items, value.shape()).into_py_any(py)?, None => PyArray::from(value.clone()).into_py_any(py)? }; Ok((owner, self.keys()?.into_py_any(py)?, self.names()?)) } @@ -313,49 +315,52 @@ impl PyArray { fn parts(&self, py: Python<'_>) -> PyResult> { array(py, self.value()?) } /// The items as nested lists of Python scalars, or one scalar at rank 0, read without NumPy. Only compact items convert. fn tolist<'py>(&self, py: Python<'py>) -> PyResult> { - let value = self.value()?; + let value = observed(self.value()?, ReadKind::Value)?; let items = value.view().ok_or_else(|| PyTypeError::new_err("only compact items convert to a list"))?; rustymath_buffer::tolist(py, items, value.shape()) } /// The string that an unkeyed, unnamed character vector holds, or `None`. #[getter] fn text(&self) -> PyResult> { - let value = self.value()?; + let value = observed(self.value()?, ReadKind::Value)?; let plain = value.shape().len() == 1 && !value.has_keys() && value.axis_names().is_empty(); - Ok(match value.view() { Some(View::Characters(c)) if plain => Some(c.iter().collect()), _ => None }) + Ok(plain.then(|| keyed::name(&value).map(|text| text.to_string())).flatten()) } /// Character content, independent of metadata, as MIME's text rule reads it. #[getter] - fn text_content(&self) -> PyResult> { Ok(basedpl::keyed::name(self.value()?).map(|s| s.to_string())) } + fn text_content(&self) -> PyResult> { Ok(basedpl::keyed::name(&observed(self.value()?, ReadKind::Value)?).map(|s| s.to_string())) } /// Whether NumPy can read the items through the buffer protocol: compact storage, or a structural copy that reads compact items through an affine map. #[getter] - fn exports(&self) -> PyResult { Ok(lendable(self.value()?).is_some()) } + fn exports(&self) -> PyResult { Ok(lendable(&observed(self.value()?, ReadKind::Value)?).is_some()) } #[getter] fn export_dtype(&self) -> PyResult> { Ok(self.value()?.export_widening().map(|w| DType::Integer(w).name())) } /// Lends physical storage read-only. The public NumPy adapter selects the logical export dtype before applying the requested dtype and copy policy. unsafe fn __getbuffer__(slf: Bound<'_, Self>, view: *mut ffi::Py_buffer, flags: c_int) -> PyResult<()> { - let value = slf.borrow().value()?.clone(); + let value = observed(slf.borrow().value()?, ReadKind::Value)?; let (items, offset, strides) = lendable(&value).ok_or_else(|| PyBufferError::new_err("an array of mixed or exact items has no buffer"))?; - let dtype = items.view().expect("lendable items are compact").dtype(); + let dtype = items.physical().expect("stored physical items").0.dtype(); // SAFETY: the offset and strides reach only items of `items`, as the copy's map does. An atom holds its items inline, so the pointer comes from the copy that the export holds. unsafe { rustymath_buffer::lend( - view, flags, slf.py(), items, |v| v.view().expect("the items are compact").as_ptr().add(offset * dtype.bytes()), dtype, value.shape(), + view, flags, slf.py(), items, |v| v.physical().expect("stored physical items").0.as_ptr().add(offset * dtype.bytes()), dtype, value.shape(), strides.as_deref()) } } /// The items for NumPy to view writably: moved with no copy where nothing else holds them, and copied otherwise. Integers that static kinds store narrower than their kind come widened to it (`Value::widened_for_export`). For items it can't move, such as exact numbers, it still empties the array and returns `None`, so the caller copies them first. fn own(&mut self) -> PyResult> { - self.value()?; - let value = self.inner.take().expect("checked above"); + let value = observed(self.value()?, ReadKind::Value)?; + self.inner = None; let shape = value.shape().to_vec(); let items = match value.widened_for_export() { Some(items) => Some(items), None => value.into_buffer().ok() }; Ok(items.map(|items| Owned::new(items, &shape))) } - fn __repr__(&self) -> PyResult { Ok(self.value()?.to_string()) } - fn literal(&self) -> PyResult { Ok(self.value()?.literal()) } - fn native_atom(&self) -> PyResult { + fn __repr__(&self) -> PyResult { let value = self.value()?; + Python::attach(|py| py.detach(|| Session::current(|session| session.show(value)))).map_err(value_error) + } + fn literal(&self) -> PyResult { Ok(observed(self.value()?, ReadKind::Value)?.literal()) } + fn native_atom(&self) -> PyResult { + let value = observed(self.value()?, ReadKind::Value)?; if !value.is_singleton() { return Err(PyValueError::new_err("conversion requires a singleton array")); } let item = value.at(0); if matches!(item, Value::Array(_)) { return Err(PyTypeError::new_err("conversion requires an atom")); } @@ -365,27 +370,23 @@ impl PyArray { fn cells(&self) -> PyResult> { let value = self.value()?; if value.shape().is_empty() { return Err(PyTypeError::new_err("a unit has no major cells")); } - let cells = value.major_cells().map_err(|e| PyValueError::new_err(e.to_string()))?; + let cells = Session::current(|_| value.major_cells()).map_err(value_error)?; Ok(cells.into_iter().map(Self::from).collect()) } fn select(&self, py: Python<'_>, index: &Bound<'_, PyAny>, convert: &Bound<'_, PyAny>) -> PyResult> { - let (value, selection) = (self.value()?.clone(), selection(self.value()?, index, convert)?); - let value = py.detach(move || { - let _turn = Session::current(); - match selection { IndexSelection::Planned(plan, atom) => value.selected(plan, atom), IndexSelection::Rich(parts) => value.select(&parts) } - }).map_err(index_error)?; + let (value, selection) = (self.value()?.clone(), selection(self.value()?, index, false, convert)?); + let value = py.detach(move || Session::current(|_| match selection { + IndexSelection::Planned(plan, atom) => value.selected(plan, atom), IndexSelection::Rich(parts) => value.select(&parts) + })).map_err(index_error)?; let mut result = Evaluation::default(); match value { Value::Function(f) => result.function = Some(f), a => result.value = Some(a) } response(py, result) } fn assign(&self, py: Python<'_>, index: &Bound<'_, PyAny>, values: PyRef<'_, PyArray>, convert: &Bound<'_, PyAny>) -> PyResult { - let (value, values, selection) = (self.value()?.clone(), values.value()?.clone(), selection(self.value()?, index, convert)?); - py.detach(move || { - let _turn = Session::current(); - match selection { - IndexSelection::Planned(plan, atom) => value.assigned(plan, atom, &values), IndexSelection::Rich(parts) => value.assigned_parts(&parts, &values) - } - }).map(Self::from).map_err(index_error) + let (value, values, selection) = (self.value()?.clone(), values.value()?.clone(), selection(self.value()?, index, true, convert)?); + py.detach(move || Session::current(|_| match selection { + IndexSelection::Planned(plan, atom) => value.assigned(plan, atom, &values), IndexSelection::Rich(parts) => value.assigned_parts(&parts, &values) + })).map(Self::from).map_err(index_error) } /// Whether this array holds `query`, a native array or a `str`. A one-character string is an atom. A longer one is one item where the major cells are items, as a list of strings holds them, and otherwise a character vector, which matches a row of characters. fn contains(&self, py: Python<'_>, query: &Bound<'_, PyAny>) -> PyResult { @@ -399,12 +400,9 @@ impl PyArray { }, Err(_) => query.extract::>()?.value()?.clone(), }; - py.detach(move || { - let _turn = Session::current(); - value.contains(&query) - }).map_err(|e| match &e.kind { + py.detach(move || Session::current(|_| value.contains(&query))).map_err(|e| match &e.kind { basedpl::ErrorKind::Array(e) if e.kind == rustymath::error::Kind::Rank => PyTypeError::new_err(e.message.to_string()), - _ => PyValueError::new_err(e.to_string()), + _ => value_error(e), }) } } @@ -412,9 +410,9 @@ impl PyArray { #[pyclass(frozen, name = "_Function")] struct PyFunction { inner: Function } -fn operand(value: &Bound<'_, PyAny>) -> PyResult { - if let Ok(f) = value.extract::>() { return Ok(Operand::Function(f.inner.clone())); } - Ok(Operand::Value(value.extract::>()?.value()?.clone())) +fn operand(value: &Bound<'_, PyAny>) -> PyResult { + if let Ok(function) = value.extract::>() { return Ok(Value::Function(function.inner.clone())); } + Ok(value.extract::>()?.value()?.clone()) } #[pymethods] @@ -426,18 +424,22 @@ impl PyFunction { #[staticmethod] fn build(kind: &str, values: Vec>) -> PyResult { let operands = values.iter().map(operand).collect::>>()?; - Function::build(kind, operands).map(|inner| Self { inner }).map_err(|e| PyValueError::new_err(e.to_string())) + Function::build(kind, operands).map(|inner| Self { inner }).map_err(value_error) + } + fn __repr__(&self) -> PyResult { + let Value::Function(function) = observed(&Value::Function(self.inner.clone()), ReadKind::Value)? else { unreachable!("a function stays a function") }; + Ok(function.bpl()) } - fn __repr__(&self) -> String { self.inner.bpl() } /// This function with `g` attached as its inverse, as BPL's `f⁻¹←g` attaches it. fn with_inverse(&self, g: PyRef<'_, PyFunction>) -> Self { Self { inner: self.inner.with_inverse(g.inner.clone()) } } /// This function with `g` attached as its derivative, as BPL's `f∂←g` attaches it. fn with_derivative(&self, g: PyRef<'_, PyFunction>) -> Self { Self { inner: self.inner.with_derivative(g.inner.clone()) } } fn parts(&self, py: Python<'_>) -> PyResult>)>> { let Some((kind, operands)) = self.inner.parts() else { return Ok(None); }; - let args = operands.into_iter().map(|a| match a { - Operand::Function(inner) => Py::new(py, PyFunction { inner }).map(|f| f.into_any()), - Operand::Value(inner) => Py::new(py, PyArray::from(inner)).map(|a| a.into_any()), + let args = operands.into_iter().map(|value| match value { + Value::Function(inner) => Py::new(py, PyFunction { inner }).map(|function| function.into_any()), + Value::Operator(inner) => Py::new(py, PyOperator { inner }).map(|operator| operator.into_any()), + inner => Py::new(py, PyArray::from(inner)).map(|array| array.into_any()), }).collect::>>()?; Ok(Some((kind, args))) } @@ -453,19 +455,20 @@ impl PyOperator { #[pyo3(signature = (left, right=None))] fn derive(&self, left: &Bound<'_, PyAny>, right: Option<&Bound<'_, PyAny>>) -> PyResult { let right = right.map(operand).transpose()?; - self.inner.derive(operand(left)?, right).map(|inner| PyFunction { inner }).map_err(|e| PyValueError::new_err(e.to_string())) + self.inner.derive(operand(left)?, right).map(|inner| PyFunction { inner }).map_err(value_error) + } + fn __repr__(&self) -> PyResult { + let Value::Operator(operator) = observed(&Value::Operator(self.inner.clone()), ReadKind::Value)? else { unreachable!("an operator stays an operator") }; + Ok(operator.bpl()) } - fn __repr__(&self) -> String { self.inner.to_string() } } /// What a request runs: source code, or a function called with arguments. enum Run { Code(String), Call(Function, Vec) } -struct EvalRequest { run: Option, bindings: Vec<(String, Operand)>, options: EvalOptions } +struct EvalRequest { run: Option, bindings: Vec<(String, Value)>, options: EvalOptions } impl EvalRequest { fn run(&mut self, session: &Session) -> Result { for (name, value) in self.bindings.drain(..) { - match value { - Operand::Value(a) => session.set(&name, a), Operand::Function(f) => session.set_function(&name, f) - }.map_err(|_| "binding requires an ordinary BPL name and an exportable value")?; + session.set(&name, value).map_err(|_| "binding requires an ordinary BPL name and an exportable value")?; } let options = std::mem::take(&mut self.options); Ok(match &self.run { @@ -501,10 +504,9 @@ impl PySession { #[pyo3(signature = (*, name=None, function=None))] fn inspect(&self, py: Python<'_>, name: Option, function: Option>) -> PyResult>> { let function = function.map(|f| f.inner.clone()); - let info = py.detach(move || { - let session = Session::current(); - match (name, function) { (Some(name), _) => session.inspect(&name), (_, Some(f)) => Some(f.inspect(&session)), _ => None } - }); + let info = py.detach(move || Session::current(|session| { + match (name, function) { (Some(name), _) => session.inspect(&name), (_, Some(function)) => Some(function.inspect(session)), _ => None } + })); info.map(|i| { let d = PyDict::new(py); d.set_item("kind", i.kind)?; @@ -515,7 +517,7 @@ impl PySession { } /// The function that `expression` writes, late-bound, with its numbers read at the session's default width. fn late_bound(&self, py: Python<'_>, expression: &str) -> PyResult> { - let result = match py.detach(|| Session::current().late_bound(expression)) { + let result = match py.detach(|| Session::current(|session| session.late_bound(expression))) { Ok(function) => Evaluation { function: Some(function), ..Evaluation::default() }, Err(error) => Evaluation { error: Some(error), ..Evaluation::default() }, }; @@ -530,10 +532,7 @@ impl PySession { fn make_interactive(&self) { Session::make_interactive() } #[pyo3(signature = (prefix="", classes=vec![2, 3, 4], complete=false))] fn names(&self, py: Python<'_>, prefix: &str, classes: Vec, complete: bool) -> Vec { - py.detach(|| { - let session = Session::current(); - if complete { session.complete(prefix) } else { session.name_list(&classes, prefix) } - }) + py.detach(|| Session::current(|session| if complete { session.complete(prefix) } else { session.name_list(&classes, prefix) })) } fn interrupt(&self) { if let Some(active) = &*self.active.lock().unwrap() { active.interrupt(); } } } @@ -548,13 +547,12 @@ impl PySession { request.options.output = show.map(|show| stream(show, shown.clone(), signal.clone())); let mut result = { let active = &self.active; - let result = py.detach(move || { - let session = Session::current(); + let result = py.detach(move || Session::current(|session| { let outer = active.lock().unwrap().replace(request.options.interrupt.clone()); - let result = request.run(&session); + let result = request.run(session); *active.lock().unwrap() = outer; result - }); + })); result.map_err(PyValueError::new_err)? }; result.output.append(&mut shown.lock().unwrap()); @@ -648,7 +646,7 @@ fn output(py: Python<'_>, events: &[basedpl::Output]) -> PyResult> { p /// BPL source for a value captured from another interpreter, given as JSON. #[pyfunction] fn _captured_literal(value: &str) -> PyResult { - let value = serde_json::from_str(value).map_err(|e| PyValueError::new_err(e.to_string()))?; + let value = serde_json::from_str(value).map_err(value_error)?; basedpl::reference::expected_array(&value).map(|v| v.literal()).ok_or_else(|| PyValueError::new_err("invalid captured array")) } diff --git a/pyproject.toml b/pyproject.toml index 1410a261..3c193577 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ classifiers = [ ] [project.optional-dependencies] -dev = ["fastship>=0.0.11", "fastws-cli>=0.0.32", "maturin>=1.13,<2.0", "pytest", "pytest-timeout", "numpy", "pandas", "ipython>=9.17.1", "nbdev>=3.3.24", "conkernelclient>=0.0.23", "pillow", "rustymath>=0.1.49"] +dev = ["fastship>=0.0.11", "fastws-cli>=0.0.33", "maturin>=1.13,<2.0", "pytest", "pytest-timeout", "numpy", "pandas", "ipython>=9.17.1", "nbdev>=3.3.24", "conkernelclient>=0.0.23", "pillow", "rustymath>=0.1.49"] [project.scripts] basedpl-install = "basedpl.install:main" @@ -55,8 +55,7 @@ branch = "main" [tool.pytest.ini_options] testpaths = ["tests"] -timeout = 60 -timeout_func_only = true +timeout = 30 asyncio_default_fixture_loop_scope = "function" [tool.nbdev] diff --git a/python/basedpl/__init__.py b/python/basedpl/__init__.py index ee3b9ee9..05d5acd3 100644 --- a/python/basedpl/__init__.py +++ b/python/basedpl/__init__.py @@ -184,7 +184,7 @@ def __init__(self, *values, keys=None, names=None, **fields): def _classify(self): "Classify built-in array wrappers without changing user-defined subclasses." if type(self) in (Array, FunctionArray): - self.__class__ = FunctionArray if self._inner.holds_functions else type(self) + self.__class__ = FunctionArray if self._inner.holds_functions else Array @property def shape(self): return tuple(self._inner.shape) @property diff --git a/python/basedpl/bpl.tmLanguage.json b/python/basedpl/bpl.tmLanguage.json index 75b88c01..341eb11d 100644 --- a/python/basedpl/bpl.tmLanguage.json +++ b/python/basedpl/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" }, { @@ -53,11 +53,11 @@ "name": "support.function.bpl" }, { - "match": "(?:[¯]?[∞]|[¯]?(\\d+([.]\\d+)?|[.]\\d+)([eEₑ][¯]?\\d+)?([jJⱼ][¯]?(\\d+([.]\\d+)?|[.]\\d+)([eEₑ][¯]?\\d+)?|[rᵣ][¯]?\\d+)?)(?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ)?|[ₑⱼᵣ]", + "match": "(?:[¯]?[∞]|[¯]?(\\d+([.]\\d+)?|[.]\\d+)([eEₑ][¯]?\\d+)?([jJⱼ][¯]?(\\d+([.]\\d+)?|[.]\\d+)([eEₑ][¯]?\\d+)?|[rᵣ][¯]?\\d+)?)(?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ|ₐ)?|[ₑⱼᵣ]", "name": "constant.numeric.bpl" }, { - "match": "[⍬](?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ)?", + "match": "[⍬](?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ|ₐ)?", "name": "constant.numeric.bpl" }, { @@ -69,13 +69,13 @@ "name": "string.quoted.double.bpl" }, { - "begin": "[\\])](?=(?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ))", + "begin": "[\\])](?=(?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ|ₐ))", "beginCaptures": { "0": { "name": "source.bpl" } }, - "end": "(?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ)|$", + "end": "(?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ|ₐ)|$", "contentName": "constant.numeric.bpl", "patterns": [], "applyEndPatternLast": true, @@ -90,11 +90,11 @@ "name": "support.function.bpl" }, { - "match": "(?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ)", + "match": "(?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ|ₐ)", "name": "keyword.operator.monadic.bpl" }, { - "match": "[¨ᵇ⍨⊗⌸⇄∂/⌿\\\\⍀⁰¹²³⁴⁵⁶⁷⁸⁹⁻ᵀᵘₓ]", + "match": "[¨ᵇ⍨⊗⌸⇄∂/⌿\\\\⍀⁰¹²³⁴⁵⁶⁷⁸⁹⁻ᵀᵘₓₐ]", "name": "keyword.operator.monadic.bpl" }, { diff --git a/python/basedpl/bpltests.py b/python/basedpl/bpltests.py index a3441957..64ce4236 100644 --- a/python/basedpl/bpltests.py +++ b/python/basedpl/bpltests.py @@ -1,5 +1,5 @@ "Read BPL test cases, convert reference inventories, activate reviewed cases, time BPL as the speed test does, and set the budgets of basedpl's and rustymath's speed tests." -import json, math, os, re, statistics, subprocess, sys, tempfile, time, tomllib +import difflib, json, math, os, re, statistics, subprocess, sys, tempfile, time, tomllib from concurrent.futures import ThreadPoolExecutor from collections import defaultdict from itertools import accumulate @@ -7,13 +7,16 @@ from pathlib import Path from typing import Annotated from fastws import store +from fastws.cargo import run_tests from fastws.timelock import timing_lock +from fastws.procs import JOB_ENV, bounded, kill_group from fastcore.script import parse_cli from fastcore.basics import listify +from fastcore.meta import delegates from ._core import _captured_literal, _reference_file, _retry_pauses, _budget, _budget_lines, _budget_columns as _BUDGETS from .reference import Corpus, _dyalog_string -from . import BplError -from .worker import NATIVE, Worker, resolved, _commented +from . import BplError, _value +from .worker import LIMIT, Limit, Worker, environment, fresh, resolved, _commented, _installed from .syntax import linear SEPARATOR = '⍝ =>' @@ -189,21 +192,36 @@ def add(ids, output='tests/reference', directory='provenance'): return list(rows) -def _checked(worker, kind, spec, sentinel): - "The failures that `worker` gives for a `kind` request, `reference` or `page`, with `spec`, without the case that `sentinel` picks. A reply that doesn't report that case raises `ValueError`, so a worker that checked nothing, or passed everything, never reads as a pass." - r = worker.request({kind: spec}) +_STALL = 60 # Seconds that a check's worker may start no case or example before it counts as stuck, as a deadlock leaves it: a case runs at most a few evaluations of `reference::TIMEOUT`, 2 s each + +def _checked(worker, kind, spec, sentinel, bpl, limit): + "The failures that `worker` gives for a `kind` request, `reference` or `page`, with `spec`, without the case that `sentinel` picks. A reply that doesn't report that case raises `ValueError`, so a worker that checked nothing, or passed everything, never reads as a pass. A reference worker notes each case on stderr as it starts it (`case LINE`), and a page worker each example (`example LINE`). Where an abort, such as a stack overflow, ends the worker, or the worker starts nothing for `_STALL` seconds, the last noted case fails as `crashed` or `timeout`, and the cases after it run in a fresh worker of `bpl` while `limit` has time left. A stuck worker is closed, so no worker outlives the timing lock's hold (`_failures`). Each request gets what's left of `limit`: where it runs out, the worker stops and gives the next case or example as `unchecked`, and the sentinel goes unchecked too." + try: r = worker.request({kind: spec}, timeout=limit.left(), stall=_STALL) + except (EOFError, TimeoutError) as e: + started = re.findall(r'^case (\d+)$', worker.diagnostics, re.M) + if kind != 'reference' or not started: raise + line, stuck = int(started[-1]), isinstance(e, TimeoutError) + tail = worker.diagnostics.rsplit(f'case {line}\n', 1)[-1].strip() + rest = [i for i in spec['ids'] if not isinstance(i, int) or i > line] + try: limit.left() + except TimeoutError: rest = None + message = (str(e) if stuck else 'the worker aborted') + (f': {tail}' if tail else '') + ('; the call reached its limit, so the cases after it are unchecked' if rest is None else '') + failed = dict(id='', line=line, section='', comment='', code='', expected_code='', status='timeout' if stuck else 'crashed', message=message, actual=None) + if rest is None: return [failed] + with Worker(bpl) as w: return [failed, *_checked(w, kind, dict(spec, ids=rest), sentinel, bpl, limit)] if e := r['error']: raise ValueError(e['message']) - if sum(map(sentinel, r['failures'])) != 1: raise ValueError("the worker didn't report the case that must fail, so its result can't be trusted") + stopped = any(f.get('status') == 'unchecked' for f in r['failures']) + if not stopped and sum(map(sentinel, r['failures'])) != 1: raise ValueError("the worker didn't report the case that must fail, so its result can't be trusted") return [f for f in r['failures'] if not sentinel(f)] -def _failures(kind, specs, sentinel, build=None): - "The failures that `_checked` gives for each of `specs`, each in a fresh worker, all at once, every one running the same build, resolved once. The workers run while this holds the timing lock shared, as the reference test does. Every worker closes before this returns or raises, which ends any request still running, as after a Ctrl-C. Each failure holds its code's linear form (`syntax.linear`) as `linear`." - bpl = resolved(build) - with timing_lock(): +def _failures(kind, specs, sentinel, build=None, timeout=LIMIT): + "The failures that `_checked` gives for each of `specs`, each in a fresh worker, all at once, every one running the same build, resolved once. The workers run while this holds the timing lock shared, as the reference test does, and share `timeout`'s limit (`worker.Limit`), counted while this holds the lock. Every worker closes before this returns or raises, which ends any request still running, as after a Ctrl-C. Each failure holds its code's linear form (`syntax.linear`) as `linear`." + bpl, limit = resolved(build), Limit.of(timeout) + with timing_lock(), limit.running(): workers = [Worker(bpl) for _ in specs] pool = ThreadPoolExecutor(len(workers)) - try: return [f | {'linear': linear(f['code'])} for job in [pool.submit(_checked, w, kind, s, sentinel) for w, s in zip(workers, specs)] for f in job.result()] + try: return [f | {'linear': linear(f['code'])} for job in [pool.submit(_checked, w, kind, s, sentinel, bpl, limit) for w, s in zip(workers, specs)] for f in job.result()] finally: for w in workers: w.close() pool.shutdown() @@ -211,11 +229,12 @@ def _failures(kind, specs, sentinel, build=None): def check_file( path, # A `.bpl` reference file - ids:Annotated[int|str, {'nargs':'*'}]=None, # Check only these cases, each named by its id or its header's line number - build:str=None, # The build to check, resolved once (`worker.resolved`): a path to a `bpl`, a prefix of the id of a build that `cargo develop` kept, or None for the installed build + ids:Annotated[int|str, {'nargs':'*'}]=None, # Check only these cases: one, or a list, each named by its id or by its header's line number, as an `int` or a string of digits + build:str=None, # The build to check, resolved once (`worker.resolved`): a path to a `bpl`, a prefix of the id of a build that `cargo develop` kept, or None or `installed` for the installed build + timeout:float=LIMIT, # Seconds of work that the check gets, or a `Limit` to share (`worker.Limit`); None: no limit ): "Check the cases in `path` as `tests/reference.rs` does, and their route directives as the route test does, in a fresh worker for each core that takes every n-th case, and return the failures in the file's order." - if isinstance(ids, (int, str)): ids = [ids] + ids = None if ids is None else [int(i) if isinstance(i, str) and i.isdigit() else i for i in ([ids] if isinstance(ids, (int, str)) else ids)] source = Path(path).read_text() cases = _reference_file(source)['cases'] lines = [c['line'] for c in cases if ids is None or c['id'] in ids or c['line'] in ids] @@ -224,18 +243,19 @@ def check_file( text = source.rstrip('\n') + '\n\n⍝ bpltests-sentinel — The checker must report this case\n0 ⍝ 1\n' n = max(1, min(os.cpu_count(), len(lines))) specs = [dict(text=text, ids=[*lines[k::n], *unknown, 'bpltests-sentinel']) for k in range(n)] - return sorted(_failures('reference', specs, lambda f: f.get('id') == 'bpltests-sentinel', build), key=lambda f: f['line']) + return sorted(_failures('reference', specs, lambda f: f.get('id') == 'bpltests-sentinel', build, timeout), key=lambda f: f['line']) def check_page( path, # A `.qmd` page - build:str=None, # The build to check, resolved once (`worker.resolved`): a path to a `bpl`, a prefix of the id of a build that `cargo develop` kept, or None for the installed build, - unbatched:bool=False # Run each example with the `batch` pref off + build:str=None, # The build to check, resolved once (`worker.resolved`): a path to a `bpl`, a prefix of the id of a build that `cargo develop` kept, or None or `installed` for the installed build, + unbatched:bool=False, # Run each example with the `batch` pref off + timeout:float=LIMIT # Seconds of work that the check gets, or a `Limit` to share (`worker.Limit`); None: no limit ): "Run the BPL examples in `.qmd` page `path` in a fresh worker, as `tests/core.rs` does, and return the failures." text = Path(path).read_text().rstrip('\n') + '\n\n```bpl\n0 ⍝ 1\n```\n' line = text.count('\n') - 1 - return _failures('page', [dict(text=text, batch=False if unbatched else None)], lambda f: f.get('line') == line, build) + return _failures('page', [dict(text=text, batch=False if unbatched else None)], lambda f: f.get('line') == line, build, timeout) _ROOT = Path(__file__).parents[2] _SPEED = _ROOT/'tests'/'speed' @@ -262,30 +282,42 @@ def __repr__(self): w = 13 - len(self.unit) return '\n'.join([head, *(''.join(f'{r[1]:>{w},.0f}{self.unit}' for r in rs) + f' {rs[-1][1]/rs[0][1]-1:>+7.1%} {_commented(rs[0][0])}' for rs in zip(*self))]) +@delegates(environment) def time_cases( cases:Annotated[str, {'nargs':'+'}], # A BPL expression to time, or a list of them setup:str=None, # BPL that defines the data and its number of items `n`; None: the setup in tests/speed/arrays.bpl prefs='', # BPL that runs before `setup`, such as `•prefs ["float":32]` + section:str=None, # A speed file's section, as `file:section` such as `models.bpl:static`: the file's setup in place of `setup`, and the section's preferences after `prefs`, as the speed test runs them threads=1, # Threads that each case's rustymath calls split across, for a scaling check; 0: rustymath's default - native=False, # Time rustymath's own linear algebra in place of Accelerate, as `Worker`'s `native` does - build:Annotated[str, {'nargs':'+'}]=None, # The build to time, resolved once (`worker.resolved`): a path to a `bpl`, a prefix of the id of a build that `cargo develop` kept, or None for the installed build; or a list of builds to compare - rounds=2, # With several builds, the fresh workers that each build gets, taking turns with the other builds' in alternating order - timeout:float=None # Seconds before each worker is closed + build:Annotated[str, {'nargs':'+'}]=None, # The build to time, resolved once (`worker.resolved`): a path to a `bpl`, a prefix of the id of a build that `cargo develop` kept, or None or `installed` for the installed build; or a list of builds to compare + rounds=2, # The fresh workers that each build gets, taking turns with any other builds' in alternating order + timeout:float=LIMIT, # Seconds of work that the call gets across its builds and rounds, or a `Limit` to share (`worker.Limit`), counted from when each worker holds the timing lock; None: no limit + **kwargs ): - "Time `cases` in a fresh worker, as `tests/speed.rs` does: the fastest of five runs, per item and in copies of the data. Given several builds, it gives `ByBuild`, holding each case's fastest time on each build, and resolves each build once, as the comparison starts." + "Time `cases` in fresh workers, as `tests/speed.rs` does: in each worker the fastest of five runs, per item and in copies of the data, and of those the fastest over `rounds` workers, since a large case's time varies between processes by more than within one. Given several builds, it gives `ByBuild`, holding each case's fastest time on each build, and resolves each build once, as the comparison starts." + if section: + name, _, part = section.partition(':') + speed = _reference_file((_SPEED/name).read_text()) + header = next((c.get('prefs', '') for c in speed['cases'] if c['section'] == part), None) + if header is None: raise ValueError(f'tests/speed/{name} has no section {part!r}') + setup, prefs = speed['setup'], '\n'.join([prefs, header]) setup = _reference_file((_SPEED/'arrays.bpl').read_text())['setup'] if setup is None else setup cases = listify(cases) + limit = Limit.of(timeout) def timed(build): - with Worker(build, native) as w: r = w.request(dict(timing=dict(setup=setup, prefs=prefs, cases=cases, threads=threads)), timeout) - if e := r['error']: raise BplError(e, r['output']) if 'display' in e else ValueError(e['message']) + with Worker(build, **kwargs) as w: + r = w.request(dict(timing=dict(setup=setup, prefs=prefs, cases=cases, threads=threads)), limit.left()) + limit.spend(w.ran) + if e := r['error']: + limit.reached(e) + raise BplError(e, r['output']) if 'display' in e else ValueError(e['message']) return Costs((c['code'], c['seconds']*1e12/r['n'], c['copies']) for c in r['costs']) builds = listify(build) or [None] - if len(builds) == 1: return timed(builds[0]) - runs = [[] for _ in builds] - paths = [resolved(b) for b in builds] + runs, paths = [[] for _ in builds], [resolved(b) for b in builds] for r in range(rounds): for i in (range(len(builds)) if r % 2 == 0 else reversed(range(len(builds)))): runs[i].append(timed(paths[i])) - return ByBuild([Costs(min(rows, key=lambda row: row[1]) for rows in zip(*each)) for each in runs], builds, ' ps') + fastest = [Costs(min(rows, key=lambda row: row[1]) for rows in zip(*each)) for each in runs] + return fastest[0] if len(builds) == 1 else ByBuild(fastest, builds, ' ps') class Scaling(list): "Each case's time per item in picoseconds on one thread and on `threads` threads, its speedup, and the least speedup it must reach, as `(code, one, many, speedup, least)` rows. A case whose speedup is below its least is marked `slow`." @@ -310,14 +342,14 @@ def _physical_cores(): def thread_scaling( cases:str=None, # Codes of the cases in `_SCALING` to time, or one code; None: all of them, a 1024×1024 product, a 512×512 SVD and Hermitian eigendecomposition, QR decompositions of a stack of 16,384 8×8 matrices, a product of a 3 by 3 convolution's windows, a copy of those windows, and `*` on 4,194,304 floats setup:str=_SCALING_SETUP, # BPL that defines the cases' data and its number of items `n` - build:str=None, # The build to time, resolved once (`worker.resolved`): a path to a `bpl`, a prefix of the id of a build that `cargo develop` kept, or None for the installed build - timeout:float=None, # Seconds before each timing stops + build:str=None, # The build to time, resolved once (`worker.resolved`): a path to a `bpl`, a prefix of the id of a build that `cargo develop` kept, or None or `installed` for the installed build + timeout:float=LIMIT, # Seconds of work that the check gets across its timings and retries, or a `Limit` to share (`worker.Limit`); None: no limit ): "Time each case with `time_cases` on one thread and on t threads, the smaller of `_SCALING_THREADS` and this machine's physical cores (`_physical_cores`), and give its speedup, both on one build. Speed budgets are single-thread measurements, so this checks multithreaded speed instead: in the manual `Speed` workflow and locally, not in the `Test` workflow, where a runner's cores and sharing make it noisy. Each case must reach half its expected gain, 1 + (S − 1)/2, where S = 1/(s + (1 − s)/t) for its serial fraction s in `_SCALING`, or in `_ACCELERATED` on macOS. A case expected not to split, with s of 1, must reach 0.9. As the speed tests do, it times a case below its least again after the rest, after each pause in `rustymath::timing::RETRY_PAUSES`, and prints each retry. Raises `ValueError` with the table when a case is below its least every time." serial = _SCALING | (_ACCELERATED if sys.platform == 'darwin' else {}) threads = min(_SCALING_THREADS, _physical_cores()) least = lambda s: 1 + (1 / (s + (1 - s) / threads) - 1) / 2 if s < 1 else 0.9 - build = resolved(build) + build, timeout = resolved(build), Limit.of(timeout) def timed(codes): one, many = (time_cases(codes, setup, threads=t, build=build, timeout=timeout) for t in (1, threads)) return {code: (code, a, b, a / b, least(serial[code])) for (code, a, _), (_, b, _) in zip(one, many)} @@ -341,12 +373,15 @@ def __repr__(self): return '\n'.join(f'{n:>12,.0f} {code}' for code, n in self) # The line of each platform's counter that gives the instructions a process retired: `/usr/bin/time -l`'s on macOS, `perf stat -x,`'s on Linux. _RETIRED = dict(darwin=re.compile(r'^\s*(\d+)\s+instructions retired$', re.M), linux=re.compile(r'^(\d+),[^,]*,instructions', re.M)) -def _retired(code, bpl): - "The instructions that a fresh `bpl` process retires running `code`." +# The environment of the processes that count a case's instructions or profile its functions, with the settings that `worker.environment` takes. Loops run their steps as ordinary code (`settings::Loops`), so that a count measures a call's interpretation, as `tests/instructions.txt` records it. With `replays`, loops replay their recordings, as real runs do. +def _counting(replays=False, **kwargs): return environment(**kwargs) | ({} if replays else dict(BASEDPL_LOOPS='ordinary')) + +def _retired(code, bpl, replays, settings, limit): + "The instructions that a fresh `bpl` process retires running `code`, in the environment that `_counting` gives for `replays` and `settings`, killed once `limit` (`worker.Limit`) runs out." if sys.platform not in _RETIRED: raise ValueError(f'no instruction counter on {sys.platform}') with tempfile.NamedTemporaryFile('r') as report: counter = ['/usr/bin/time', '-l', '-o', report.name] if sys.platform == 'darwin' else ['perf', 'stat', '-x,', '-e', 'instructions:u', '-o', report.name, '--'] - p = subprocess.run([*counter, resolved(bpl), '-e', code], capture_output=True, text=True) + p = bounded([*counter, resolved(bpl), '-e', code], limit.left(), env=_counting(replays, **settings)) if p.returncode: raise ValueError(p.stdout + p.stderr) return int(_RETIRED[sys.platform].search(report.read())[1]) @@ -365,24 +400,37 @@ def _stepped(case, force=False): if _NAMES_N.search(_QUOTED.sub('', case)): return case return f'{{x←{case} ⋄ (×≢⍴x)∧×≢x?x.[0]←x.[0] ⋄ ⍵;⍵}}⍣N 0' if force else f'{{{case} ⋄ ⍵}}⍣N 0' +def _locked(pool, calls, limit=None): + "The results of `calls`, each a function and its arguments, run at once on `pool` as one batch, holding the machine's timing lock shared for that batch alone, and spending `limit` (`worker.Limit`) while it runs. A timing that waits for the lock runs between batches. An interrupt or error cancels the calls that haven't started; those running end within `limit` (`fastws.procs.bounded`)." + with timing_lock(), (limit or Limit(None)).running(): + futures = [pool.submit(*call) for call in calls] + try: return [f.result() for f in futures] + except BaseException: + for f in futures: f.cancel() + raise + +@delegates(environment) def instructions( cases:Annotated[str, {'nargs':'+'}], # A BPL expression to run once per step, such as `k(⌈/⊢)⌸a`, or one that names `N` and makes its own call per step of it, such as `{⍵}⍣N 0`, or a list of them setup='', # BPL that defines what the cases use, run before each case force=False, # Compute each step's result in full where it would stay pending, such as large windows or a transpose, for the cases that don't name `N` counts:Annotated[int, {'nargs':'+'}]=None, # Two or more values of `N`; None: four values past the allocator's warm-up, from a rough count at 1,000 steps runs=3, # Processes at each value of `N`, of which the lowest count is used - build:Annotated[str, {'nargs':'+'}]=None # The build to count, resolved once (`worker.resolved`): a path to a `bpl`, a prefix of the id of a build that `cargo develop` kept, or None for the installed build; or a list of builds to compare + replays=False, # Count loops as real runs make them, replaying their recordings, in place of running every step as ordinary code + build:Annotated[str, {'nargs':'+'}]=None, # The build to count, resolved once (`worker.resolved`): a path to a `bpl`, a prefix of the id of a build that `cargo develop` kept, or None or `installed` for the installed build; or a list of builds to compare + timeout:float=LIMIT, # Seconds of work that the count gets, or a `Limit` to share (`worker.Limit`), spent while its processes run; None: no limit + **kwargs ): "The instructions that each case retires per unit of `N`, on one thread. A case that doesn't name `N` runs once per step inside `{case ⋄ ⍵}⍣N 0`. That dfn's own call adds about 1,900 instructions per step. For each pair of successive values of `N`, the difference between fresh `bpl` processes, divided by the difference in `N`, cancels startup and `setup`. The result is the median of those slopes. The median drops a one-off cost that starts once `N` passes some size, because such a cost falls between only one pair. Unlike timings, instruction counts don't vary with load or code layout, so they show per-call changes of a few percent, and its processes run at once, one for each core. A case that names `N` must make one call per step of `N`, as `⍣` does: a map such as `f¨N⍴0` calls a dfn once on all its cells, so it gives no per-call cost. Given several builds, it gives `ByBuild`, counting each case at the same values of `N` on every build. Each build is resolved once, as the count starts." cases = listify(cases) builds = listify(build) or [None] stepped = [_stepped(case, force) for case in cases] def slope(ns, cs): return statistics.median((c1-c0)/(n1-n0) for n0, n1, c0, c1 in zip(ns, ns[1:], cs, cs[1:])) - paths = [resolved(b) for b in builds] - with timing_lock(), ThreadPoolExecutor(os.cpu_count()) as pool: + paths, limit = [resolved(b) for b in builds], Limit.of(timeout) + with ThreadPoolExecutor(os.cpu_count()) as pool: def lowest(jobs, runs=runs): - "The lowest count of `runs` processes for each `(code, n, build)` in `jobs`, from processes that run at once." - got = list(pool.map(lambda job: _retired(_program(job[0], job[1], setup), job[2]), [job for job in jobs for _ in range(runs)])) + "The lowest count of `runs` processes for each `(code, n, build)` in `jobs`, from processes that run at once as one batch (`_locked`)." + got = _locked(pool, [(_retired, _program(code, n, setup), build, replays, kwargs, limit) for code, n, build in jobs for _ in range(runs)], limit) return [min(got[i:i+runs]) for i in range(0, len(got), runs)] if counts: ns = [counts]*len(cases) else: @@ -408,15 +456,15 @@ def line(f, *ns): return (f'{ns[0]:>10,.0f} {ns[0]/self.totals[0][1]:>6.1%}' if # A frame in the call graph that macOS `sample` reports: the tree's drawing characters, whose width gives the frame's depth, then its samples and its symbol. _FRAME, _THREAD = re.compile(r'^([ +!:|]*)(\d+) (\S.*?) [\[(]'), re.compile(r'^\s*\d+ Thread_') -def _self_samples(code, bpl, seconds): - "The samples at which each function was at the top of the main thread's stack while a fresh `bpl` process ran `code`, as macOS `sample` counts them over `seconds`, with demangled names, and the thread's total. `code` must run for longer than half a second plus `seconds`. The process is then killed. It raises `ValueError` when `sample` runs a minute past `seconds`, as it does while macOS waits for an answer to a Developer Tools authorization prompt." - p = subprocess.Popen([resolved(bpl), '-e', code], stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True) +def _self_samples(code, bpl, seconds, replays, within, settings): + "The samples at which each function was at the top of the main thread's stack while a fresh `bpl` process ran `code` in the environment that `_counting` gives for `replays` and `settings`, as macOS `sample` counts them over `seconds`, with demangled names, and the thread's total. `code` must run for longer than half a second plus `seconds`. The process is then killed. It raises `TimeoutError` when `sample` runs past `within` seconds, as it does while macOS waits for an answer to a Developer Tools authorization prompt." + p = subprocess.Popen([resolved(bpl), '-e', code], stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True, env=_counting(replays, **settings)) try: time.sleep(0.5) if p.poll() is not None: raise ValueError(p.stderr.read()) with tempfile.NamedTemporaryFile('r', suffix='.txt') as report: - try: s = subprocess.run(['sample', str(p.pid), str(seconds), '-file', report.name], capture_output=True, text=True, timeout=seconds + 60) - except subprocess.TimeoutExpired: raise ValueError(rf"`sample` didn't finish within a minute of its {seconds} s sampling time. macOS may be waiting for an answer to a Developer Tools authorization prompt.") from None + try: s = subprocess.run(['sample', str(p.pid), str(seconds), '-file', report.name], capture_output=True, text=True, timeout=within) + except subprocess.TimeoutExpired: raise TimeoutError(rf"`sample` didn't finish in the {within:.1f} s left of the call's limit. macOS may be waiting for an answer to a Developer Tools authorization prompt.") from None if s.returncode: raise ValueError(s.stdout + s.stderr) lines = report.read().splitlines() finally: p.kill(); p.wait() @@ -437,43 +485,51 @@ def pop(): for name, n in zip(subprocess.run(['c++filt'], input='\n'.join(selfs), capture_output=True, text=True).stdout.splitlines(), selfs.values()): named[name] += n return named, total -def _estimates(case, setup, build, seconds, force): - "Each function's estimated instructions per step of `N` for `case` on `build`, and the build's instructions per step, as `profile` describes." +def _estimates(case, setup, build, seconds, force, replays, limit, settings): + "Each function's estimated instructions per step of `N` for `case` on `build`, and the build's instructions per step, as `profile` describes, under the settings that `worker.environment` takes." if sys.platform != 'darwin': raise ValueError(f'no sampling profiler on {sys.platform}') stepped = _stepped(case, force) bpl = resolved(build) - with timing_lock(): - [(_, per_step)] = instructions(stepped, setup, build=bpl) - samples, total = _self_samples(_program(f'({{{stepped} ⋄ ⍵}}⍣1000000000)0', max(1, round(1e8/per_step)), setup), bpl, seconds) + [(_, per_step)] = instructions(stepped, setup, build=bpl, replays=replays, timeout=limit, **settings) + with timing_lock(), limit.running(): samples, total = _self_samples(_program(f'({{{stepped} ⋄ ⍵}}⍣1000000000)0', max(1, round(1e8/per_step)), setup), bpl, seconds, replays, limit.left(), settings) return {f: per_step*n/total for f, n in samples.items()}, per_step +@delegates(environment) def profile( case, # A BPL expression, as `instructions` takes it, such as `k(⌈/⊢)⌸a` or `{⍵}⍣N 0` setup='', # BPL that defines what the case uses, run before it force=False, # Compute each step's result in full, as `instructions`' `force` does - build:str=None, # The build to profile, resolved once (`worker.resolved`): a path to a `bpl`, a prefix of the id of a build that `cargo develop` kept, or None for the installed build, + replays=False, # Count loops as real runs make them, as `instructions`' `replays` does + build:str=None, # The build to profile, resolved once (`worker.resolved`): a path to a `bpl`, a prefix of the id of a build that `cargo develop` kept, or None or `installed` for the installed build, seconds=3, # How long macOS `sample` watches the build - rows=20 # How many functions to show + timeout:float=LIMIT, # Seconds of work that the count and sampling get, or a `Limit` to share (`worker.Limit`); None: no limit + rows=20, # How many functions to show + **kwargs ): "Where `case`'s instructions per step of `N` go, by function, on macOS. A fresh process repeats the case on one thread while `sample` counts how often each function is at the top of the main thread's stack. A function's estimate is its share of those samples times the build's instructions per step from `instructions`. The shares come from time, so the estimates assume every function retires instructions at the same rate." - estimates, total = _estimates(case, setup, build, seconds, force) + estimates, total = _estimates(case, setup, build, seconds, force, replays, Limit.of(timeout), kwargs) return FunctionCounts(sorted(estimates.items(), key=lambda r: -r[1])[:rows], [('total', total)]) +@delegates(environment) def profile_diff( case, # A BPL expression, as `instructions` takes it, such as `k(⌈/⊢)⌸a` or `{⍵}⍣N 0` base:str, # The build to compare with, resolved as `build` is, such as the one installed before a change setup='', # BPL that defines what the case uses, run before it force=False, # Compute each step's result in full, as `instructions`' `force` does - build:str=None, # The build to compare with `base`, resolved once (`worker.resolved`): a path to a `bpl`, a prefix of the id of a build that `cargo develop` kept, or None for the installed build, + replays=False, # Count loops as real runs make them, as `instructions`' `replays` does + build:str=None, # The build to compare with `base`, resolved once (`worker.resolved`): a path to a `bpl`, a prefix of the id of a build that `cargo develop` kept, or None or `installed` for the installed build, seconds=3, # How long macOS `sample` watches each build - rows=20 # How many functions to show + timeout:float=LIMIT, # Seconds of work that both builds' counts and sampling get, or a `Limit` to share (`worker.Limit`); None: no limit + rows=20, # How many functions to show + **kwargs ): "Where `case`'s instructions per step of `N` moved between two builds, by function, on macOS, from `profile`'s estimates for each build. A function that only one build calls out of line, such as one that stopped being inlined, appears in that build alone." - (before, tb), (after, ta) = _estimates(case, setup, base, seconds, force), _estimates(case, setup, build, seconds, force) + limit = Limit.of(timeout) + (before, tb), (after, ta) = _estimates(case, setup, base, seconds, force, replays, limit, kwargs), _estimates(case, setup, build, seconds, force, replays, limit, kwargs) fs = sorted(before.keys() | after.keys(), key=lambda f: -abs(after.get(f, 0) - before.get(f, 0))) return FunctionCounts([(f, before.get(f, 0), after.get(f, 0)) for f in fs[:rows]], [('total', tb, ta)]) -# The script that `call_profile` runs in `lldb`'s own Python, 3.9 on macOS. `run` reads its arguments from a JSON file. It launches `bpl` with `-e code` and lets `skip` calls of the function that `pattern` names run. It then single-steps the next call to its return, the first instruction where the stack pointer is back where it was and the program counter at the address in the link register. An instruction counts under its function's name, or under `caller > function` where the function's caller, read as the program counter enters the function, matches the allocator's pattern. `call_profile` then puts what the allocator calls, such as the zeroing of a freed block and a read of the clock, in the allocator's row. It writes JSON to `out`: how many instructions each function retired, or the number of calls where the process ended first, or that the call passed `limit`. +# The script that `call_profile` runs in `lldb`'s own Python, 3.9 on macOS. `run` reads its arguments from a JSON file. It launches `bpl` with `-e code` and lets `skip` calls of the function that `pattern` names run. It then single-steps the next call to its return, the first instruction where the stack pointer is back where it was and the program counter at the address in the link register. An instruction counts under its function's name, or under `caller > function` where the function's caller, read as the program counter enters the function, matches the allocator's pattern. An instruction in the allocator also names, after a tab, the first function on the stack outside the allocator and Rust's standard library, the one whose code allocated. `call_profile` then puts what the allocator calls, such as the zeroing of a freed block and a read of the clock, in the allocator's row. It writes JSON to `out`: how many instructions each function retired, or the number of calls where the process ended first, or that the call passed `limit`. _LLDB_STEPS = r''' import collections, json, lldb, re @@ -503,6 +559,9 @@ def run(path): if here != current: current, caller = here, thread.GetFrameAtIndex(1).GetSymbol() name = f'{caller.GetName()} > {here}' if caller.IsValid() and allocator.search(caller.GetName()) and not allocator.search(here) else here + if allocator.search(name): + frames = (thread.GetFrameAtIndex(i).GetFunctionName() or '' for i in range(1, thread.GetNumFrames())) + name += '\t' + next((f for f in frames if f and not allocator.search(f) and not re.match(r' function` name. `call_profile` gives them one row, because the instructions they retire depend on the heap's state. _ALLOCATOR = re.compile(r'malloc|^_*free|mfm|xzm|szone|nanov2|__rust_(?:alloc|dealloc|realloc|no_alloc)') +@delegates(environment) def call_profile( case, # A BPL expression, as `instructions` takes it, such as `v_4ₓ₄` or `{⍵}⍣N 0` function, # A regex that the demangled name of the function to count matches, such as `binder::derived`. A function that is always inlined has no code of its own and never stops the process setup='', # BPL that defines what the case uses, run before it base:str=None, # The build to compare with, resolved as `build` is, such as the one installed before a change; None: count one build - build:str=None, # The build to count, resolved once (`worker.resolved`): a path to a `bpl`, a prefix of the id of a build that `cargo develop` kept, or None for the installed build, + build:str=None, # The build to count, resolved once (`worker.resolved`): a path to a `bpl`, a prefix of the id of a build that `cargo develop` kept, or None or `installed` for the installed build, skip=3, # Calls of `function` to let run first, while the case warms up force=False, # Compute each step's result in full, as `instructions`' `force` does - rows=20 # How many functions to show + timeout:float=LIMIT, # Seconds of work that the counts get, or a `Limit` to share (`worker.Limit`). `lldb` steps about 450 instructions a second, so a call of more than about 13,000 needs a larger one; None: no limit + replays=False, # Count loops as real runs make them, as `instructions`' `replays` does + allocations=False, # Split the allocator's row by the first function outside the allocator and Rust's standard library that called it + rows=20, # How many functions to show + **kwargs ): - "Every instruction that one call of `function` retires, with the calls it makes, counted by function, on macOS on Apple silicon. `lldb` runs `case` once per step in a fresh process, lets `skip` calls of `function` run, and single-steps the next one to its return. The counts are exact, where `profile`'s estimates come from time, but they cover one call. At the 450 or so instructions a second that `lldb` steps, the largest call it counts, 100,000 instructions, takes about four minutes. The allocator's functions, with the functions they call, share the row `allocator`, which the second total leaves out." + "Every instruction that one call of `function` retires, with the calls it makes, counted by function, on macOS on Apple silicon. `lldb` runs `case` once per step in a fresh process, lets `skip` calls of `function` run, and single-steps the next one to its return. The counts are exact, where `profile`'s estimates come from time, but they cover one call. At the 450 or so instructions a second that `lldb` steps, the largest call it counts, 100,000 instructions, takes about four minutes. The allocator's functions, with the functions they call, share the row `allocator`, which the second total leaves out. With `allocations`, they have a row for each function that called the allocator, named `allocator ← function`." if sys.platform != 'darwin' or os.uname().machine != 'arm64': raise ValueError('`call_profile` runs only on macOS on Apple silicon') - code = _program(_stepped(case, force), 1000, setup) - merged = [defaultdict(int) for _ in range(2 if base else 1)] - for b, m in zip([base, build] if base else [build], merged): - for f, n in _counted_call(code, function, skip, b).items(): m['allocator' if _ALLOCATOR.search(f) else f] += n - totals = [('total', *(sum(m.values()) for m in merged)), ('outside the allocator', *(sum(m.values()) - m['allocator'] for m in merged))] + code, timeout = _program(_stepped(case, force), 1000, setup), Limit.of(timeout) + merged, allocated = [defaultdict(int) for _ in range(2 if base else 1)], [0, 0] + for k, (b, m) in enumerate(zip([base, build] if base else [build], merged)): + for f, n in _counted_call(code, function, skip, b, timeout, kwargs, replays=replays).items(): + name, _, site = f.partition('\t') + if _ALLOCATOR.search(name): allocated[k], name = allocated[k] + n, f'allocator ← {site}' if allocations else 'allocator' + m[name] += n + totals = [('total', *(sum(m.values()) for m in merged)), ('outside the allocator', *(sum(m.values()) - a for m, a in zip(merged, allocated)))] fs = sorted(set().union(*merged), key=lambda f: -abs(merged[-1].get(f, 0) - merged[0].get(f, 0)) if base else -merged[0].get(f, 0)) return FunctionCounts([(f, *(m.get(f, 0) for m in merged)) for f in fs[:rows]], totals) @@ -574,7 +644,7 @@ def listing(*flags): return [_NM_LINE.match(line) for line in subprocess.run(['n def disasm( pattern, # A regex that the function's demangled name matches, such as `reduce::sliding::` x86=False, # Read the x86-64 code that CI and Linux run, in place of this Mac's - build:str=None, # The build to read, resolved once (`worker.resolved`): a path to a `bpl`, a prefix of the id of a build that `cargo develop` kept, or None for the installed build. With `x86`, unused + build:str=None, # The build to read, resolved once (`worker.resolved`): a path to a `bpl`, a prefix of the id of a build that `cargo develop` kept, or None or `installed` for the installed build. With `x86`, unused ): "The machine code of the function whose demangled name matches `pattern`, as `objdump` shows it, with names demangled. Where functions of more than one name match, it gives their names. With `x86`, it first builds basedpl's library and the rustymath library it links for Linux x86-64, as CI's runner compiles them, holding the timing lock shared. Where basedpl doesn't compile, it reads rustymath's library alone and says so first. After a change, that build takes minutes." files, note = _x86_libraries() if x86 else ([resolved(build)], '') @@ -595,10 +665,11 @@ def jit_disasm( case, # BPL whose compiled kernels to show, such as `+/ux>127ₓ₁` setup:str='', # BPL that defines what the case uses, run before it prefs:str='', # BPL that runs first, such as `•prefs ["kinds":"static"]` - build:str=None, # The build to run, resolved once (`worker.resolved`): a path to a `bpl`, a prefix of the id of a build that `cargo develop` kept, or None for the installed build + build:str=None, # The build to run, resolved once (`worker.resolved`): a path to a `bpl`, a prefix of the id of a build that `cargo develop` kept, or None or `installed` for the installed build + timeout:float=LIMIT, # Seconds that the run gets, or a `Limit` to share (`worker.Limit`); None: no limit ): - "The machine code of each kernel that the JIT compiles while `prefs`, `setup` and `case` run, after a line that describes the kernel, as `BASEDPL_JIT_DISASM` prints them. A stage compiles on its first run only from about 3 million items (`COMPILES_AT`)." - p = subprocess.run([resolved(build), '-e', '\n'.join(c for c in (prefs, setup, case) if c)], capture_output=True, text=True, env={**os.environ, 'BASEDPL_JIT_DISASM': '1'}) + "The machine code of each kernel that the JIT compiles while `prefs`, `setup` and `case` run, after a line that describes the kernel, as `BASEDPL_JIT_DISASM` prints them. A stage compiles on its first run where that run repays the compile (`Blocks::repays`), from about 2.7 million items for two elementwise steps, and a recording's stages compile when it's reused." + p = bounded([resolved(build), '-e', '\n'.join(c for c in (prefs, setup, case) if c)], Limit.of(timeout).left(), env={**os.environ, 'BASEDPL_JIT_DISASM': '1'}) if p.returncode: raise ValueError(p.stderr) return Report(p.stderr) @@ -630,11 +701,12 @@ def outside(self): return [row for row in self if _mark(*row[1:])] def instruction_budgets( calls:str=None, # Text in `instructions.txt`'s format; None: `tests/instructions.txt` - build:str=None, # The build to count, resolved once (`worker.resolved`): a path to a `bpl`, a prefix of the id of a build that `cargo develop` kept, or None for the installed build + build:str=None, # The build to count, resolved once (`worker.resolved`): a path to a `bpl`, a prefix of the id of a build that `cargo develop` kept, or None or `installed` for the installed build + timeout:float=LIMIT, # Seconds of work that the counts get, or a `Limit` to share (`worker.Limit`); None: no limit ): "Count each case in `calls` with `instructions`, beside its count." setup, cases = _calls(_CALLS.read_text() if calls is None else calls) - counted = instructions([code for code, _ in cases], setup, build=build) + counted = instructions([code for code, _ in cases], setup, build=build, timeout=timeout) return InstructionBudgets((code, n, count) for (code, n), (_, count) in zip(counted, cases)) def _or_dash(x, spec): return '-' if x is None else format(x, spec) @@ -667,28 +739,32 @@ def _wasm_env(wasm): "The environment in which `tests/wasm-pkg.mjs` loads the browser build that `cargo wasm --out` wrote to `wasm`." return os.environ | dict(BASEDPL_WASM=str(Path(wasm).expanduser())) +@delegates(environment) def speed_costs( wasm:Path=None, # A directory that `cargo wasm --out` wrote; None: a fresh native worker - native=False, # Time Rustymath's own linear algebra instead of Accelerate - build:str=None, # The build to time, resolved once (`worker.resolved`): a path to a `bpl`, a prefix of the id of a build that `cargo develop` kept, or None for the installed build, + build:str=None, # The build to time, resolved once (`worker.resolved`): a path to a `bpl`, a prefix of the id of a build that `cargo develop` kept, or None or `installed` for the installed build, speed:str=None, # One BPL speed file's text; None: tests/speed/*.bpl prefs='', # BPL run before each section's preferences and setup - timeout:float=None, # Seconds before timing stops - cases:Annotated[str, {'nargs':'*', 'opt':False}]=None # Codes or file-qualified labels to time; None: all + timeout:float=LIMIT, # Seconds of work that the timing gets, or a `Limit` to share (`worker.Limit`), counted from when it holds the timing lock; None: no limit + cases:Annotated[str, {'nargs':'*', 'opt':False}]=None, # Codes or file-qualified labels to time; None: all + **kwargs ): "Time the BPL speed records, with their budgets, through the shared native/browser runner." files = [dict(name=p.name, text=p.read_text()) for p in sorted(_SPEED.glob('*.bpl'))] if speed is None else [dict(name='', text=speed)] - cases = listify(cases) or None + cases, limit = listify(cases) or None, Limit.of(timeout) if wasm is None: - with Worker(build, native) as w: r = w.request(dict(costs=dict(files=files, cases=cases, prefs=prefs)), timeout) - if e := r['error']: raise ValueError(e['message']) + with Worker(build, **kwargs) as w: + r = w.request(dict(costs=dict(files=files, cases=cases, prefs=prefs)), limit.left()) + limit.spend(w.ran) + if e := r['error']: + limit.reached(e) + raise ValueError(e['message']) rows = r['costs'] else: script = (f'import {{ costs }} from {json.dumps(_PKG.as_uri())}; import {{ readFileSync }} from "node:fs"; ' f'process.stdout.write(JSON.stringify(costs(JSON.parse(readFileSync(0, "utf8")), {json.dumps(cases)}, {json.dumps(prefs)})))') - with timing_lock(exclusive=True): - p = subprocess.run([_node(), '--no-liftoff', '--input-type=module', '-e', script], input=json.dumps(files), capture_output=True, - text=True, timeout=timeout, env=_wasm_env(wasm)) + with timing_lock(exclusive=True), limit.running(): + p = bounded([_node(), '--no-liftoff', '--input-type=module', '-e', script], limit.left(), input=json.dumps(files), env=_wasm_env(wasm)) if p.returncode: raise ValueError(p.stderr) if p.stderr: sys.stderr.write(p.stderr) rows = json.loads(p.stdout) @@ -751,48 +827,194 @@ def over(row): return _over(row[3], row[4]) changed = [r for r in rows if over(r) or moved(r) > CHANGE] + [r for r in only if over(r)] return CostChanges(sorted(changed, key=lambda r: (not over(r), -moved(r))) + [r for r in only if not over(r)]) -def _run_costs(cmd, path, timeout, native=False, env=None): - "The rows that `logged_costs` reads from what `cmd` prints, run in the package at `path` with `_COSTS` set. A case over its budget still gives its row." - path = Path(path).expanduser() - name = tomllib.loads((path/'Cargo.toml').read_text())['package']['name'] - p = subprocess.run(cmd, cwd=path, capture_output=True, text=True, timeout=timeout, env=os.environ | {f'{name.upper()}_COSTS': '1'} | (NATIVE if native else {}) | (env or {})) - costs = logged_costs(p.stdout + p.stderr, _platform()) - if p.returncode and not costs: raise ValueError(p.stdout + p.stderr) +class Bisection(list): + "The builds that `bisect` measured, oldest first, as `(build, verdict, measure)` rows, where `build` is a `store.builds` record and `verdict` is `good` or `bad`. `before` and `after` are the adjacent builds where the verdict changes, or None where the endpoints don't differ. `edits` holds the journal's records of edits to `after`'s input files, from the build before `before` up to `after`, or None where the process has no journal (`EXHASH_JOURNAL`), as a shell has none." + def __init__(self, rows=(), before=None, after=None, edits=()): + super().__init__(rows) + self.before, self.after, self.edits = before, after, edits + def __repr__(self): + def at(t, spec='%m-%d %H:%M:%S'): return time.strftime(spec, time.localtime(t)) + def names(files): return ', '.join(sorted(Path(f).name for f in files)) or 'none' + def shown(m): return f'{m:,.0f}' if isinstance(m, float) else str(m).replace('\n', ' ¶ ')[:120] + verdict = {b['id']: v for b, v, _ in self} + rows = [f" {b['id'][:8]} {at(b['time'])} {v:4} {shown(m)}" for b, v, m in self] + if self.after is None: return '\n'.join(['no change between the endpoints:', *rows]) + b, a = self.before, self.after + lines = [f"first {verdict[a['id']]} build {a['id'][:8]} ({at(a['time'])}), after {verdict[b['id']]} build {b['id'][:8]} ({at(b['time'])})", + f" inputs changed: {names(a['changes'] or [])}"] + lines += [f" {x['id'][:8]} inputs possibly modified since build started: {names(x['edited'])}" for x in (b, a) if x['edited']] + lines.append(' no journal in this process: EXHASH_JOURNAL is unset' if self.edits is None else ' journal edits to its inputs:' if self.edits else ' no journal edits to its inputs') + lines += [f" {at(e['time'], '%H:%M:%S')} {(e['author'] or '-')[:8]:8} {os.path.relpath(e['path'], _ROOT.parent)} +{e['added']} -{e['removed']}" for e in self.edits or ()] + return '\n'.join([*lines, ' measured:', *rows]) + +def bisect( + case, # What to measure on each build: a BPL expression, or for `by='check'` a reference case's id or header line in `path` + good:str, # A kept build where `case` passes: a prefix of its id, as `store.builds` lists them + bad:str=None, # A kept build where it fails, older or newer than `good`: a prefix of its id; None or `installed`: the installed build + by:Annotated[str, {'choices':['check', 'value', 'instructions', 'time']}]='value', # How a build is judged: `check` (the reference case passes), `value` (the expression prints what it prints on `good`), `instructions` or `time` (its cost is nearer `good`'s than `bad`'s) + setup:str='', # BPL that defines what `case` uses, run before it, for `value`, `instructions` and `time` + prefs:str='', # BPL that runs first in each worker, such as `•prefs ["kinds":"static"]`, for `value` and `time` + path:Path=_ROOT/'tests'/'reference'/'core.bpl', # The reference file, for `check` + timeout:float=LIMIT, # Seconds of work that every measurement together gets, or a `Limit` to share (`worker.Limit`); None: no limit +): + "Find the adjacent kept builds between `good` and `bad` where `case`'s verdict changes. Each step measures, in fresh workers, the build halfway between the nearest good and bad builds so far. The result names both builds, the input files that changed between them, the edits to those files that every session's journal recorded (the directories beside `EXHASH_JOURNAL`) and each measurement." + kept = store.builds(_ROOT, None) + def index(prefix): + found = [i for i, b in enumerate(kept) if b['id'].startswith(prefix)] + if len(found) != 1: raise ValueError(f'{len(found)} kept builds have ids starting {prefix!r}') + return found[0] + limit = Limit.of(timeout) + def printed(b): + try: return fresh(f'{setup} ⋄ {case}' if setup else case, prefs=prefs, build=b, timeout=limit) + except (BplError, EOFError, TimeoutError) as e: + limit.left() + return f'{type(e).__name__}: {e}' + measure = dict(check=lambda b: '; '.join(f"{f['status']}: {f['message']}" for f in check_file(path, [case], build=b, timeout=limit)), + value=printed, + instructions=lambda b: instructions(case, setup, build=b, timeout=limit)[0][1], + time=lambda b: time_cases(case, setup or None, prefs, build=b, timeout=limit)[0][1])[by] + g, b = index(good), index(kept.installed if bad in (None, 'installed') else bad) + measured = {g: measure(kept[g]['id']), b: measure(kept[b]['id'])} + is_bad = dict(check=bool, value=lambda m: m != measured[g]).get(by, lambda m: abs(m - measured[b]) < abs(m - measured[g])) + lo, hi = sorted((g, b)) + found = not is_bad(measured[g]) and is_bad(measured[b]) + while found and hi - lo > 1: + mid = (lo + hi) // 2 + measured[mid] = measure(kept[mid]['id']) + if is_bad(measured[mid]) == is_bad(measured[max(g, b)]): hi = mid + else: lo = mid + edits = None + if found and (log := os.environ.get('EXHASH_JOURNAL')): + from exhash import journal + sessions = [d for d in Path(log).parent.iterdir() if d.is_dir()] + edits = sorted((e for d in sessions for e in journal(d, since=kept[max(lo-1, 0)]['time']) if e['time'] <= kept[hi]['time'] and e['path'] in kept[hi]['inputs']), key=lambda e: e['time']) + rows = [(kept[i], 'bad' if is_bad(m) else 'good', m) for i, m in sorted(measured.items())] + return Bisection(rows, kept[lo] if found else None, kept[hi] if found else None, edits) + +_LONG = ('listing', 'site') # Columns that `ExplainDiff` shows on lines of their own and never pairs rows by +_VARIES = ('site', 'microseconds') # Columns that vary between runs of one build, which a `~` row never reports + +class ExplainDiff(list): + "The rows that `explain_diff` reports, as `(record, sign, row, moved)` rows in `•explain`'s order of records. `sign` is `-` for a row that only `base` gave, `+` for a row that only `build` gave, and `~` for a row that both gave with different counts or listings. `row` maps each column to the row's value, from `build` except in a `-` row. `moved` maps each column that differs in a `~` row to its values in `base` and `build`. A row's `site`, where `•explain` filled it, and the `listing` of a program that only one build gave show on lines below the row; a moved listing shows as a diff." + def __repr__(self): + def cell(k, v): return f'{k} {v!r}' if isinstance(v, str) else f'{k} {v}' + lines = [] + for record, sign, row, moved in self: + cells = [f'{k} {moved[k][0]!r}→{moved[k][1]!r}' if k in moved else cell(k, v) for k, v in row.items() if k not in _LONG and (v != '' or k in moved)] + lines.append(f'{sign} {record:13} {", ".join(cells)}') + if 'listing' in moved: lines += [' ' + l for l in list(difflib.unified_diff(*(m.splitlines() for m in moved['listing']), lineterm='', n=1))[2:]] + elif sign != '~' and row.get('listing'): lines += [' ' + l for l in row['listing'].splitlines()] + if row.get('site'): lines.append(f' site {row["site"]}') + return '\n'.join(lines) or 'no difference' + +def _explained(case, setup, prefs, build, sites, limit): + "`•explain`'s records of `case` on `build`, as a route check captures them (the worker's `route` request), spending `limit`: each record's name to its rows, each row a dict from column to value. A build older than that request runs `case` as `•explain` text instead (`_explained_by_text`)." + with Worker(resolved(build)) as w: + r = w.request(dict(route=dict(code=case, setup=setup, prefs=prefs, sites=sites)), limit.left()) + limit.spend(w.ran) + if (r['error'] or {}).get('kind') == 'REQUEST': r = _explained_by_text(w, case, setup, prefs, sites, limit) + limit.reached(r['error']) + if r['error']: raise BplError(r['error'], r['output']) + return {name: [{k: v.item() if hasattr(v, 'item') else v for k, v in zip(t, vals)} for vals in zip(*t.values())] for name, t in _value(r['value']).items()} + +def _explained_by_text(w, case, setup, prefs, sites, limit): + "The reply of `w`, a build older than the worker's `route` request, to `•explain` of a second run of `case`, after `prefs`, `setup` and an explained first run, each evaluation spending `limit`. A build older than `•explain`'s options runs without `sites`." + def run(code): + r = w.eval(code, limit.left()) + limit.spend(w.ran) + return r + text, opts = '"' + case.replace('"', '""') + '"', '["sites":$t]' if sites else '' + for code in filter(None, [prefs, setup, f'(•explain {text}) ⋄ 0']): + if (r := run(code))['error']: return r + r = run(f'1↓{opts}•explain {text}') + return run(f'1↓•explain {text}') if r['error'] and opts else r + +def _paired(old, new): + "One record's `ExplainDiff` rows, as `(sign, row, moved)`: rows of `old` and `new` paired first by every column but `_LONG`'s, then by their text columns alone." + out, old, new = [], list(old), list(new) + cols = [k for k in new[0] if k in old[0]] if old and new else [] + for text_only in (False, True): + def key(r): return [(k, r[k]) for k in cols if k not in _LONG and (isinstance(r[k], str) or not text_only)] + for b in list(new): + a = next((a for a in old if key(a) == key(b)), None) + if a is None: continue + old.remove(a); new.remove(b) + if moved := {k: (a[k], b[k]) for k in cols if k not in _VARIES and a[k] != b[k]}: out.append(('~', b, moved)) + return [('-', a, {}) for a in old] + [('+', b, {}) for b in new] + out + +def explain_diff( + case, # BPL code that `•explain` runs, such as `{+/⍵×⍵}∂ v` + base:str, # The build to compare with, resolved as `build` is, such as the one installed before a change + setup='', # BPL that defines what the case uses, run first in each worker + prefs='', # BPL that runs before `setup`, such as `•prefs ["kinds":"static"]` + build:str=None, # The build to compare with `base`, resolved once (`worker.resolved`): a path to a `bpl`, a prefix of the id of a build that `cargo develop` kept, or None or `installed` for the installed build + sites=False, # Fill each `forced` and `stopped` row's `site` column, as `•explain`'s `"sites"` option does, on builds that have the option + timeout:float=LIMIT # Seconds of work that both captures get, or a `Limit` to share (`worker.Limit`); None: no limit +): + "How `•explain`'s records of `case` differ between two builds: each row that only one build gave, and each row that both gave whose counts or program listing moved. Each build captures `case` as the route test does (the worker's `route` request), in a fresh worker: `prefs` and `setup` run, then an explained warm-up run, then the run whose records it compares." + limit = Limit.of(timeout) + old, new = (_explained(case, setup, prefs, b, sites, limit) for b in (base, build)) + return ExplainDiff((name, *r) for name in [*new, *(n for n in old if n not in new)] for r in _paired(old.get(name, []), new.get(name, []))) + +def _cost_env(path, **kwargs): + name = tomllib.loads((Path(path).expanduser()/'Cargo.toml').read_text())['package']['name'] + return environment(**kwargs) | {f'{name.upper()}_COSTS': '1'} + +def _cost_rows(text, returncode): + costs = logged_costs(text, _platform()) + if returncode and not costs: raise ValueError(text) return costs +def _run_costs(cmd, path, timeout, env=None, **kwargs): + "The rows that `logged_costs` reads from what `cmd` prints, run in the package at `path` with `_COSTS` set, `env`'s variables and the settings that `worker.environment` takes. A case over its budget still gives its row." + path = Path(path).expanduser() + limit = Limit.of(timeout) + with limit.running(): p = bounded(cmd, limit.left(), cwd=path, env=_cost_env(path, **kwargs) | (env or {})) + return _cost_rows(p.stdout + p.stderr, p.returncode) + +@delegates(environment) def cargo_costs( path='.', # A Cargo package, such as basedpl or rustymath, whose `tests/speed.rs` prints every cost when `_COSTS` is set build:str=None, # A copy of the speed test's executable to run in place of the one that `cargo test` builds, such as one saved before a change - native=False, # Time rustymath's own linear algebra in place of Accelerate, as `Worker`'s `native` does - timeout:float=None # Seconds before the run stops + timeout:float=LIMIT, # Seconds of work that the run gets, or a `Limit` to share (`worker.Limit`); None: no limit + **kwargs ): - "The rows that `logged_costs` reads from what the speed test of the package at `path` prints, run as `cargo test` runs it. A case over its budget still gives its row." - return _run_costs([Path(build).expanduser()] if build else ['cargo', 'test', '--test', 'speed'], path, timeout, native) + "The speed test's logged cost rows. Build once outside the execution budget, then run that binary directly; `build` runs a saved executable instead. Time inside the binary, including lock waits, spends `timeout`. A case over its budget still gives its row." + if build: return _run_costs([Path(build).expanduser()], path, timeout, **kwargs) + limit = Limit.of(timeout) + left = limit.left() + try: result = run_tests(path, '--test', 'speed', timeout=left, env=_cost_env(path, **kwargs)) + except TimeoutError: + limit.spend(left) + raise + limit.spend(result.seconds) + return _cost_rows(result.text, result.returncode) def _python_path(path, build): "The environment variables that put `build`'s Python package before the installed one on `PYTHONPATH`, for the package at `path`, or none where `build` is None. `build` is a directory that holds a copy of the package, or a prefix of the id of a build that `cargo develop` kept (`fastws.store`)." - if build is None: return {} + if _installed(build): return {} d = Path(build).expanduser() if not d.exists(): d = store.resolve(Path(path).expanduser(), str(build))/'python' return dict(PYTHONPATH=os.pathsep.join(filter(None, [str(d), os.environ.get('PYTHONPATH')]))) def pytest_costs( path='.', # A package, such as rustymath, whose `tests/test_speed.py` prints every cost when `_COSTS` is set - build:str=None, # The build of the package to time: a prefix of the id of a build that `cargo develop` kept (`fastws.store`), or a directory that holds a copy of its Python package; None: the installed one + build:str=None, # The build of the package to time: a prefix of the id of a build that `cargo develop` kept (`fastws.store`), or a directory that holds a copy of its Python package; None or `installed`: the installed one native=False, # Time rustymath's own linear algebra in place of Accelerate, as `Worker`'s `native` does - timeout:float=None # Seconds before the run stops + timeout:float=LIMIT # Seconds of work that the run gets, or a `Limit` to share (`worker.Limit`); None: no limit ): "The rows that `logged_costs` reads from what the Python speed test of the package at `path` prints, run with `pytest`. A case over its budget still gives its row." - return _run_costs([sys.executable, '-m', 'pytest', '-q', 'tests/test_speed.py'], path, timeout, native, _python_path(path, build)) + return _run_costs([sys.executable, '-m', 'pytest', '-q', 'tests/test_speed.py'], path, timeout, env=_python_path(path, build), native=native) def run_pytest( tests:Annotated[str, {'nargs':'+'}], # A test's `pytest` node ID, such as `tests/test_structure.py::test_edge_modes_pad_take_and_windows`, or a list of them path='.', # A package, such as rustymath, whose tests the node IDs name - build:str=None, # The build of the package to test: a prefix of the id of a build that `cargo develop` kept (`fastws.store`), or a directory that holds a copy of its Python package; None: the installed one - timeout:float=None # Seconds before the run stops + build:str=None, # The build of the package to test: a prefix of the id of a build that `cargo develop` kept (`fastws.store`), or a directory that holds a copy of its Python package; None or `installed`: the installed one + timeout:float=LIMIT # Seconds of work that the run gets, or a `Limit` to share (`worker.Limit`); None: no limit ): "What `pytest -q` prints for `tests`, run in the package at `path`." - p = subprocess.run([sys.executable, '-m', 'pytest', '-q', *listify(tests)], cwd=Path(path).expanduser(), capture_output=True, text=True, timeout=timeout, env=os.environ | _python_path(path, build)) + limit = Limit.of(timeout) + with limit.running(): p = bounded([sys.executable, '-m', 'pytest', '-q', *listify(tests)], limit.left(), cwd=Path(path).expanduser(), env=os.environ | _python_path(path, build)) return Report(p.stdout + p.stderr) class Report(str): @@ -802,13 +1024,12 @@ def __repr__(self): return str(self) def wasm_test( wasm, # A directory that `cargo wasm --out` wrote speed=False, # Run `tests/wasm-speed.mjs`, with V8's `--no-liftoff` and `BASEDPL_COSTS` as CI runs it, in place of `tests/wasm.mjs`? Its report then lists every case's cost, which `logged_costs` reads - timeout:float=None # Seconds before the run stops + timeout:float=LIMIT # Seconds of work that the run gets, or a `Limit` to share (`worker.Limit`), counted while it holds the timing lock; None: no limit ): "Run `tests/wasm.mjs` or `tests/wasm-speed.mjs` under `node --test` against the browser build in `wasm`, as CI runs them, and give the runner's report. Only the `node` process gets `BASEDPL_WASM` and `BASEDPL_COSTS`. It holds the machine's timing lock, exclusively for the speed test and shared for the others. A failing test raises `ValueError` with the report, starting on a line of its own so that `logged_costs` reads every row of it." - test = _SPEED.with_name('wasm-speed.mjs' if speed else 'wasm.mjs') - with timing_lock(exclusive=speed): - p = subprocess.run([_node(), *(['--no-liftoff'] if speed else []), '--test', str(test)], cwd=_ROOT, capture_output=True, - text=True, timeout=timeout, env=_wasm_env(wasm) | (dict(BASEDPL_COSTS='1') if speed else {})) + test, limit = _SPEED.with_name('wasm-speed.mjs' if speed else 'wasm.mjs'), Limit.of(timeout) + with timing_lock(exclusive=speed), limit.running(): + p = bounded([_node(), *(['--no-liftoff'] if speed else []), '--test', str(test)], limit.left(), cwd=_ROOT, env=_wasm_env(wasm) | (dict(BASEDPL_COSTS='1') if speed else {})) report = Report(p.stdout + p.stderr) if p.returncode: raise ValueError(f'\n{report}') return report @@ -819,28 +1040,28 @@ def __repr__(self): return '\n'.join(f'{per:6.2f} KB a level {base:7.1f} KB base def wasm_stack( wasm, # A directory that `cargo wasm --out` wrote + timeout:float=LIMIT, # Seconds of work that every trial together gets, or a `Limit` to share (`worker.Limit`), counted while the trials run; None: no limit ): - "The worker stack, in KB, that each recursion shape of `tests/wasm.mjs` takes per level and below its first level, under V8's TurboFan alone (`--no-liftoff`). The browser's depth limit, `MAX_CALL_DEPTH` in `src/eval.rs`, is set from these. For each shape, it bisects to 1 KB the least stack at which the shape's recursion reaches the test's depth, and half that depth. Each trial runs `tests/wasm.mjs depths` in a fresh `node` process. Its worker compiles the module, as the test's worker does. The bisections run at once, one for each core, with the machine's timing lock held shared." - node, test, env, most = _node(), _SPEED.with_name('wasm.mjs'), _wasm_env(wasm), 2048 + "The worker stack, in KB, that each recursion shape of `tests/wasm.mjs` takes per level and below its first level, under V8's TurboFan alone (`--no-liftoff`). The browser's depth limit, `MAX_CALL_DEPTH` in `src/eval.rs`, is set from these. For each shape, it bisects to 1 KB the least stack at which the shape's recursion reaches the test's depth, and half that depth. Each trial runs `tests/wasm.mjs depths` in a fresh `node` process. Its worker compiles the module, as the test's worker does. The bisections run at once, one for each core, in rounds that each run one trial for every unfinished bisection as one batch (`_locked`)." + node, test, env, most, limit = _node(), _SPEED.with_name('wasm.mjs'), _wasm_env(wasm), 2048, Limit.of(timeout) def depths(kb, *shape): "What `tests/wasm.mjs depths` gives with `kb` KB of worker stack, for every shape or for `shape`, a name and a depth: each shape's depth, the depth it reached, and what deeper recursion gave. `None` where the stack is too small for the worker to load the module." - p = subprocess.run([node, '--no-liftoff', test, 'depths', str(kb), *map(str, shape)], cwd=_ROOT, capture_output=True, text=True, env=env) - if p.returncode and 'RangeError: Maximum call stack size exceeded' in p.stderr: return None + p = bounded([node, '--no-liftoff', test, 'depths', str(kb), *map(str, shape)], limit.left(), cwd=_ROOT, env=env) + # A worker whose stack is too small to load the module overflows before it posts results. Node then exits with a RangeError, printed as `RangeError [Error]` where an internal module overflowed, or exits 0 with no output. + if 'Maximum call stack size exceeded' in p.stderr or not (p.returncode or p.stdout.strip()): return None if p.returncode: raise ValueError(p.stderr) return json.loads(p.stdout) - def least(job): - "The least worker stack, in KB, at which shape `name` reaches depth `n`, searched from the 192 KB that Node keeps in reserve." - (name, n), lo, hi = job, 192, most - while hi - lo > 1: - mid = (lo+hi)//2 - if (got := depths(mid, name, n)) and got[name][1] == n: hi = mid - else: lo = mid - return hi - with timing_lock(): - full = depths(most) - if short := [name for name, (n, reached, *_) in full.items() if reached != n]: raise ValueError(f'With {most} KB of stack, {", ".join(short)} fall short of their depths') - jobs = [(name, d) for name, (n, *_) in full.items() for d in (n//2, n)] - with ThreadPoolExecutor(os.cpu_count()) as pool: kb = dict(zip(jobs, pool.map(least, jobs))) + with timing_lock(), limit.running(): full = depths(most) + if short := [name for name, (n, reached, *_) in full.items() if reached != n]: raise ValueError(f'With {most} KB of stack, {", ".join(short)} fall short of their depths') + jobs = [(name, d) for name, (n, *_) in full.items() for d in (n//2, n)] + # Each bisection searches for the least worker stack at which its shape reaches its depth, from the 192 KB that Node keeps in reserve. + bounds = {job: (192, most) for job in jobs} + with ThreadPoolExecutor(os.cpu_count()) as pool: + while trials := {job: (lo+hi)//2 for job, (lo, hi) in bounds.items() if hi - lo > 1}: + for (job, mid), got in zip(trials.items(), _locked(pool, [(depths, mid, *job) for job, mid in trials.items()], limit)): + lo, hi = bounds[job] + bounds[job] = (lo, mid) if got and got[job[0]][1] == job[1] else (mid, hi) + kb = {job: hi for job, (lo, hi) in bounds.items()} per = {name: (kb[name, n] - kb[name, n//2])/(n - n//2) for name, (n, *_) in full.items()} return StackUse((name, per[name], kb[name, n] - per[name]*n) for name, (n, *_) in full.items()) @@ -956,35 +1177,39 @@ def _preflight(): return notes, problems, node def _run(steps, logs, jobs, keep_going): - "Run `steps`, each once the steps it reads have passed, writing each step's output to a log in `logs`. At most `jobs` steps run at once, or any number for `None`, and each timing step runs alone after the rest. Gives each step's `(name, status, seconds, log)` row. Unless `keep_going`, a failure ends the running steps and starts no more." + "Run `steps`, each once its inputs passed, logging output in `logs`. At most `jobs` run at once (None: no limit); timing steps run alone after the rest. Give `(name, status, seconds, log)` rows. Own each step's process group unless already in a managed job. Clean owned groups on every exit, even after a leader exits. A nested failure ends the containing job; otherwise `keep_going` permits independent steps after a failure." if unknown := {a for s in steps for a in s.after} - {s.name for s in steps}: raise ValueError(f'No step is named {", ".join(sorted(unknown))}') rows, running, pending = {}, {}, list(steps) + owner = not os.environ.get(JOB_ENV) def end(name, status): p, f, start, _ = running.pop(name) - if p.poll() is None: - os.killpg(p.pid, signal.SIGTERM) - try: p.wait(10) - except subprocess.TimeoutExpired: os.killpg(p.pid, signal.SIGKILL); p.wait() + if owner: kill_group(p) + elif p.poll() is None: p.kill(); p.wait() f.close() rows[name] = (name, status, time.monotonic()-start, f.name) - while pending or running: - for name, (p, _, start, s) in list(running.items()): - if p.poll() is not None: end(name, 'failed' if p.returncode else 'ok') - elif time.monotonic()-start > s.timeout: end(name, 'timed out') - if not keep_going and any(r[1] != 'ok' for r in rows.values()): - for name in list(running): end(name, 'stopped') - break - for s in [s for s in pending if any(rows[a][1] != 'ok' for a in s.after if a in rows)]: - rows[s.name] = (s.name, 'not run', 0, '') - pending.remove(s) - for s in list(pending): - if not all(a in rows for a in s.after) or (s.timing and (running or any(not t.timing for t in pending))): continue - if not s.timing and jobs and len(running) >= jobs: continue - f = open(logs/f"{re.sub(r'\W+', '-', s.name).strip('-')}.log", 'w') - running[s.name] = (subprocess.Popen(s.cmd, cwd=s.cwd, env=os.environ | s.env, stdout=f, stderr=subprocess.STDOUT, start_new_session=True), f, time.monotonic(), s) - pending.remove(s) - if not running: break - time.sleep(0.5) + try: + while pending or running: + for name, (p, _, start, s) in list(running.items()): + if p.poll() is not None: end(name, 'failed' if p.returncode else 'ok') + elif time.monotonic()-start > s.timeout: end(name, 'timed out') + failed = [r for r in rows.values() if r[1] != 'ok'] + if failed and not owner: raise RuntimeError(f'{failed[0][0]} {failed[0][1]}; stopping the containing job') + if failed and not keep_going: break + for s in [s for s in pending if any(rows[a][1] != 'ok' for a in s.after if a in rows)]: + rows[s.name] = (s.name, 'not run', 0, '') + pending.remove(s) + for s in list(pending): + if not all(a in rows for a in s.after) or (s.timing and (running or any(not t.timing for t in pending))): continue + if not s.timing and jobs and len(running) >= jobs: continue + f = open(logs/f"{re.sub(r'\W+', '-', s.name).strip('-')}.log", 'w') + try: p = subprocess.Popen(s.cmd, cwd=s.cwd, env=os.environ | s.env | {JOB_ENV: '1'}, stdout=f, stderr=subprocess.STDOUT, start_new_session=owner) + except BaseException: f.close(); raise + running[s.name] = (p, f, time.monotonic(), s) + pending.remove(s) + if not running: break + time.sleep(0.5) + finally: + for name in list(running): end(name, 'stopped') rows |= {s.name: (s.name, 'not run', 0, '') for s in pending} return [rows[s.name] for s in steps] @@ -1018,8 +1243,145 @@ def prerelease( raise ValueError(f'\n{result!r}' + (f'\n\nThe end of the log of {name}, {log}:\n' + '\n'.join(Path(log).read_text().splitlines()[-40:]) if log else '')) return result +_ENGINE = 'graph fusion plan pervasive agreement program jit array number element error settings explain keyed'.split() +_USE = re.compile(r'\buse\s+(crate::|super::)?([^;]*);') +_RUST_PATH = re.compile(r'\b(crate|super)::((?:\w+::)*\w+)') + +def _use_paths(tree, prefix=()): + "The paths that a Rust `use` tree names, each a tuple of segments, without aliases." + tree = tree.strip() + if tree.startswith('{'): + paths, depth, start = [], 0, 1 + for i, ch in enumerate(tree): + depth += (ch == '{') - (ch == '}') + if (depth == 1 and ch == ',') or depth == 0: + if tree[start:i].strip(): paths += _use_paths(tree[start:i], prefix) + start = i + 1 + return paths + head, brace, rest = tree.partition('::{') + if brace: return _use_paths('{' + rest, prefix + tuple(head.split('::'))) + return [prefix + tuple(re.sub(r'\s+as\s+\w+$', '', tree).split('::'))] + +def module_edges( + engine:Annotated[str, {'nargs':'+'}]=None, # Top-level modules that count as the engine; None: those of meta/crate-split-2026-10-31.md + root:Path=_ROOT, # The crate whose `src` to scan +): + "Count the references from the engine's modules into the crate's other modules, and find the largest cycle of references between top-level modules. A text scan of `crate::` and `super::` paths and `use` trees, with `lib.rs`'s re-exports resolved: it counts paths, not distinct items." + engine, src = set(engine or _ENGINE), Path(root).expanduser()/'src' + texts = {'::'.join(p.relative_to(src).with_suffix('').parts): re.sub(r'(?m)//.*$', '', p.read_text()) for p in src.rglob('*.rs')} + lib = texts.pop('lib') + tops = {m.split('::')[0] for m in texts} + def module(path): return None if not path or path[0] not in tops else '::'.join(path[:2]) if '::'.join(path[:2]) in texts else path[0] + reexported = {p[-1]: m for _, tree in _USE.findall(lib) for p in _use_paths(tree) if (m := module(p))} + refs = defaultdict(lambda: defaultdict(int)) + for m, text in texts.items(): + paths = [(kind, p) for kind, tree in _USE.findall(text) if kind for p in _use_paths(tree)] + paths += [(kind, tuple(path.split('::'))) for kind, path in _RUST_PATH.findall(_USE.sub('', text))] + for kind, path in paths: + if kind.startswith('super'): path = tuple(m.split('::')[:-1]) + path + target = reexported.get(path[0]) or module(path) + if target and target != m: refs[m][target] += 1 + top = lambda m: m.split('::')[0] + graph, lines = defaultdict(set), defaultdict(int) + for m, targets in refs.items(): graph[top(m)] |= {top(t) for t in targets} - {top(m)} + for m, text in texts.items(): lines[top(m)] += len(text.splitlines()) + index, low, stack, cycles = {}, {}, [], [] + def visit(v): + index[v] = low[v] = len(index); stack.append(v) + for w in graph[v]: + if w not in index: visit(w); low[v] = min(low[v], low[w]) + elif w in stack: low[v] = min(low[v], index[w]) + if low[v] == index[v]: + cycle = [stack.pop()] + while cycle[-1] != v: cycle.append(stack.pop()) + cycles.append(cycle) + for v in sorted(tops - index.keys()): visit(v) + cycle = max(cycles, key=len) + into = defaultdict(lambda: defaultdict(int)) + for m, targets in refs.items(): + for t, n in targets.items(): + if top(m) in engine and top(t) not in engine: into[t][m] += n + rows = [f"{sum(n for c in into.values() for n in c.values())} references from {len({m for c in into.values() for m in c})} of the engine's modules into others:"] + for t, c in sorted(into.items(), key=lambda tc: -sum(tc[1].values())): + rows.append(f'{sum(c.values()):5} {t}: ' + ', '.join(f'{m} {n}' for m, n in sorted(c.items(), key=lambda mn: -mn[1]))) + rows.append(f'Largest cycle: {len(cycle)} of {len(tops)} top-level modules, {sum(lines[m] for m in cycle):,} of {sum(lines.values()):,} lines: {" ".join(sorted(cycle))}') + return Report('\n'.join(rows)) -_COMMANDS = '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 wasm_test wasm_stack target_builds prerelease'.split() +_CRATES = {'basedpl': ('src', 'py', 'cli', 'wasm', 'xtask'), 'rustymath': ('src', 'py', 'buffer')} +_SKIPPED_DIRS = ['target', 'links', 'node_modules', '_proc', '_docs', 'meta'] +_FN = re.compile(r'^(\s*)(?:pub(?:\([^)]*\))?\s+)?(?:const\s+)?(?:async\s+)?(?:unsafe\s+)?(?:extern\s+"[^"]*"\s+)?fn\s+(\w+)') +_IMPL = re.compile(r'^(\s*)(?:unsafe\s+)?(impl|trait|pub(?:\([^)]*\))?\s+trait)\b') +# Attributes through which a test runner, Python or JavaScript reaches a function by another route than its name. +_REACHED = re.compile(r'#\[(?:test|bench|pyfunction|pymethods|pymodule|pyclass|wasm_bindgen|no_mangle|export_name|unsafe\((?:no_mangle|export_name))') + +def _sources(base): + "The Rust, Python, JavaScript and TOML files under `base`, outside build output, linked checkouts, notebooks' copies and notes." + for top, dirs, files in os.walk(base): + dirs[:] = [d for d in dirs if d not in _SKIPPED_DIRS and not d.startswith('.')] + yield from (Path(top)/f for f in files if Path(f).suffix in ('.rs', '.py', '.js', '.mjs', '.toml')) + +def _code(line): + "`line` without a trailing `//` comment outside a string." + i = line.find('//') + while i >= 0 and line[:i].count('"') % 2: i = line.find('//', i + 2) + return line if i < 0 else line[:i] + +def _test_lines(lines): + "The numbers, from 1, of `lines` inside an item marked `#[test]` or `#[cfg(test)]`, its attributes included." + marked, marking, inside, depth, opened = set(), False, False, 0, False + for n, line in enumerate(lines, 1): + s, code = line.lstrip(), _code(line) + if not inside: + if s.startswith('#[') and (marking or re.match(r'#\[(?:cfg\()?test\)?\]', s)): marked.add(n); marking = True; continue + if not marking or not s: continue + inside, depth, opened, marking = True, 0, False, False + marked.add(n) + depth += code.count('{') - code.count('}') + opened = opened or '{' in code + if (opened and depth <= 0) or (not opened and code.rstrip().endswith(';')): inside = False + return marked + +def _is_test(path): + "Whether `path` is a test file: under a `tests` directory, or a Python file named `test_*.py`." + return 'tests' in path.parts or path.name.startswith('test_') + +def dead_functions( + root:Path=_ROOT, # basedpl's checkout, with rustymath's beside it + tested:bool=False, # Also report functions that only tests name: references from test files, `#[test]` functions and `#[cfg(test)]` modules don't count +): + "Functions whose name appears nowhere else: each `fn` in basedpl's and rustymath's crates whose name no other place in either repository's Rust, Python, JavaScript or TOML files uses. A text scan by name, so a name shared with anything else counts as used. It leaves out `main`, tests and their helpers, Python's dunder methods, a trait implementation's methods, and functions that Python, JavaScript or the linker reach through an attribute." + roots = {'basedpl': Path(root).expanduser(), 'rustymath': Path(root).expanduser().parent/'rustymath'} + defs, words = [], defaultdict(int) + for repo, base in roots.items(): + for path in _sources(base): + if tested and _is_test(path.relative_to(base)): continue + lines = path.read_text().splitlines() + tests = _test_lines(lines) if path.suffix == '.rs' else set() + counted = [_code(l) for n, l in enumerate(lines, 1) if not (tested and n in tests)] if path.suffix == '.rs' else lines + for w in re.findall(r'\b[A-Za-z_]\w*\b', '\n'.join(counted)): words[w] += 1 + if path.suffix != '.rs' or path.relative_to(base).parts[0] not in _CRATES[repo]: continue + blocks, attrs = [], '' + for n, line in enumerate(lines, 1): + if not line.strip() or n in tests: continue + indent = len(line) - len(line.lstrip()) + while blocks and blocks[-1][0] >= indent and not line.lstrip().startswith(('#', 'where', '+', ')')): blocks.pop() + if line.lstrip().startswith('#['): attrs += line; continue + if m := _IMPL.match(line): + header = ' '.join(lines[n-1:n+4]).split('{')[0] + reached = m[2] == 'impl' and (re.search(r'\sfor\s', header) or _REACHED.search(attrs)) + blocks.append((indent, bool(reached))) + elif (m := _FN.match(line)) and m[2] != 'main' and not m[2].startswith('__') and not _REACHED.search(attrs) and not (blocks and blocks[-1][1]): + defs.append((path.relative_to(base.parent), n, m[2])) + attrs = '' + counts = defaultdict(int) + for _, _, name in defs: counts[name] += 1 + dead = [(p, n, name) for p, n, name in defs if words[name] <= counts[name]] + reach = 'nothing outside tests names' if tested else 'nothing else names' + return Report('\n'.join([f'{len(dead)} function{"s" * (len(dead) != 1)} that {reach}:'] + [f'{p}:{n} {name}' for p, n, name in sorted(dead)])) + + + +_COMMANDS = '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 dead_functions'.split() def main(): "Run a function in `_COMMANDS` from the shell, as `bpl-tests NAME ARGUMENT...`, and print its result. `bpl-tests NAME -h` lists NAME's arguments." diff --git a/python/basedpl/browser/language.js b/python/basedpl/browser/language.js index 18ae1c51..be7e02ab 100644 --- a/python/basedpl/browser/language.js +++ b/python/basedpl/browser/language.js @@ -1,3 +1,3 @@ // Generated by basedpl.editors.browser. -export const grammar = {"name": "bpl", "displayName": "BPL", "scopeName": "source.bpl", "fileTypes": ["bpl"], "patterns": [{"begin": "[⍝]", "beginCaptures": {"0": {"name": "comment.line.bpl"}}, "end": "$", "contentName": "comment.line.bpl", "patterns": [], "applyEndPatternLast": true}, {"begin": "\"", "beginCaptures": {"0": {"name": "string.quoted.double.bpl"}}, "end": "\"|$", "contentName": "string.quoted.double.bpl", "patterns": [{"match": "\"\"", "name": "string.quoted.double.bpl"}], "applyEndPatternLast": true, "endCaptures": {"0": {"name": "string.quoted.double.bpl"}}}, {"match": "'.?'", "name": "string.quoted.single.bpl"}, {"match": "(?:_\\d+|_\\S|_|[^\\s\\p{Cc}0-9←⎕•⍺⍵⍶⍹∇⍢⍝⋄⁝\\?¯∞⍬\\+\\-×÷⌈⌊\\|\\*⍟○π√!∧∨⊼⊽\\~=≠<≤>≥⍳↦:⍴≢≡,⍪⊂⊃⊆∊∪∩⍋⍒↑↓⌽⊖⍉⊤⊥⍎⍕⌷⌹\\#↕⍭⨸⌻⍸⍷⊢⊣¿¨ᵇ⍨↣⍤∘⍠⍥↢\\.⊗⌸⍣⇄⊘@∂⍚⌺/⌿\\\\⍀⁰¹²³⁴⁵⁶⁷⁸⁹⁻ᵀᵘₓ₀₁₂₃₄₅₆₇₈₉₋ₓ₁ₓ₂ₓ₄ₓ₈ₓₕₛₚ\\(\\)\\[\\]\\{\\}'\";\\$_ₑⱼᵣ])+", "name": "variable.other.bpl"}, {"match": "[•][A-Za-z_∆⍙][A-Za-z0-9_∆⍙]*", "name": "support.function.bpl"}, {"match": "(?:[¯]?[∞]|[¯]?(\\d+([.]\\d+)?|[.]\\d+)([eEₑ][¯]?\\d+)?([jJⱼ][¯]?(\\d+([.]\\d+)?|[.]\\d+)([eEₑ][¯]?\\d+)?|[rᵣ][¯]?\\d+)?)(?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ)?|[ₑⱼᵣ]", "name": "constant.numeric.bpl"}, {"match": "[⍬](?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ)?", "name": "constant.numeric.bpl"}, {"match": "\\$[tfn]", "name": "constant.numeric.bpl"}, {"match": "\\$[ad]", "name": "string.quoted.double.bpl"}, {"begin": "[\\])](?=(?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ))", "beginCaptures": {"0": {"name": "source.bpl"}}, "end": "(?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ)|$", "contentName": "constant.numeric.bpl", "patterns": [], "applyEndPatternLast": true, "endCaptures": {"0": {"name": "constant.numeric.bpl"}}}, {"match": "[\\+\\-×÷⌈⌊\\|\\*⍟○π√!∧∨⊼⊽\\~=≠<≤>≥⍳↦:⍴≢≡,⍪⊂⊃⊆∊∪∩⍋⍒↑↓⌽⊖⍉⊤⊥⍎⍕⌷⌹\\#↕⍭⨸⌻⍸⍷⊢⊣¿]", "name": "support.function.bpl"}, {"match": "(?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ)", "name": "keyword.operator.monadic.bpl"}, {"match": "[¨ᵇ⍨⊗⌸⇄∂/⌿\\\\⍀⁰¹²³⁴⁵⁶⁷⁸⁹⁻ᵀᵘₓ]", "name": "keyword.operator.monadic.bpl"}, {"match": "[↣⍤∘⍠⍥↢\\.⍣⊘@⍚⌺]", "name": "keyword.operator.dyadic.bpl"}, {"match": "[⍺⍵⍶⍹∇⍢]", "name": "variable.parameter.bpl"}, {"match": "[←⎕⋄⁝\\?₀₁₂₃₄₅₆₇₈₉₋]", "name": "keyword.control.bpl"}]}; +export const grammar = {"name": "bpl", "displayName": "BPL", "scopeName": "source.bpl", "fileTypes": ["bpl"], "patterns": [{"begin": "[⍝]", "beginCaptures": {"0": {"name": "comment.line.bpl"}}, "end": "$", "contentName": "comment.line.bpl", "patterns": [], "applyEndPatternLast": true}, {"begin": "\"", "beginCaptures": {"0": {"name": "string.quoted.double.bpl"}}, "end": "\"|$", "contentName": "string.quoted.double.bpl", "patterns": [{"match": "\"\"", "name": "string.quoted.double.bpl"}], "applyEndPatternLast": true, "endCaptures": {"0": {"name": "string.quoted.double.bpl"}}}, {"match": "'.?'", "name": "string.quoted.single.bpl"}, {"match": "(?:_\\d+|_\\S|_|[^\\s\\p{Cc}0-9←⎕•⍺⍵⍶⍹∇⍢⍝⋄⁝\\?¯∞⍬\\+\\-×÷⌈⌊\\|\\*⍟○π√!∧∨⊼⊽\\~=≠<≤>≥⍳↦:⍴≢≡,⍪⊂⊃⊆∊∪∩⍋⍒↑↓⌽⊖⍉⊤⊥⍎⍕⌷⌹\\#↕⍭⨸⌻⍸⍷⊢⊣¿¨ᵇ⍨↣⍤∘⍠⍥↢\\.⊗⌸⍣⇄⊘@∂⍚⌺/⌿\\\\⍀⁰¹²³⁴⁵⁶⁷⁸⁹⁻ᵀᵘₓₐ₀₁₂₃₄₅₆₇₈₉₋ₓ₁ₓ₂ₓ₄ₓ₈ₓₕₛₚₐ\\(\\)\\[\\]\\{\\}'\";\\$_ₑⱼᵣ])+", "name": "variable.other.bpl"}, {"match": "[•][A-Za-z_∆⍙][A-Za-z0-9_∆⍙]*", "name": "support.function.bpl"}, {"match": "(?:[¯]?[∞]|[¯]?(\\d+([.]\\d+)?|[.]\\d+)([eEₑ][¯]?\\d+)?([jJⱼ][¯]?(\\d+([.]\\d+)?|[.]\\d+)([eEₑ][¯]?\\d+)?|[rᵣ][¯]?\\d+)?)(?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ|ₐ)?|[ₑⱼᵣ]", "name": "constant.numeric.bpl"}, {"match": "[⍬](?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ|ₐ)?", "name": "constant.numeric.bpl"}, {"match": "\\$[tfn]", "name": "constant.numeric.bpl"}, {"match": "\\$[ad]", "name": "string.quoted.double.bpl"}, {"begin": "[\\])](?=(?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ|ₐ))", "beginCaptures": {"0": {"name": "source.bpl"}}, "end": "(?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ|ₐ)|$", "contentName": "constant.numeric.bpl", "patterns": [], "applyEndPatternLast": true, "endCaptures": {"0": {"name": "constant.numeric.bpl"}}}, {"match": "[\\+\\-×÷⌈⌊\\|\\*⍟○π√!∧∨⊼⊽\\~=≠<≤>≥⍳↦:⍴≢≡,⍪⊂⊃⊆∊∪∩⍋⍒↑↓⌽⊖⍉⊤⊥⍎⍕⌷⌹\\#↕⍭⨸⌻⍸⍷⊢⊣¿]", "name": "support.function.bpl"}, {"match": "(?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ|ₐ)", "name": "keyword.operator.monadic.bpl"}, {"match": "[¨ᵇ⍨⊗⌸⇄∂/⌿\\\\⍀⁰¹²³⁴⁵⁶⁷⁸⁹⁻ᵀᵘₓₐ]", "name": "keyword.operator.monadic.bpl"}, {"match": "[↣⍤∘⍠⍥↢\\.⍣⊘@⍚⌺]", "name": "keyword.operator.dyadic.bpl"}, {"match": "[⍺⍵⍶⍹∇⍢]", "name": "variable.parameter.bpl"}, {"match": "[←⎕⋄⁝\\?₀₁₂₃₄₅₆₇₈₉₋]", "name": "keyword.control.bpl"}]}; export const symbols = [{"glyph": "←", "name": "left-arrow", "kind": "syntax", "monad": "assign", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["是", "是"], "shortcut": " h"}, {"glyph": "⎕", "name": "quad", "kind": "syntax", "monad": "io", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["出入", "出入"], "shortcut": " q"}, {"glyph": "•", "name": "bullet", "kind": "system", "monad": "system", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["系", "系"], "shortcut": " y"}, {"glyph": "⍺", "name": "alpha", "kind": "argument", "monad": "left-argument", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["甲", "甲"], "shortcut": " a"}, {"glyph": "⍵", "name": "omega", "kind": "argument", "monad": "right-argument", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["乙", "乙"], "shortcut": " w"}, {"glyph": "⍶", "name": "alpha-underbar", "kind": "argument", "monad": "left-operand", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["丙", "丙"], "shortcut": " - a"}, {"glyph": "⍹", "name": "omega-underbar", "kind": "argument", "monad": "right-operand", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["丁", "丁"], "shortcut": " - w"}, {"glyph": "∇", "name": "del", "kind": "argument", "monad": "recursion", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["递", "遞"], "shortcut": " g"}, {"glyph": "⍢", "name": "del-diaeresis", "kind": "argument", "monad": "operator-recursion", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["循", "循"], "shortcut": " 1 g"}, {"glyph": "⍝", "name": "lamp", "kind": "comment", "monad": "comment", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["注", "注"], "shortcut": " o c"}, {"glyph": "⋄", "name": "diamond", "kind": "syntax", "monad": "separator", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["句", "句"], "shortcut": " `"}, {"glyph": "⁝", "name": "tricolon", "kind": "syntax", "monad": "token-separator", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["隔", "隔"], "shortcut": " :"}, {"glyph": "?", "name": "question", "kind": "syntax", "monad": "predicate", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["若", "若"], "shortcut": ""}, {"glyph": "¯", "name": "overbar", "kind": "literal", "monad": "negative", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["负", "負"], "shortcut": " -"}, {"glyph": "∞", "name": "infinity", "kind": "literal", "monad": "", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["永", "永"], "shortcut": " 8"}, {"glyph": "⍬", "name": "zilde", "kind": "literal", "monad": "empty", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["空", "空"], "shortcut": " )"}, {"glyph": "+", "name": "plus", "kind": "function", "monad": "conjugate", "dyad": "add", "monad_rank": "0", "dyad_ranks": "0 0", "hanzi": ["加", "加"], "shortcut": ""}, {"glyph": "-", "name": "minus", "kind": "function", "monad": "negate", "dyad": "subtract", "monad_rank": "0", "dyad_ranks": "0 0", "hanzi": ["减", "減"], "shortcut": ""}, {"glyph": "×", "name": "times", "kind": "function", "monad": "sign", "dyad": "multiply", "monad_rank": "0", "dyad_ranks": "0 0", "hanzi": ["乘号", "乘號"], "shortcut": " m"}, {"glyph": "÷", "name": "obelus", "kind": "function", "monad": "reciprocal", "dyad": "divide", "monad_rank": "0", "dyad_ranks": "0 0", "hanzi": ["除", "除"], "shortcut": " u"}, {"glyph": "⌈", "name": "upstile", "kind": "function", "monad": "ceiling", "dyad": "maximum", "monad_rank": "0", "dyad_ranks": "0 0", "hanzi": ["上", "上"], "shortcut": " s"}, {"glyph": "⌊", "name": "downstile", "kind": "function", "monad": "floor", "dyad": "minimum", "monad_rank": "0", "dyad_ranks": "0 0", "hanzi": ["下", "下"], "shortcut": " d"}, {"glyph": "|", "name": "stile", "kind": "function", "monad": "magnitude", "dyad": "residue", "monad_rank": "0", "dyad_ranks": "0 0", "hanzi": ["绝余", "絕餘"], "shortcut": ""}, {"glyph": "*", "name": "star", "kind": "function", "monad": "exponential", "dyad": "exponent", "monad_rank": "0", "dyad_ranks": "0 0", "hanzi": ["幂", "冪"], "shortcut": ""}, {"glyph": "⍟", "name": "circle-star", "kind": "function", "monad": "natural-log", "dyad": "logarithm", "monad_rank": "0", "dyad_ranks": "0 0", "hanzi": ["对", "對"], "shortcut": " o 8"}, {"glyph": "○", "name": "circle", "kind": "function", "monad": "cis", "dyad": "circular", "monad_rank": "0", "dyad_ranks": "0 0", "hanzi": ["圆", "圓"], "shortcut": " o"}, {"glyph": "π", "name": "pi", "kind": "function", "monad": "pi-times", "dyad": "pi-ratio", "monad_rank": "0", "dyad_ranks": "0 0", "hanzi": ["周", "周"], "shortcut": " p"}, {"glyph": "√", "name": "radical", "kind": "function", "monad": "sqrt", "dyad": "root", "monad_rank": "0", "dyad_ranks": "0 0", "hanzi": ["根", "根"], "shortcut": " 3"}, {"glyph": "!", "name": "exclamation", "kind": "function", "monad": "factorial", "dyad": "binomial", "monad_rank": "0", "dyad_ranks": "0 0", "hanzi": ["阶拣", "階揀"], "shortcut": ""}, {"glyph": "∧", "name": "wedge", "kind": "function", "monad": "polar", "dyad": "and", "monad_rank": "0", "dyad_ranks": "0 0", "hanzi": ["极且", "極且"], "shortcut": " 0"}, {"glyph": "∨", "name": "vee", "kind": "function", "monad": "real-imag", "dyad": "or", "monad_rank": "0", "dyad_ranks": "0 0", "hanzi": ["析或", "析或"], "shortcut": " 9"}, {"glyph": "⊼", "name": "wedge-bar", "kind": "function", "monad": "square", "dyad": "nand", "monad_rank": "0", "dyad_ranks": "0 0", "hanzi": ["方否", "方否"], "shortcut": " - 0"}, {"glyph": "⊽", "name": "vee-bar", "kind": "function", "monad": "double", "dyad": "nor", "monad_rank": "0", "dyad_ranks": "0 0", "hanzi": ["双莫", "雙莫"], "shortcut": " - 9"}, {"glyph": "~", "name": "tilde", "kind": "function", "monad": "not", "dyad": "without", "monad_rank": "0", "dyad_ranks": "∞ ∞", "hanzi": ["非无", "非無"], "shortcut": ""}, {"glyph": "=", "name": "equals", "kind": "function", "monad": "classify", "dyad": "equal", "monad_rank": "∞", "dyad_ranks": "0 0", "hanzi": ["等", "等"], "shortcut": ""}, {"glyph": "≠", "name": "not-equals", "kind": "function", "monad": "unique-mask", "dyad": "not-equal", "monad_rank": "∞", "dyad_ranks": "0 0", "hanzi": ["别初", "別初"], "shortcut": " +"}, {"glyph": "<", "name": "less-than", "kind": "function", "monad": "sort-up", "dyad": "less", "monad_rank": "∞", "dyad_ranks": "0 0", "hanzi": ["小", "小"], "shortcut": ""}, {"glyph": "≤", "name": "less-than-or-equal", "kind": "function", "monad": "decrement", "dyad": "less-equal", "monad_rank": "0", "dyad_ranks": "0 0", "hanzi": ["限退", "限退"], "shortcut": " <"}, {"glyph": ">", "name": "greater-than", "kind": "function", "monad": "sort-down", "dyad": "greater", "monad_rank": "∞", "dyad_ranks": "0 0", "hanzi": ["大", "大"], "shortcut": ""}, {"glyph": "≥", "name": "greater-than-or-equal", "kind": "function", "monad": "increment", "dyad": "greater-equal", "monad_rank": "0", "dyad_ranks": "0 0", "hanzi": ["达进", "達進"], "shortcut": " >"}, {"glyph": "⍳", "name": "iota", "kind": "function", "monad": "indices", "dyad": "index-of", "monad_rank": "1", "dyad_ranks": "∞ ∞", "hanzi": ["位", "位"], "shortcut": " i"}, {"glyph": "↦", "name": "right-arrow-from-bar", "kind": "function", "monad": "range", "dyad": "to", "monad_rank": "0", "dyad_ranks": "0 0", "hanzi": ["至", "至"], "shortcut": " l"}, {"glyph": ":", "name": "colon", "kind": "function", "monad": "unkey", "dyad": "keyed", "monad_rank": "∞", "dyad_ranks": "1 ∞", "hanzi": ["键卸", "鍵卸"], "shortcut": ""}, {"glyph": "⍴", "name": "rho", "kind": "function", "monad": "shape", "dyad": "reshape", "monad_rank": "∞", "dyad_ranks": "1 ∞", "hanzi": ["形", "形"], "shortcut": " r"}, {"glyph": "≢", "name": "equal-underbar-slash", "kind": "function", "monad": "tally", "dyad": "not-match", "monad_rank": "∞", "dyad_ranks": "∞ ∞", "hanzi": ["数异", "數異"], "shortcut": " ?"}, {"glyph": "≡", "name": "equal-underbar", "kind": "function", "monad": "depth", "dyad": "match", "monad_rank": "∞", "dyad_ranks": "∞ ∞", "hanzi": ["同深", "同深"], "shortcut": " ="}, {"glyph": ",", "name": "comma", "kind": "function", "monad": "ravel", "dyad": "catenate", "monad_rank": "∞", "dyad_ranks": "∞ ∞", "hanzi": ["连", "連"], "shortcut": ""}, {"glyph": "⍪", "name": "comma-bar", "kind": "function", "monad": "table", "dyad": "catenate-first", "monad_rank": "∞", "dyad_ranks": "∞ ∞", "hanzi": ["接矩", "接矩"], "shortcut": " - ,"}, {"glyph": "⊂", "name": "left-shoe", "kind": "function", "monad": "enclose", "dyad": "partitioned-enclose", "monad_rank": "∞", "dyad_ranks": "1 ∞", "hanzi": ["包", "包"], "shortcut": " z"}, {"glyph": "⊃", "name": "right-shoe", "kind": "function", "monad": "mix", "dyad": "pick", "monad_rank": "0", "dyad_ranks": "1 ∞", "hanzi": ["选合", "選合"], "shortcut": " x"}, {"glyph": "⊆", "name": "left-shoe-underbar", "kind": "function", "monad": "nest", "dyad": "partition", "monad_rank": "∞", "dyad_ranks": "1 ∞", "hanzi": ["巢段", "巢段"], "shortcut": " - z"}, {"glyph": "∊", "name": "epsilon", "kind": "function", "monad": "enlist", "dyad": "member", "monad_rank": "∞", "dyad_ranks": "∞ ∞", "hanzi": ["属展", "屬展"], "shortcut": " e"}, {"glyph": "∪", "name": "down-shoe", "kind": "function", "monad": "unique", "dyad": "union", "monad_rank": "∞", "dyad_ranks": "∞ ∞", "hanzi": ["并独", "併獨"], "shortcut": " v"}, {"glyph": "∩", "name": "up-shoe", "kind": "function", "monad": "duplicates", "dyad": "intersection", "monad_rank": "∞", "dyad_ranks": "∞ ∞", "hanzi": ["交重", "交重"], "shortcut": " c"}, {"glyph": "⍋", "name": "delta-stile", "kind": "function", "monad": "grade-up", "dyad": "grade-up-by", "monad_rank": "∞", "dyad_ranks": "∞ ∞", "hanzi": ["升", "升"], "shortcut": " \\ 7"}, {"glyph": "⍒", "name": "del-stile", "kind": "function", "monad": "grade-down", "dyad": "grade-down-by", "monad_rank": "∞", "dyad_ranks": "∞ ∞", "hanzi": ["降", "降"], "shortcut": " \\ g"}, {"glyph": "↑", "name": "up-arrow", "kind": "function", "monad": "first", "dyad": "take", "monad_rank": "∞", "dyad_ranks": "1 ∞", "hanzi": ["取", "取"], "shortcut": " k"}, {"glyph": "↓", "name": "down-arrow", "kind": "function", "monad": "split", "dyad": "drop", "monad_rank": "1", "dyad_ranks": "1 ∞", "hanzi": ["去拆", "去拆"], "shortcut": " j"}, {"glyph": "⌽", "name": "circle-stile", "kind": "function", "monad": "reverse", "dyad": "rotate", "monad_rank": "1", "dyad_ranks": "0 1", "hanzi": ["转", "轉"], "shortcut": " 2"}, {"glyph": "⊖", "name": "circle-bar", "kind": "function", "monad": "reverse-first", "dyad": "rotate-first", "monad_rank": "∞", "dyad_ranks": "∞ ∞", "hanzi": ["纵", "縱"], "shortcut": " o -"}, {"glyph": "⍉", "name": "circle-backslash", "kind": "function", "monad": "transpose", "dyad": "reorder-axes", "monad_rank": "∞", "dyad_ranks": "1 ∞", "hanzi": ["翻", "翻"], "shortcut": " o \\"}, {"glyph": "⊤", "name": "down-tack", "kind": "function", "monad": "binary-encode", "dyad": "encode", "monad_rank": "∞", "dyad_ranks": "1 0", "hanzi": ["编", "編"], "shortcut": " n"}, {"glyph": "⊥", "name": "up-tack", "kind": "function", "monad": "binary-decode", "dyad": "decode", "monad_rank": "1", "dyad_ranks": "1 1", "hanzi": ["译", "譯"], "shortcut": " b"}, {"glyph": "⍎", "name": "hydrant", "kind": "function", "monad": "execute", "dyad": "execute-in", "monad_rank": "1", "dyad_ranks": "∞ ∞", "hanzi": ["行", "行"], "shortcut": " o b"}, {"glyph": "⍕", "name": "thorn", "kind": "function", "monad": "format", "dyad": "format-spec", "monad_rank": "∞", "dyad_ranks": "1 ∞", "hanzi": ["文", "文"], "shortcut": " o n"}, {"glyph": "⌷", "name": "squad", "kind": "function", "monad": "materialise", "dyad": "index", "monad_rank": "∞", "dyad_ranks": "1 ∞", "hanzi": ["索", "索"], "shortcut": " q \\"}, {"glyph": "⌹", "name": "domino", "kind": "function", "monad": "matrix-inverse", "dyad": "matrix-divide", "monad_rank": "2", "dyad_ranks": "∞ 2", "hanzi": ["解逆", "解逆"], "shortcut": " q u"}, {"glyph": "#", "name": "hash", "kind": "function", "monad": "runs", "dyad": "replicate", "monad_rank": "∞", "dyad_ranks": "1 ∞", "hanzi": ["复", "複"], "shortcut": ""}, {"glyph": "↕", "name": "up-down-arrow", "kind": "function", "monad": "pairs", "dyad": "windows", "monad_rank": "∞", "dyad_ranks": "∞ ∞", "hanzi": ["窗", "窗"], "shortcut": " K"}, {"glyph": "⍭", "name": "stile-tilde", "kind": "function", "monad": "prime", "dyad": "prime-mode", "monad_rank": "0", "dyad_ranks": "0 0", "hanzi": ["素", "素"], "shortcut": " \\"}, {"glyph": "⨸", "name": "circle-divide", "kind": "function", "monad": "factors", "dyad": "factor-spec", "monad_rank": "0", "dyad_ranks": "0 0", "hanzi": ["因", "因"], "shortcut": " o u"}, {"glyph": "⌻", "name": "quad-jot", "kind": "function", "monad": "roots", "dyad": "polyval", "monad_rank": "1", "dyad_ranks": "1 0", "hanzi": ["零值", "零值"], "shortcut": " q t"}, {"glyph": "⍸", "name": "iota-underbar", "kind": "function", "monad": "where", "dyad": "interval-index", "monad_rank": "∞", "dyad_ranks": "∞ ∞", "hanzi": ["处间", "處間"], "shortcut": " - i"}, {"glyph": "⍷", "name": "epsilon-underbar", "kind": "function", "monad": "groups", "dyad": "find", "monad_rank": "∞", "dyad_ranks": "∞ ∞", "hanzi": ["寻群", "尋群"], "shortcut": " - e"}, {"glyph": "⊢", "name": "right-tack", "kind": "function", "monad": "same", "dyad": "right", "monad_rank": "∞", "dyad_ranks": "∞ ∞", "hanzi": ["右", "右"], "shortcut": " '"}, {"glyph": "⊣", "name": "left-tack", "kind": "function", "monad": "same-left", "dyad": "left", "monad_rank": "∞", "dyad_ranks": "∞ ∞", "hanzi": ["左", "左"], "shortcut": " ;"}, {"glyph": "¿", "name": "inverted-question", "kind": "function", "monad": "roll", "dyad": "deal", "monad_rank": "0", "dyad_ranks": "0 0", "hanzi": ["掷抽", "擲抽"], "shortcut": " /"}, {"glyph": "¨", "name": "diaeresis", "kind": "monadic-operator", "monad": "each", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["遍", "遍"], "shortcut": " 1"}, {"glyph": "ᵇ", "name": "superscript-b", "kind": "monadic-operator", "monad": "bitwise", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["按", "按"], "shortcut": " 6 b"}, {"glyph": "⍨", "name": "tilde-diaeresis", "kind": "monadic-operator", "monad": "self", "dyad": "commute", "monad_rank": "", "dyad_ranks": "", "hanzi": ["己换", "己換"], "shortcut": " 1 `"}, {"glyph": "↣", "name": "right-tail-arrow", "kind": "dyadic-operator", "monad": "before", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["先", "先"], "shortcut": " ."}, {"glyph": "⍤", "name": "jot-diaeresis", "kind": "dyadic-operator", "monad": "rank", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["秩", "秩"], "shortcut": " 1 t"}, {"glyph": "∘", "name": "jot", "kind": "dyadic-operator", "monad": "atop", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["承", "承"], "shortcut": " t"}, {"glyph": "⍠", "name": "quad-colon", "kind": "dyadic-operator", "monad": "axis", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["轴", "軸"], "shortcut": " q ;"}, {"glyph": "⍥", "name": "circle-diaeresis", "kind": "dyadic-operator", "monad": "over", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["经", "經"], "shortcut": " 1 o"}, {"glyph": "↢", "name": "left-tail-arrow", "kind": "dyadic-operator", "monad": "after", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["后", "後"], "shortcut": " ,"}, {"glyph": ".", "name": "dot", "kind": "dyadic-operator", "monad": "determinant", "dyad": "inner-product", "monad_rank": "", "dyad_ranks": "", "hanzi": ["内", "內"], "shortcut": ""}, {"glyph": "⊗", "name": "circle-times", "kind": "monadic-operator", "monad": "outer-product", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["表", "表"], "shortcut": " o m"}, {"glyph": "⌸", "name": "quad-equal", "kind": "monadic-operator", "monad": "key", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["组", "組"], "shortcut": " q ="}, {"glyph": "⍣", "name": "star-diaeresis", "kind": "dyadic-operator", "monad": "power", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["次止", "次止"], "shortcut": " 1 8"}, {"glyph": "⇄", "name": "right-left-arrows", "kind": "monadic-operator", "monad": "optimize", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["优", "優"], "shortcut": " 4"}, {"glyph": "⊘", "name": "circle-slash", "kind": "dyadic-operator", "monad": "valences", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["兼", "兼"], "shortcut": " o /"}, {"glyph": "@", "name": "at", "kind": "dyadic-operator", "monad": "under", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["于", "於"], "shortcut": ""}, {"glyph": "∂", "name": "partial", "kind": "monadic-operator", "monad": "derivative", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["导梯", "導梯"], "shortcut": " f"}, {"glyph": "⍚", "name": "diamond-underbar", "kind": "dyadic-operator", "monad": "agenda", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["择", "擇"], "shortcut": " - `"}, {"glyph": "⌺", "name": "quad-diamond", "kind": "dyadic-operator", "monad": "stencil", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["邻", "鄰"], "shortcut": " q `"}, {"glyph": "/", "name": "slash", "kind": "monadic-operator", "monad": "reduce", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["总", "總"], "shortcut": ""}, {"glyph": "⌿", "name": "slash-bar", "kind": "monadic-operator", "monad": "reduce-first", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["汇", "匯"], "shortcut": " - /"}, {"glyph": "\\", "name": "backslash", "kind": "monadic-operator", "monad": "scan", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["累", "累"], "shortcut": ""}, {"glyph": "⍀", "name": "backslash-bar", "kind": "monadic-operator", "monad": "scan-first", "dyad": "", "monad_rank": "", "dyad_ranks": "", "hanzi": ["续", "續"], "shortcut": " - \\"}]; diff --git a/python/basedpl/hpl.tmLanguage.json b/python/basedpl/hpl.tmLanguage.json index 6161b516..3540a2a1 100644 --- a/python/basedpl/hpl.tmLanguage.json +++ b/python/basedpl/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" }, { @@ -27,11 +27,11 @@ "name": "support.function.bpl" }, { - "match": "(?:[¯负負]?[∞永]|[¯负負]?(\\d+([.内內]\\d+)?|[.内內]\\d+)([eEₑ][¯负負]?\\d+)?([jJⱼ][¯负負]?(\\d+([.内內]\\d+)?|[.内內]\\d+)([eEₑ][¯负負]?\\d+)?|[rᵣ][¯负負]?\\d+)?)(?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ)?|[ₑⱼᵣ]", + "match": "(?:[¯负負]?[∞永]|[¯负負]?(\\d+([.内內]\\d+)?|[.内內]\\d+)([eEₑ][¯负負]?\\d+)?([jJⱼ][¯负負]?(\\d+([.内內]\\d+)?|[.内內]\\d+)([eEₑ][¯负負]?\\d+)?|[rᵣ][¯负負]?\\d+)?)(?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ|ₐ)?|[ₑⱼᵣ]", "name": "constant.numeric.bpl" }, { - "match": "[⍬空](?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ)?", + "match": "[⍬空](?:ₓ₁|ₓ₂|ₓ₄|ₓ₈|ₓ|ₕ|ₛ|ₚ|ₐ)?", "name": "constant.numeric.bpl" }, { diff --git a/python/basedpl/notebook_runner.py b/python/basedpl/notebook_runner.py index dc7cfee0..9fdaf933 100644 --- a/python/basedpl/notebook_runner.py +++ b/python/basedpl/notebook_runner.py @@ -1,6 +1,6 @@ "Run BPL notebooks directly, optionally saving their output." -import json +import difflib, json from pathlib import Path from fastcore.script import call_parse from fastcore.xtras import working_directory @@ -10,36 +10,56 @@ LANGUAGES = {'bpl', 'hpl'} -def run_notebook(path, save=False): - "Run BPL cells in a cleared workspace, from the notebook's directory. Save outputs only when requested." +def _outputs(events): + "A cell's notebook outputs, from the events of its BPL run." + return [dict(output_type='stream', name='stdout', text=e['data']['text/plain']+'\n') if e['kind'] == 'explicit' + else dict(output_type='display_data', data=e['data'], metadata={}) for e in events] + + +def _shown(outputs): + "The text that `outputs` show: each output's text, or the values of its data." + def text(v): return (''.join(v) if isinstance(v, list) else str(v)).rstrip('\n') + return '\n'.join(text(o['text']) if 'text' in o else '\n'.join(text(v) for v in o.get('data', {}).values()) for o in outputs) + + +def run_notebook(path, save=False, compare=False): + "Run BPL cells in a cleared workspace, from the notebook's directory. Save outputs only when requested. With `compare`, give `(cell number, id, stored, new)` for each cell whose shown text changed." path = Path(path) nb = json.loads(path.read_text()) if nb['metadata']['kernelspec']['language'] not in LANGUAGES: raise ValueError(f'{path}: expected a BPL notebook') bpl(']clear') + changed = [] with working_directory(path.parent): for i, cell in enumerate(nb['cells'], 1): if cell['cell_type'] != 'code': continue try: result = bpl(''.join(cell['source']), 'repl') except Exception as e: raise RuntimeError(f'{path}: cell {i} ({cell.get("id", "")}): {e}') from e - if save: - cell['outputs'] = [dict(output_type='stream', name='stdout', text=e['data']['text/plain']+'\n') if e['kind'] == 'explicit' - else dict(output_type='display_data', data=e['data'], metadata={}) for e in result.events] + outputs = _outputs(result.events) + if compare and (old := _shown(cell.get('outputs', []))) != (new := _shown(outputs)): changed.append((i, cell.get('id', ''), old, new)) + if save: cell['outputs'] = outputs if save: write_nb(nb, path) + return changed @call_parse(pos=['path']) def main( path:Path=Path('nbs'), # BPL notebook or directory to search save:bool=False, # Save outputs in the notebook + compare:bool=False, # Print a diff for each cell whose outputs differ from the stored ones, and exit with status 1 if any do ): "Run BPL notebooks without starting Jupyter, holding the timing lock shared." try: from fastws.timelock import timing_lock except ImportError: raise SystemExit("bpl-nb runs basedpl's own notebooks and needs the dev extras: pip install 'basedpl[dev]'") + changed = [] with timing_lock(): paths = sorted(path.rglob('*.ipynb')) if path.is_dir() else [path] for p in paths: if path.is_dir() and json.loads(p.read_text())['metadata'].get('kernelspec', {}).get('language') not in LANGUAGES: continue - run_notebook(p, save) + changed += [(p, *c) for c in run_notebook(p, save, compare)] + for p, i, cid, old, new in changed: + print(f'{p}: cell {i} ({cid})') + print('\n'.join(difflib.unified_diff(old.splitlines(), new.splitlines(), 'stored', 'new', lineterm=''))) + if changed: raise SystemExit(1) if __name__ == '__main__': main() diff --git a/python/basedpl/worker.py b/python/basedpl/worker.py index 6f8ac1e7..a1cc5f70 100644 --- a/python/basedpl/worker.py +++ b/python/basedpl/worker.py @@ -1,39 +1,99 @@ "Persistent JSON-lines worker with cooperative interruption and a hard timeout fallback." -import json, math, os, queue, subprocess, sysconfig, threading +import json, math, os, queue, subprocess, sysconfig, threading, time from pathlib import Path +from typing import Annotated from collections import deque, namedtuple from decimal import Decimal from . import BplError, _output_text, _written from .syntax import linear from fastcore.meta import delegates +from contextlib import contextmanager INSTALLED_BPL = Path(sysconfig.get_path('scripts'))/'bpl' # The `bpl` that `cargo develop` installs, a link through its installed build (`fastws.store`) -NATIVE = {'RUSTYMATH_NATIVE': '1'} # The environment that runs rustymath's own linear algebra in place of Accelerate, in a macOS build with rustymath's `native` feature +Compiles = Annotated[str, {'choices': ['always', 'never']}] # The values that `environment`'s `compile` takes, as choices for a shell argument +Defers = Annotated[str, {'choices': ['all', 'reference']}] # The values that `environment`'s `defer` takes, as choices for a shell argument +def environment( + native=False, # Run rustymath's own linear algebra in place of Accelerate, through `RUSTYMATH_NATIVE=1`, in a macOS build with rustymath's `native` feature + compile:Compiles=None, # How block stages compile, through `BASEDPL_COMPILE` (`settings::Compiles`): `always`, every stage on its first run; `never`, none; None, by the compile policy + defer:Defers=None # Which costs decide whether a call waits, through `BASEDPL_DEFER` (`settings::waits`): `all`, every call waits, as fusion at any size runs; `reference`, the Mac's costs, as route checks take; None, this platform's costs +): + "This process's environment, with these settings, for a `bpl` process or speed test that it starts. Every tool that starts one takes them through `@delegates(environment)`." + return os.environ | ({'RUSTYMATH_NATIVE': '1'} if native else {}) | ({'BASEDPL_COMPILE': compile} if compile else {}) | ({'BASEDPL_DEFER': defer} if defer else {}) + +LIMIT = 30 # Seconds of work that a check or timing call gets unless its `timeout` says otherwise; builds and waits for the timing lock don't count + +class Limit: + "The seconds of work left to one call that `seconds` bounds, with no bound for `None`. Each piece of the call's work spends the seconds it runs: a worker's request spends its `ran`, and `running` spends a block's seconds. Waits for builds and for the timing lock spend nothing. A tool's `timeout` takes seconds or a `Limit`, so a caller can share one limit across the tools it calls (`of`)." + def __init__(self, seconds=LIMIT): self.seconds, self.spent, self.since = seconds, 0, None + + @classmethod + def of(cls, timeout): return timeout if isinstance(timeout, Limit) else cls(timeout) + + def passed(self): return TimeoutError(f'the call reached its {self.seconds:g} s limit; check or time less at once, or pass a larger `timeout`') + + def left(self): + "The seconds left, or `None` for no bound. Raises `TimeoutError` once none are left." + if self.seconds is None: return None + left = self.seconds - self.spent - (0 if self.since is None else time.monotonic() - self.since) + if left <= 0: raise self.passed() + return left + + def spend(self, seconds): self.spent += seconds + + def reached(self, error): + "Raise `TimeoutError` where `error`, a request's error, is the timeout that the request's deadline gave." + if error and error.get('kind') == 'TIMEOUT': raise self.passed() + + @contextmanager + def running(self): + "Spend the seconds that the block runs, such as processes that run while the call holds the timing lock." + self.since = time.monotonic() + try: yield + finally: self.spent, self.since = self.spent + time.monotonic() - self.since, None class Worker: - "One request at a time. Interrupt from another thread; never retry a request automatically." + "One request at a time. Interrupt from another thread; never retry a request automatically. Inherit the caller's process group; this transport stops its worker, while the containing job owns descendant cleanup." + @delegates(environment) def __init__( self, - bpl=None, # The native `bpl` to run (`resolved`); None: the installed build - native=False # Run rustymath's own linear algebra in place of Accelerate, through `RUSTYMATH_NATIVE=1`, in a macOS build with rustymath's `native` feature + bpl=None, # The native `bpl` to run (`resolved`); None or `installed`: the installed build + **kwargs ): - env = os.environ | NATIVE if native else None self.process = subprocess.Popen((resolved(bpl), '--worker'), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - encoding='utf-8', bufsize=1, start_new_session=True, env=env) + encoding='utf-8', bufsize=1, env=environment(**kwargs)) self._replies, self._stderr = queue.Queue(), deque(maxlen=30) self._write_lock, self._request_lock = threading.Lock(), threading.Lock() - self._id, self._active = 0, None + self._id, self._active, self._heard, self._started, self.ran = 0, None, time.monotonic(), time.monotonic(), None self._readers = [threading.Thread(target=self._read, daemon=True), threading.Thread(target=self._read_errors, daemon=True)] for t in self._readers: t.start() def _read(self): try: - for line in self.process.stdout: self._replies.put(json.loads(line, parse_int=lambda n: int(Decimal(n)))) + with self.process.stdout as stream: + for line in stream: self._replies.put(json.loads(line, parse_int=lambda n: int(Decimal(n)))) except Exception as e: self._replies.put(e) finally: self._replies.put(EOFError('worker exited; its session is lost')) def _read_errors(self): - for line in self.process.stderr: self._stderr.append(line) + with self.process.stderr as stream: + for line in stream: + if line == 'started\n': self._started = time.monotonic() + else: self._stderr.append(line) + self._heard = time.monotonic() + + def _reply(self, within, stall): + "The next reply, which must arrive within `within` seconds of the request, or of the worker's `started` note where a timing request writes one once it holds the timing lock (`timing::started`), and within `stall` seconds of the worker's last line of stderr or of the request, or else `TimeoutError`. `None` sets no limit. `ran` then holds the seconds from that start to the reply." + self._started = self._heard = time.monotonic() + while True: + ends = [t for t in (None if within is None else self._started+within, None if stall is None else self._heard+stall) if t is not None] + left = min(ends) - time.monotonic() if ends else None + if left is not None and left <= 0: + late = within is not None and time.monotonic() >= self._started+within + raise TimeoutError(f'worker {"did not stop before the deadline" if late else f"wrote nothing for {stall} seconds"}; its session is lost') + try: reply = self._replies.get(timeout=left) + except queue.Empty: continue + self.ran = time.monotonic() - self._started + return reply @property def diagnostics(self): return ''.join(self._stderr) @@ -57,11 +117,12 @@ def interrupt(self): def request( self, payload, # Request fields, such as `code`, `reference`, `page`, `timing` or `costs` - timeout=None, # Seconds before the worker interrupts the request - grace=1 # Seconds to wait for the reply after `timeout`, or after a Ctrl-C, before closing the worker + timeout=None, # Seconds before the worker stops the request, counted from its `started` note where a timing request writes one once it holds the timing lock + grace=1, # Seconds to wait for the reply after `timeout`, or after a Ctrl-C, before closing the worker + stall=None # Seconds the worker may write no line to stderr, as a reference or page check notes each case or example it starts, before it counts as stuck ): - "Send one request to the worker and return its result. A Ctrl-C interrupts the request and re-raises with its partial `output` and `events`. If no reply arrives within `timeout` plus `grace`, the worker closes and its session is lost. The call then raises `TimeoutError`. A worker that exits, as a crash ends it, raises `EOFError` with its last lines of stderr." - for name, seconds in [('timeout', timeout), ('grace', grace)]: + "Send one request to the worker and return its result. A Ctrl-C interrupts the request and re-raises with its partial `output` and `events`. If no reply arrives within `timeout` plus `grace`, or within `stall` seconds of the worker's last line of stderr, the worker closes and its session is lost, since a deadlocked worker never takes the interrupt. The call then raises `TimeoutError`. A worker that exits, as a crash ends it, raises `EOFError` with its last lines of stderr." + for name, seconds in [('timeout', timeout), ('grace', grace), ('stall', stall)]: if seconds is not None and (not math.isfinite(seconds) or seconds < 0): raise ValueError(f'{name} must be finite and nonnegative') with self._request_lock: if self.process.poll() is not None: raise RuntimeError('worker is closed; its session is lost') @@ -75,7 +136,7 @@ def request( self._active = self._id self.process.stdin.write(message) self.process.stdin.flush() - reply = self._replies.get(timeout=None if timeout is None else timeout+grace) + reply = self._reply(None if timeout is None else timeout+grace, stall) except KeyboardInterrupt as e: e.output, e.events = [], [] self.interrupt() @@ -86,9 +147,9 @@ def request( e.output = _output_text(e.events) except queue.Empty: self.close() raise - except queue.Empty: + except TimeoutError: self.close() - raise TimeoutError('worker did not stop before the deadline; its session is lost') from None + raise if isinstance(reply, Exception): self.close() if isinstance(reply, EOFError) and self.diagnostics: raise EOFError(f'{reply}\n{self.diagnostics.rstrip()}') @@ -101,21 +162,20 @@ def request( with self._write_lock: self._active = None def close(self): - if self.process.poll() is None: - self.process.terminate() - try: self.process.wait(timeout=1) - except subprocess.TimeoutExpired: - self.process.kill() - self.process.wait() - for t in self._readers: t.join() - for stream in (self.process.stdin, self.process.stdout, self.process.stderr): stream.close() + "Stop and reap the worker without waiting on pipes inherited by its descendants. Reader threads close their streams when their writers end." + if self.process.poll() is None: self.process.kill(); self.process.wait() + self.process.stdin.close() def __enter__(self): return self def __exit__(self, *args): self.close() +def _installed(build): + "Whether `build` names the installed build: None, or `installed`, which a shell can write where it can't write None." + return build is None or build == 'installed' + def resolved(build=None): - "The native `bpl` of `build`, resolved now, so that no later `cargo develop` changes it: a path to a `bpl` (`~` expands), a prefix of the id of a build that `cargo develop` kept (`fastws.store`), or None for the installed build." - if build is None: return Path(os.path.realpath(INSTALLED_BPL)) + "The native `bpl` of `build`, resolved now, so that no later `cargo develop` changes it: a path to a `bpl` (`~` expands), a prefix of the id of a build that `cargo develop` kept (`fastws.store`), or None or `installed` for the installed build." + if _installed(build): return Path(os.path.realpath(INSTALLED_BPL)) if (path := Path(build).expanduser()).exists(): return path from fastws.store import resolve return resolve(Path(__file__).resolve().parents[2], str(build))/'bin'/'bpl' @@ -143,26 +203,31 @@ def __new__(cls, text, code): return self def __repr__(self): return repr(Outputs([Output(self.code, str(self), None)])) -def _outcome(code, timeout, prefs, kwargs): - "What `code` prints in a fresh worker after `prefs` runs, and the `BplError`, `TimeoutError` or `EOFError` it raised, or `None`. An error in `prefs` raises." +def _outcome(code, limit, prefs, kwargs): + "What `code` prints in a fresh worker after `prefs` runs, and the `BplError`, `TimeoutError` or `EOFError` it raised, or `None`. Both evaluations spend `limit`. An error in `prefs` raises." with Worker(**kwargs) as w: - if prefs and (r := w.eval(prefs, timeout))['error']: raise BplError(r['error'], r['output']) - try: r = w.eval(code, timeout) + try: + if prefs: + if (r := w.eval(prefs, limit.left()))['error']: raise BplError(r['error'], r['output']) + limit.spend(w.ran) + r = w.eval(code, limit.left()) + limit.spend(w.ran) except (TimeoutError, EOFError) as e: return '', e return _written(r['output']).removesuffix('\n'), r['error'] and BplError(r['error'], r['output']) @delegates(Worker, but=['bpl']) def fresh( code, # BPL source, or a list of expressions to run each in its own fresh worker - timeout=60, # Seconds before each evaluation is interrupted, and a second more before an unresponsive worker is killed; None: no limit + timeout=LIMIT, # Seconds of work that the call gets, or a `Limit` to share (`Limit.of`): each evaluation is interrupted once they're spent, and its worker killed a second later; None: no limit prefs='', # BPL that runs before `code` in each worker, such as `•prefs ["kinds":"static"]` - build=None, # The build to run (`resolved`): a path to a `bpl`, a prefix of the id of a build that `cargo develop` kept, or None for the installed build + build=None, # The build to run (`resolved`): a path to a `bpl`, a prefix of the id of a build that `cargo develop` kept, or None or `installed` for the installed build **kwargs ): - "Return what `code` prints in a fresh worker process, as `Printed`. Its display shows the code's linear form (`syntax.linear`) before the text. A BPL error raises `BplError`, with the code and its linear form as a note. A worker that doesn't reply within `timeout` plus a second is killed, and `TimeoutError` raises. A worker that exits, as a crash ends it, raises `EOFError` with its last lines of stderr. A list of expressions gives `Outputs`, with each one's output or error, timeouts and exits included, and runs every one on the same build." + "Return what `code` prints in a fresh worker process, as `Printed`. Its display shows the code's linear form (`syntax.linear`) before the text. A BPL error raises `BplError`, with the code and its linear form as a note. A worker that doesn't reply within `timeout` plus a second is killed, and `TimeoutError` raises. A list's expressions share `timeout`. A worker that exits, as a crash ends it, raises `EOFError` with its last lines of stderr. A list of expressions gives `Outputs`, with each one's output or error, timeouts and exits included, and runs every one on the same build." kwargs |= dict(bpl=resolved(build)) - if not isinstance(code, str): return Outputs(Output(c, *_outcome(c, timeout, prefs, kwargs)) for c in code) - output, error = _outcome(code, timeout, prefs, kwargs) + limit = Limit.of(timeout) + if not isinstance(code, str): return Outputs(Output(c, *_outcome(c, limit, prefs, kwargs)) for c in code) + output, error = _outcome(code, limit, prefs, kwargs) if error: error.add_note(_commented(code)) raise error diff --git a/src/agreement.rs b/src/agreement.rs index 7b1ad70b..c1ea89b0 100644 --- a/src/agreement.rs +++ b/src/agreement.rs @@ -1,4 +1,4 @@ -use crate::{ErrorKind, Value, array::{Layout, generated_len}}; +use crate::{ErrorKind, Value, array::{Layout, generated_len}, program::Extents}; use rustymath::{agreement::Mapping, error::Error, gather::Steps}; use std::borrow::Cow; @@ -21,8 +21,8 @@ pub(crate) trait Agree: Sized { } /// `values`, borrowing each item that its argument holds as a value. fn values_ref<'a>(&self, left: Option<&'a Value>, right: &'a Value, i: usize) -> (Option>, Cow<'a, Value>); - /// `left` and `right` laid out as the result, each repeated over the axes and positions it lacks, as broadcasting repeats it. - fn laid(&self, left: Option<&Value>, right: &Value) -> (Option, Value); + /// `left` and `right` laid out as the result, each repeated over the axes and positions it lacks, as broadcasting repeats it. An atom stays as it stands, since the call reads it at every position (main's decision 23). `Err` where an argument can't be laid, as a ragged batch stops its run. + fn laid(&self, left: Option<&Value>, right: &Value) -> Result<(Option, Value), ErrorKind>; } impl Agree for Agreement { @@ -40,9 +40,9 @@ impl Agree for Agreement { let item = |a: &'a Value, j: Option, map: &Mapping| j.map_or_else(|| Cow::Owned(a.mapped_fill(map)), |j| a.at_ref(j)); (left.map(|a| item(a, x, &self.left)), item(right, y, &self.right)) } - fn laid(&self, left: Option<&Value>, right: &Value) -> (Option, Value) { - let laid = |v: &Value, mapping: &Mapping| laid_cells(v, 0, mapping, &self.layout).expect("an agreement's layout fits"); - (left.map(|x| laid(x, &self.left)), laid(right, &self.right)) + fn laid(&self, left: Option<&Value>, right: &Value) -> Result<(Option, Value), ErrorKind> { + let laid = |v: &Value, mapping: &Mapping| if v.is_atom() { Ok(v.clone()) } else { laid_cells(v, 0, mapping, &self.layout, &Extents::EXACT) }; + Ok((left.map(|x| laid(x, &self.left)).transpose()?, laid(right, &self.right)?)) } } @@ -51,9 +51,10 @@ fn with_renderer(core: Result, left: &Layout, right: &Layout) -> Re Ok(core?.map_layout(|core| Layout::paired(core, left, right))) } -/// The tangent `t` laid out as `layout`, its primal's: repeated where it was smaller, as broadcasting repeats it. A linear operation, whose transpose sums back. A dual keeps the smaller tangent that a pervasive rule gives, which later calls broadcast as they go, and only code that copies items expands it (`Dual::full_tangent`). -pub(crate) fn expand(t: &Value, layout: &crate::array::Layout) -> Value { - Agreement::new(layout, t.layout()).expect("a tangent agrees with its primal").laid(None, t).1 +/// The tangent `t` laid out as `layout`, its primal's, whose lengths follow a running trace's extents where `extents` sets them (`Extents::of` of the primal): repeated where it was smaller, as broadcasting repeats it. A linear operation, whose transpose sums back. A dual keeps the smaller tangent that a pervasive rule gives, which later calls broadcast as they go, and only code that copies items expands it (`Dual::full_tangent`). +pub(crate) fn expand(t: &Value, layout: &crate::array::Layout, extents: &Extents) -> Value { + let agreement = Agreement::new(layout, t.layout()).expect("a tangent agrees with its primal"); + laid_cells(t, 0, &agreement.right, &agreement.layout, extents).expect("an agreement's layout fits") } /// The tables that `primitive::copied` reads to lay an argument with cells of shape `cell` over `frame`: under each position of `frame`, the cell that `mapping` reads there. The result has shape `frame` followed by `cell`. @@ -64,18 +65,39 @@ pub(crate) fn broadcast_tables(mapping: &Mapping, frame: &[usize], cell: &[usize Steps::with_base(base * size, steps.into_iter().map(|s| s.scaled(size)).chain(cells).collect()) } -/// `value`, whose cells have rank `rank`, laid out as `frame` followed by its cells' axes: at each position of `frame`, the cell that `mapping` reads there, as broadcasting reads it. Where `mapping` gives a fill, as for a key that `value` lacks, the cell holds fills. An array copies as a structural glyph copies, through `primitive::copied`, which moves a dual's primal and tangent together and copies a record as one `Op::Copy`. An atom fills the frame. An error where the result would pass the size limit. -pub(crate) fn laid_cells(value: &Value, rank: usize, mapping: &Mapping, frame: &Layout) -> Result { +/// `value`, whose cells have rank `rank`, laid out as `frame` followed by its cells' axes: at each position of `frame`, the cell that `mapping` reads there, as broadcasting reads it. Where `mapping` gives a fill, as for a key that `value` lacks, the cell holds fills. An array copies as a structural glyph copies, through `primitive::copied`, which moves a dual's primal and tangent together and copies a record as one `Op::Copy`. An atom fills the frame where a call of its size computes at once (`fusion::waits`), and otherwise lays as a scalar that holds it, whose copy at stride 0 waits as any large copy does and stores no items. `extents` sets the lengths of `frame` that follow a running trace's extents, as a frame sized from a traced value carries them (`Extents::of`). An error where the result would pass the size limit. +pub(crate) fn laid_cells(value: &Value, rank: usize, mapping: &Mapping, frame: &Layout, extents: &Extents) -> Result { + let scalar; + let value = if value.is_atom() { + generated_len(frame.shape())?; + if !crate::fusion::waits(frame.count(), || value.dtype().map_or(8, rustymath::buffer::DType::bytes)) { return Ok(frame.filled(value)); } + scalar = Layout::default().filled(value); + &scalar + } else { value }; let n = value.shape().len(); let layout = frame.concat(&value.layout().axes(n - rank..n)); - if value.is_atom() { generated_len(layout.shape())?; return Ok(layout.filled(value)); } // A value whose frame is `frame` already holds each cell where the mapping reads it. if *value.layout().axes(0..n - rank) == **frame { return value.relaid(layout); } - crate::primitive::copied(value, layout, &mapping.with_steps(0, broadcast_tables(mapping, frame.shape(), &value.shape()[n - rank..]))) + let extents = extents.padded(layout.shape().len()); + crate::primitive::copied_to(value, layout, &extents, &mapping.with_steps(0, broadcast_tables(mapping, frame.shape(), &value.shape()[n - rank..]))) +} + +/// `value` laid over `frame` through `mapping`, as `laid_cells` lays it, keeping length 1 along each axis of `frame` where `mapping` reads one cell at every position: the form a framed value takes in a map's run, where readers expand it as broadcasting does (main's decision 23). An axis kept at length 1 drops its keys. A view where the kept axes read `value`'s items in order. `laid_cells` where `mapping` reads no axis that way, or has no entry for each axis. +pub(crate) fn kept_cells(value: &Value, rank: usize, mapping: &Mapping, frame: &Layout) -> Result { + if value.shape()[..value.shape().len() - rank] == *frame.shape() { return laid_cells(value, rank, mapping, frame, &Extents::EXACT); } + let repeats = |s: &Steps| matches!(*s, Steps::Stride { step: 0, len, .. } if len > 1); + let Some((base, steps)) = mapping.entries(frame.shape()).filter(|(_, steps)| steps.iter().any(repeats)) else { return laid_cells(value, rank, mapping, frame, &Extents::EXACT) }; + let kept: Vec = steps.iter().map(|s| !repeats(s)).collect(); + let shape = frame.shape().iter().zip(&kept).map(|(&len, &k)| if k { len } else { 1 }).collect::>(); + let keys = (0..kept.len()).map(|a| frame.keys(a).filter(|_| kept[a]).cloned()).collect(); + let names = (0..kept.len()).map(|a| frame.name(a).cloned()).collect(); + let layout = Layout::from(shape).with_keys(keys).expect("each kept axis keeps its keys").inherit_names(names); + let steps = steps.into_iter().map(|s| match s { Steps::Stride { start, step: 0, .. } => Steps::Stride { start, len: 1, step: 0 }, s => s }).collect(); + laid_cells(value, rank, &mapping.with_steps(base, steps), &layout, &Extents::EXACT) } /// `value` repeated at each position of `frame`: an array with `frame`'s axes, then `value`'s. An error where the result would pass the size limit. -pub(crate) fn spread(value: &Value, frame: &Layout) -> Result { laid_cells(value, value.shape().len(), &Mapping::Single, frame) } +pub(crate) fn spread(value: &Value, frame: &Layout) -> Result { laid_cells(value, value.shape().len(), &Mapping::Single, frame, &Extents::EXACT) } /// A view of `a` with axes of length 1 after each run of its leading axes, up to the length of the matching run in the other argument. `frame` holds the lengths of `a`'s runs and `other` the other argument's. Agreement extends an axis of length 1 over the other argument's axis, so each run of `a`'s axes pairs with the matching run of the other's. No axes of length 1 follow `a`'s last axis, because leading-axis agreement already extends `a` over trailing axes. Each axis keeps its keys and name. pub(crate) fn padded(a: &Value, frame: &[usize], other: &[usize]) -> Value { diff --git a/src/array.rs b/src/array.rs index 0471e6bb..9110efca 100644 --- a/src/array.rs +++ b/src/array.rs @@ -1,12 +1,7 @@ -use crate::execution::check; -use crate::{ - Described, Error, ErrorKind, Number, display::{Elide, positions}, element::{Stored, Whole}, fusion::Pending, keyed::Keys, number::{Numeric, int::Int, real}, - transform::Carrier -}; -use num_complex::{Complex, Complex32, Complex64}; +use crate::{Error, ErrorKind, Number, display::{Elide, positions}, eval::{Function, OperatorValue}, graph::Pending, keyed::Keys, number::Numeric}; use rustymath::{ - agreement::Mapping, buffer::{Buffer, ComplexBuf, DType, Element, Fill, FloatBuf, FloatWidth, Floats, IntBuf, Ints, View, Width}, error::Kind, - gather::Writes, search::Key, with_float_width, with_width + agreement::Mapping, buffer::{Buffer, ComplexBuf, DType, Element, FloatBuf, FloatWidth, Floats, IntBuf, View, Width}, + error::Kind, facts::Range, with_width, }; use std::{borrow::Cow, fmt, sync::Arc}; @@ -15,18 +10,63 @@ mod text; pub(crate) use {storage::*, text::*}; pub use rustymath::layout::Shape; -pub(crate) use rustymath::layout::agreed; +pub(crate) use rustymath::layout::{agreed, count, element_count, generated_len, Lanes}; -/// A number, character, array, function or operator. An atom that a running level carries, such as a number that `∂` is differentiating, is `Carried` (`transform::Carrier`). -#[derive(Clone, Debug)] +/// Part 1: a value: an atom (llms.txt "Terms": number, character, function, operator), an array, a pending value, or a level's carried value. Arrays, operators and carried values are shared handles, so cloning one costs an `Arc` clone (cost model: 5). No `Eq` or `Hash`: interning, the store's index and program keys each compare values by their own function. +#[derive(Clone)] pub enum Value { - Number(Number), Character(char), Array(Arc), Function(crate::Function), Operator(crate::Operator), Carried(crate::transform::Carrier) + /// Part 1: a number atom. + Number(Number), + /// Part 1: a character atom. + Character(char), + /// Part 1: a function atom. + Function(Function), + /// Part 1: an operator atom. + Operator(Arc), + /// Part 1: an array, shared; a write in place needs the only handle. + Array(Arc), + /// Part 1: a pending value, an atom or an array alike, its atom flag in its node's description, as a fold of a pending array to an atom gives (part 5); a call reading it reads its node. + Pending(Pending), + /// Interfaces "Levels", part 1: a level's value, dual or batch; `Value::Carried` is its 1 home. + Carried { + /// Interfaces "Levels": the id of its level. + level: usize, + /// Part 1: what the level carries, shared. + carried: Arc, + }, +} + +/// Parts 1, 11: what a level carries, dual or batch; there are no other carriers (part 11). +pub(crate) enum Carried { + /// Part 15: a `Derivative` level's dual. + Dual { + /// Part 15: the primal. + primal: Value, + /// Part 15: the tangent, a linear node of the primal's kind and width, or an older level's value. + tangent: Value, + }, + /// Part 12: a `Batched` level's framed value. + Batch { + /// Part 12: the whole, keeping each frame axis in order, length 1 where it doesn't vary. + whole: Value, + cell: crate::program::Desc, + }, } /// An array's layout: rustymath's shape, keys and names, plus the BPL function that renders the array for display. Methods that change only keys or names keep the renderer, and so does `select`. A structural copy keeps its argument's renderer when the rank doesn't change (`rearranged`). Pairing two layouts by broadcasting keeps the renderer they agree on (`paired`). Layouts built from shapes, from rustymath's layouts, or from axes have none. -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default)] pub(crate) struct Layout { core: rustymath::layout::Layout, renderer: Option } +/// Compares layouts, the renderer by identity, as `Function` has no equality. +impl PartialEq for Layout { fn eq(&self, other: &Layout) -> bool { self.core == other.core && self.renderer.as_ref().map(Function::address) == other.renderer.as_ref().map(Function::address) } } +impl Eq for Layout {} +impl std::hash::Hash for Layout { + fn hash(&self, state: &mut H) { + std::hash::Hash::hash(self.shape(), state); + std::hash::Hash::hash(self.names(), state); + } +} + impl From for Layout { fn from(core: rustymath::layout::Layout) -> Self { Self::bare(core) } } impl From for Layout { fn from(shape: Shape) -> Self { Self::bare(shape.into()) } } impl From> for Layout { fn from(shape: Vec) -> Self { Self::bare(shape.into()) } } @@ -51,6 +91,10 @@ impl Layout { pub(crate) fn paired(core: rustymath::layout::Layout, left: &Self, right: &Self) -> Self { Self::kept(core, agreed([left.renderer(), right.renderer()])) } pub fn renderer(&self) -> Option<&crate::Function> { self.renderer.as_ref() } pub fn with_renderer(self, renderer: Option) -> Self { Self { core: self.core, renderer } } + /// This layout with lengths `shape`, keeping its keys, names and renderer: each axis with keys keeps its length. + pub(crate) fn resized(&self, shape: impl Into) -> Self { Self::kept(self.core.resized(shape), self.renderer.clone()) } + /// Whether `other` has this layout's keys, names and renderer, whatever its lengths. + pub(crate) fn same_but_lengths(&self, other: &Self) -> bool { self.core.same_but_lengths(&other.core) && self.renderer == other.renderer } /// Whether the layout has no keys, names or renderer. pub fn plain(&self) -> bool { self.core.plain() && self.renderer.is_none() } pub fn with_names(self, names: Vec>>) -> Result { Ok(Self::kept(self.core.with_names(names)?, self.renderer)) } @@ -72,36 +116,39 @@ impl Layout { } /// Whether one of the first `frame` axes has a name. pub(crate) fn frame_named(&self, frame: usize) -> bool { self.names().iter().take(frame).any(Option::is_some) } - pub fn replace(&self, axes: std::ops::Range, other: &Self) -> Self { Self::bare(self.core.replace(axes, other)) } /// The array with this layout holding `items`. An empty array takes `prototype`. pub fn collect(&self, items: impl IntoIterator, prototype: impl Prototype) -> Result { - let items = items.into_iter(); - let mut data = Gather::items(items.size_hint().0); - for item in items { data.add(item); } - if data.len() != element_count(self.shape())? { return Err(Kind::Length.into()); } - data.finish(self.clone(), prototype) + Value::stored_values(self.clone(), items.into_iter().collect(), Some(prototype.value()), Widening::Items) } - /// The array with this layout holding a copy of `item` at every position, in compact storage where `item`'s kind allows it. An empty array takes `item`'s prototype. pub(crate) fn filled(&self, item: &Value) -> Value { - let n = self.count(); - let mut data = Gather::new(&[item], n); - data.fill(item, n); - data.finish(self.clone(), || item.prototype()).expect("copies of one item fill their layout") + Value::stored_values(self.clone(), vec![item.clone(); self.count()], Some(item.prototype()), Widening::Arrays) + .expect("copies of a stored item fill the layout") } /// The array with this layout holding the integers `data`, in integer storage. - pub fn integers(&self, data: Vec) -> Result { Value::integers(self.shape(), data)?.with_layout(self.clone()) } - pub fn booleans(&self, data: Vec) -> Result { Value::booleans(self.shape(), data)?.with_layout(self.clone()) } - pub fn floats(&self, width: FloatWidth, data: impl IntoIterator) -> Result { - Value::floats(self.shape(), width, data)?.with_layout(self.clone()) + pub fn integers(&self, items: Vec) -> Result { + Value::stored_buffer(self.clone(), Buffer::Integers(IntBuf::I64(items)), crate::number::Kind::stored(DType::Integer(Width::I64), false, false), None) } - /// The layout that cells share when assembled by trailing-axis agreement. Shorter cells pad with fill; keys survive only along unpadded axes, and keys and names must agree. - pub(crate) fn assembled<'a>(cells: impl Iterator + Clone) -> Result { - let rank = cells.clone().map(|c| c.shape().len()).max().unwrap_or(0); + pub fn booleans(&self, items: Vec) -> Result { + Value::stored_buffer(self.clone(), Buffer::Booleans(items), crate::number::Kind::stored(DType::Boolean, false, false), None) + } + pub fn floats(&self, width: FloatWidth, items: impl IntoIterator) -> Result { + Value::stored_buffer(self.clone(), Buffer::Floats(FloatBuf::collect(width, items.into_iter())), + crate::number::Kind::stored(DType::Float(width), false, false), None) + } + /// The shape that cells of `shapes` share when assembled by trailing-axis agreement: each axis as long as the longest cell's, where a cell of lower rank has leading axes of length 1. + pub(crate) fn assembled_shape<'a>(shapes: impl Iterator + Clone) -> Vec { + let rank = shapes.clone().map(<[usize]>::len).max().unwrap_or(0); let mut shape = vec![0; rank]; - for cell in cells.clone() { - let pad = rank - cell.shape().len(); - for (axis, len) in shape.iter_mut().enumerate() { *len = (*len).max(if axis < pad { 1 } else { cell.shape()[axis - pad] }); } + for cell in shapes { + let pad = rank - cell.len(); + for (axis, len) in shape.iter_mut().enumerate() { *len = (*len).max(if axis < pad { 1 } else { cell[axis - pad] }); } } + shape + } + /// The layout that cells share when assembled by trailing-axis agreement (`assembled_shape`). Shorter cells pad with fill; keys survive only along unpadded axes, and keys and names must agree. + pub(crate) fn assembled<'a>(cells: impl Iterator + Clone) -> Result { + let shape = Self::assembled_shape(cells.clone().map(|c| c.shape())); + let rank = shape.len(); let (mut keys, mut names) = (Vec::with_capacity(rank), Vec::with_capacity(rank)); for axis in 0..rank { let cell_axis = |cell: &Self| axis.checked_sub(rank - cell.shape().len()); @@ -112,8 +159,8 @@ impl Layout { Ok(Self::from(shape).with_keys(keys)?.inherit_names(names)) } /// The array with this layout holding `cells`, as `Value::assemble` gives it. - pub fn assemble(&self, cells: &[Value], empty_cell: &Value) -> Result { - Value::assemble_layout(self, cells, empty_cell, Widening::Items, None) + pub fn assemble(&self, cells: &[Value], empty_cell: &Value, widening: Widening) -> crate::error::Result { + crate::fusion::joined(self, cells, empty_cell, widening) } } @@ -138,14 +185,14 @@ impl Frame { } } /// Positions on this frame, each below `bound`. `bound` gives their width, with no pass over them to find it. A direct application gives its one position as an atom. - pub fn positions(self, mut items: impl ExactSizeIterator, bound: usize) -> Result { + pub fn positions(self, mut items: impl ExactSizeIterator, bound: usize) -> crate::error::Result { match (self, items.len()) { (Self::Direct, 1) => Ok(Value::integer(items.next().unwrap() as i64)), (Self::Direct, _) => Err(Kind::Length.into()), (Self::Array(layout), _) => Value::positions(layout.shape(), bound, items)?.with_layout(layout) } } /// `positions` from a buffer of `u32`s. 32-bit results keep the buffer, with no pass over it. - pub fn positions_u32(self, data: Vec, bound: usize) -> Result { + pub fn positions_u32(self, data: Vec, bound: usize) -> crate::error::Result { match self { Self::Direct => Self::Direct.positions(data.into_iter().map(|p| p as usize), bound), Self::Array(layout) => Value::positions_u32(layout.shape(), bound, data)?.with_layout(layout), @@ -166,895 +213,607 @@ impl Cells<'_> { pub fn frame_layout(&self) -> Layout { self.array.layout().axes(0..self.split) } pub fn shape(&self) -> &[usize] { &self.array.shape()[self.split..] } pub fn len(&self) -> usize { self.count } - pub fn get(&self, i: usize) -> Result { self.read(i, Value::at) } - /// Cell `i`, with `item` reading a rank-0 cell's one item: the whole array where there is no frame, or else a part of it. - fn read(&self, i: usize, item: impl Fn(&Value, usize) -> Value) -> Result { + pub fn get(&self, i: usize) -> crate::error::Result { if self.split == 0 { return Ok(self.array.clone()); } - if self.shape().is_empty() { return item(self.array, i).unit(); } self.array.part(i * self.size..(i + 1) * self.size, self.cell_layout.clone()) } - pub fn prototype(&self) -> Result { - if self.shape().is_empty() { return self.array.prototype().unit(); } - let len = generated_len(self.shape())?; - self.cell_layout.collect(vec![self.array.prototype(); len], self.array.prototype()) + pub fn prototype(&self) -> crate::error::Result { + Ok(self.cell_layout.filled(&self.array.prototype())) } - pub fn framed(&self) -> Result { - if self.split > 0 && self.shape().is_empty() && self.array.is_simple() { return Ok(self.array.clone()); } - let values = (0..self.len()).map(|i| self.get(i)).collect::, _>>()?; - self.frame_layout().collect(values, self.prototype()?) + pub fn framed(&self) -> crate::error::Result { + let values = self.collect()?; + crate::fusion::joined(&self.frame_layout(), &values, &self.prototype()?, Widening::Arrays) } - /// Every cell, as bulk iteration reads items (`Value::element`). - pub fn collect(&self) -> Result, ErrorKind> { (0..self.len()).map(|i| self.read(i, Value::element)).collect() } + pub fn collect(&self) -> crate::error::Result> { (0..self.len()).map(|i| self.get(i)).collect() } } +/// Part 1: an array's stored data, which `Value::Array` shares; a pending array is a `Value::Pending`, so no variant here is pending. #[derive(Clone, Debug)] -pub struct ArrayData { layout: Layout, data: Data } +pub enum ArrayData { + Compact { layout: Layout, kind: crate::number::Kind, buffer: SharedBuffer, range: Option }, + Segmented { layout: Layout, segments: Segments }, + Mixed { layout: Layout, items: Box<[Value]>, prototype: Option }, +} -/// An array's items. `Of` shares the items in a range of a base array's, with no copy: the whole base for a view under a new layout, or one part's range (`Value::part`). A view keeps its whole base alive, as a NumPy view does. `Node` holds the pending call that computes them on the first read (`fusion::Pending`). `Carried` holds a running level's data in their place (`transform::Carrier`): a dual's primal and tangent, or a record of linear calls, whose items are never computed. A base holds `Own` or `Node`. #[derive(Clone, Debug)] -enum Data { Own(Storage), Of(Arc, std::ops::Range, bool), Node(Arc), Carried(Carrier) } +pub(crate) enum SharedBuffer { Owned(Buffer), View { base: Arc, map: Mapping } } -impl ArrayData { - fn array(shape: Shape, data: Storage) -> Value { Value::Array(Arc::new(Self { layout: shape.into(), data: Data::Own(data) })) } - /// The storage that holds this array's items. A view shares its base's, so read a view's items through `items`. What compact storage says of itself as a whole, such as its prototype, holds for any range of it. - fn storage(&self) -> &Storage { - match &self.data { - Data::Own(storage) => storage, Data::Of(base, ..) => base.storage(), Data::Node(pending) => forced(pending), - Data::Carried(carrier) => carrier.storage() - } + +/// The containers of flattened arguments. Data, including empty prototypes, remains in leaves. +#[derive(Clone, PartialEq, Eq, Hash)] +pub(crate) struct Trees(Vec); + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub(crate) struct TreeRoot { pub(crate) index: usize, pub(crate) leaves: std::ops::Range } +impl TreeRoot { + pub(crate) fn shifted(&self, roots: usize, leaves: usize) -> Self { + Self { index: self.index + roots, leaves: self.leaves.start + leaves..self.leaves.end + leaves } } - fn dtype(&self) -> Option { - match &self.data { - Data::Own(storage) => storage.dtype(), Data::Of(base, ..) => base.dtype(), Data::Node(p) => Some(p.dtype()), Data::Carried(_) => None +} + +#[derive(Clone, PartialEq, Eq, Hash)] +enum Tree { Leaf, Array { layout: Layout, items: Vec, prototype: Option> } } + +pub(crate) fn flatten(args: &[Value]) -> Option<(Arc, Vec)> { + if !args.iter().any(|value| matches!(value, Value::Array(array) if matches!(&**array, ArrayData::Mixed { .. }))) { return None; } + let mut leaves = Vec::new(); + let trees = args.iter().map(|value| Tree::of(value, &mut leaves)).collect(); + Some((Arc::new(Trees(trees)), leaves)) +} + + +impl Tree { + fn of(value: &Value, leaves: &mut Vec) -> Self { + if let Value::Array(array) = value && let ArrayData::Mixed { layout, items, prototype } = &**array { + return Self::Array { layout: layout.clone(), items: items.iter().map(|v| Self::of(v, leaves)).collect(), + prototype: prototype.as_ref().map(|v| Box::new(Self::of(v, leaves))) }; } + leaves.push(value.clone()); + Self::Leaf } - /// Whether mixed storage holds the items, directly or as a view's base's. A pending array computes compact items, and a carrier holds a running level's data. - fn mixed(&self) -> bool { - match &self.data { - Data::Own(storage) => matches!(storage, Storage::Mixed(..)), Data::Of(base, ..) => base.mixed(), Data::Node(_) | Data::Carried(_) => false - } + + fn build(&self, leaves: &mut impl Iterator) -> Value { + self.read(&mut || Ok::<_, std::convert::Infallible>(leaves.next().expect("one binding per tree leaf"))).unwrap() } - fn fixed_width(&self) -> Option { - match &self.data { - Data::Own(storage) => storage.fixed_width(), - Data::Of(base, _, fixed) => fixed.then(|| match base.dtype() { - Some(DType::Integer(w)) => w, _ => unreachable!("only integer views have a fixed width") - }), - Data::Node(pending) => pending.prototype().fixed_width(), - Data::Carried(carrier) => carrier.template().fixed_width(), - } + fn read(&self, leaf: &mut impl FnMut() -> Result) -> Result { + Ok(match self { + Self::Leaf => return leaf(), + Self::Array { layout, items, prototype } => Value::Array(Arc::new(ArrayData::Mixed { + layout: layout.clone(), items: items.iter().map(|tree| tree.read(leaf)).collect::>()?, + prototype: prototype.as_ref().map(|tree| tree.read(leaf)).transpose()?, + })), + }) } - /// This array's items: for a view, its range of its base's. Owned storage, the common case, reads inline. - #[inline(always)] - fn items(&self) -> Items<'_> { if let Data::Own(storage) = &self.data { storage.items() } else { self.shared_items() } } - /// The items of a view, a pending array or a carried array. A view's base is never a view, so it reads its base's storage. - fn shared_items(&self) -> Items<'_> { - match &self.data { - Data::Of(base, range, fixed) => { - let items = base.storage().items().fixed(*fixed); - if range.len() == items.len() { items } else { items.slice(range.clone()) } - } - Data::Carried(carrier) => carrier.items(), - _ => self.storage().items(), - } + + fn len(&self) -> usize { + match self { Self::Leaf => 1, Self::Array { items, prototype, .. } => + items.iter().map(Self::len).sum::() + prototype.as_ref().map_or(0, |t| t.len()) } } - #[inline] - fn len(&self) -> usize { self.layout.count() } - fn into_storage(self) -> Storage { Self::owned(self.data) } - /// The items that `data` holds: taken with no copy where nothing else holds them, and copied otherwise. A pending array computes them first. - fn owned(data: Data) -> Storage { - match data { - Data::Own(storage) => storage, - Data::Of(base, range, fixed) if range.len() == base.len() => { - Arc::try_unwrap(base).map_or_else(|base| base.storage().clone(), Self::into_storage).fixed(fixed) - } - Data::Of(base, range, fixed) => Storage::copy_of(base.items().slice(range)).fixed(fixed), - Data::Node(pending) => Arc::try_unwrap(pending).map_or_else(|pending| forced(&pending).clone(), Pending::into_storage), - Data::Carried(Carrier::Labelled(labelled)) => labelled.values().clone().into_storage(), - Data::Carried(carrier) => carrier.storage().clone(), +} + +impl Trees { + pub(crate) fn leaves(count: usize) -> Self { Self(vec![Tree::Leaf; count]) } + pub(crate) fn push_leaf(&mut self) { self.0.push(Tree::Leaf); } + pub(crate) fn push(&mut self, value: &Value, leaves: &mut Vec) { self.0.push(Tree::of(value, leaves)); } + pub(crate) fn append_root(&mut self, source: &Trees, root: &TreeRoot) { self.0.push(source.root_at(root).clone()); } + fn root_at(&self, root: &TreeRoot) -> &Tree { + let tree = &self.0[root.index]; debug_assert_eq!(tree.len(), root.leaves.len()); tree + } + pub(crate) fn at_root<'a>(&self, root: &TreeRoot, leaves: &'a [Value]) -> Cow<'a, Value> { + match self.root_at(root) { + Tree::Leaf => Cow::Borrowed(&leaves[root.leaves.start]), + tree => Cow::Owned(tree.build(&mut leaves[root.leaves.clone()].iter().cloned())), } } - /// This array's storage, to change in place. A view or pending array takes its items where it holds the only reference to them, and copies them otherwise. - fn storage_mut(&mut self) -> &mut Storage { - if !matches!(self.data, Data::Own(_)) { - self.data = Data::Own(Self::owned(std::mem::replace(&mut self.data, Data::Own(Storage::Compact(Buffer::Booleans(Vec::new())))))); + pub(crate) fn read_root(&self, root: &TreeRoot, mut read_leaf: impl FnMut(usize) -> crate::error::Result) -> crate::error::Result { + let mut position = root.leaves.start; + self.root_at(root).read(&mut || { let at = position; position += 1; read_leaf(at) }) + } + pub(crate) fn concat(&self, other: &Trees) -> Trees { Self(self.0.iter().chain(&other.0).cloned().collect()) } + pub(crate) fn project_roots(&self, roots: &[TreeRoot]) -> Trees { Self(roots.iter().map(|root| self.root_at(root).clone()).collect()) } + pub(crate) fn without_root(&self, root: &TreeRoot) -> Self { + Self(self.0[..root.index].iter().chain(&self.0[root.index + 1..]).cloned().collect()) + } + pub(crate) fn build(&self, leaves: &[Value]) -> Vec { + let mut leaves = leaves.iter().cloned(); + self.0.iter().map(|tree| tree.build(&mut leaves)).collect() + } + + pub(crate) fn roots(&self) -> impl Iterator + '_ { + let mut start = 0; + self.0.iter().enumerate().map(move |(index, tree)| { + let end = start + tree.len(); let root = TreeRoot { index, leaves: start..end }; start = end; root + }) + } + pub(crate) fn leaf_ranges(&self) -> impl Iterator> + '_ { self.roots().map(|root| root.leaves) } +} + +impl ArrayData { + pub(crate) fn layout(&self) -> &Layout { + match self { Self::Compact { layout, .. } | Self::Segmented { layout, .. } | Self::Mixed { layout, .. } => layout } + } + pub(crate) fn len(&self) -> usize { self.layout().count() } + pub(crate) fn kind(&self) -> Option { + match self { Self::Compact { kind, .. } => Some(*kind), Self::Segmented { segments, .. } => segments.leaf.kind(), Self::Mixed { .. } => None } + } + pub(crate) fn storage(&self) -> Items<'_> { + match self { + Self::Compact { buffer: SharedBuffer::Owned(buffer), kind, .. } => + match (buffer.view(), kind.fixed_width()) { (View::Integers(ints), Some(_)) => Items::Fixed(ints), (view, _) => Items::Compact(view) }, + Self::Compact { buffer: SharedBuffer::View { base, map }, .. } => Items::Mapped { source: base, map, start: 0, len: self.len() }, + Self::Segmented { segments, .. } => Items::Segmented(segments.segs()), + Self::Mixed { items, .. } => Items::Values(items), } - let Data::Own(storage) = &mut self.data else { unreachable!("a view or pending array takes its storage above") }; - storage - } - /// An array of `layout` that shares `this`'s items, with no copy. A carried array gives a carrier of views of its parts (`Carrier::view`), so `Of` never wraps a carrier. - fn view(this: &Arc, layout: Layout) -> Value { - if let Data::Carried(carrier) = &this.data { return carrier.view(layout); } - Self::part(this, 0..this.len(), layout, this.fixed_width().is_some()) - } - /// An array of `layout` that shares the items in `range` of `this`'s, which isn't carried. The view reads `this`'s base, so views never chain. - fn part(this: &Arc, range: std::ops::Range, layout: Layout, fixed: bool) -> Value { - debug_assert_eq!(layout.count(), range.len()); - let (base, start) = match &this.data { Data::Of(base, of, _) => (base.clone(), of.start), _ => (this.clone(), 0) }; - Value::Array(Arc::new(Self { layout, data: Data::Of(base, start + range.start..start + range.end, fixed) })) - } - /// The call that computes this array's items, while they haven't been computed. A view answers for its base. - fn pending(&self) -> Option<&Arc> { - match &self.data { - Data::Node(pending) => Some(pending), Data::Of(base, ..) => base.pending(), Data::Own(_) | Data::Carried(_) => None - }.filter(|p| !p.is_forced()) - } - /// The running level's data that this array holds in place of items, if any. - fn carrier(&self) -> Option<&Carrier> { match &self.data { Data::Carried(carrier) => Some(carrier), _ => None } } - /// Whether a direct item is a function. Compact storage holds none, and a pending array computes numbers. - fn holds_functions(&self) -> bool { - match &self.data { - Data::Own(Storage::Compact(_) | Storage::Fixed(_)) | Data::Node(_) => false, Data::Own(storage) => storage.holds_functions(), - Data::Of(base, ..) => base.holds_functions(), Data::Carried(carrier) => carried_functions(carrier) + } + pub(crate) fn prototype(&self) -> Value { + match self { + Self::Mixed { items, prototype, .. } => prototype.clone().unwrap_or_else(|| items[0].zeroed()), + Self::Segmented { segments, .. } => segments.prototype().clone(), + Self::Compact { .. } => self.storage().prototype().expect("a compact kind has a prototype"), } } - /// Whether this array reads another array's items, as a view. - fn is_view(&self) -> bool { matches!(self.data, Data::Of(..)) } + pub(crate) fn physical(this: &Arc) -> (&Arc, Option<&Mapping>) { + match &**this { Self::Compact { buffer: SharedBuffer::View { base, map }, .. } => (base, Some(map)), _ => (this, None) } + } + pub(crate) fn segments(&self) -> Option> { + match self { Self::Segmented { segments, .. } => Some(segments.segs()), _ => None } + } } -/// The bytes of the allocations that a value keeps alive, as `Value::footprint` counts them. Each allocation counts once in `seen`. -pub(crate) trait Footprint { fn footprint_in(&self, seen: &mut Seen) -> usize; } - -/// The allocations that a footprint has counted, by address. pub(crate) type Seen = rustymath::hashing::HashSet; - +pub(crate) trait Footprint { fn footprint_in(&self, seen: &mut Seen) -> usize; } +pub(crate) fn unseen(this: &Arc, seen: &mut Seen) -> bool { seen.insert(Arc::as_ptr(this) as usize) } impl Footprint for Arc { fn footprint_in(&self, seen: &mut Seen) -> usize { if unseen(self, seen) { (**self).footprint_in(seen) } else { 0 } } } - -impl Footprint for std::sync::OnceLock { fn footprint_in(&self, seen: &mut Seen) -> usize { self.get().map_or(0, |v| v.footprint_in(seen)) } } - -/// Whether `this`'s allocation is new to `seen`, which then holds it. -pub(crate) fn unseen(this: &Arc, seen: &mut Seen) -> bool { seen.insert(Arc::as_ptr(this) as usize) } - impl Footprint for ArrayData { fn footprint_in(&self, seen: &mut Seen) -> usize { - match &self.data { - Data::Own(storage) => storage.footprint_in(seen), Data::Of(base, ..) => base.footprint_in(seen), Data::Node(pending) => pending.footprint_in(seen), - Data::Carried(carrier) => carrier.footprint_in(seen) + std::mem::size_of::() + match self { + Self::Compact { buffer: SharedBuffer::Owned(buffer), .. } => buffer.capacity() * buffer.dtype().bytes(), + Self::Compact { buffer: SharedBuffer::View { base, map }, .. } => base.footprint_in(seen) + map.footprint(), + Self::Segmented { segments, .. } => segments.leaf.footprint_in(seen), + Self::Mixed { items, prototype, .. } => items.iter().chain(prototype).map(|v| v.footprint_in(seen)).sum(), } } } - impl Footprint for Value { fn footprint_in(&self, seen: &mut Seen) -> usize { - match self { Self::Array(a) => a.footprint_in(seen), Self::Carried(carrier) => carrier.footprint_in(seen), _ => 0 } + match self { + Self::Array(array) => array.footprint_in(seen), Self::Pending(pending) => pending.node.footprint_in(seen), + Self::Carried { carried, .. } => if unseen(carried, seen) { + match &**carried { Carried::Dual { primal, tangent } => primal.footprint_in(seen) + tangent.footprint_in(seen), + Carried::Batch { whole, .. } => whole.footprint_in(seen) } + } else { 0 }, + _ => 0, + } } } -/// Drops `node`'s children in a loop, never by recursion, so that a long chain can't overflow the stack. `take` moves a node's children into the list. Each child that nothing else holds gives up its own children the same way before it drops. -pub(crate) fn drop_chain(node: &mut T, take: impl Fn(&mut T, &mut Vec>)) { - let mut chain = Vec::new(); - take(node, &mut chain); - while let Some(next) = chain.pop() { if let Ok(mut next) = Arc::try_unwrap(next) { take(&mut next, &mut chain); } } +pub(crate) fn map_leaves(value: &Value, mut f: impl FnMut(&Value) -> crate::error::Result) -> crate::error::Result { + let Some((trees, leaves)) = flatten(std::slice::from_ref(value)) else { return f(value) }; + let mapped = leaves.iter().map(&mut f).collect::>>()?; + if leaves.iter().zip(&mapped).all(|(old, new)| old.identical(new)) { return Ok(value.clone()); } + Ok(trees.build(&mapped).pop().expect("one input tree")) } -/// A pending array's items, computed on the first read. Cold and out of line, so that `ArrayData::storage`'s other arms stay small. -#[cold] -#[inline(never)] -fn forced(pending: &Pending) -> &Storage { pending.storage() } - -pub(crate) use rustymath::layout::{Lanes, count, element_count, generated_len}; -/// Whether the value that `carrier` stands for holds a function as a direct item. A batch asks its whole argument, which computes no cell, where its template would. -fn carried_functions(carrier: &Carrier) -> bool { match carrier { Carrier::Batch(b) => b.holds_functions(), carrier => carrier.template().holds_functions() } } - -/// The width of the floats that items of `dtype` hold, or of the parts of its complex numbers. +pub(crate) fn drop_chain(node: &mut T, take: impl Fn(&mut T, &mut Vec>)) { + let mut chain = Vec::new(); take(node, &mut chain); + while let Some(node) = chain.pop() { if let Ok(mut node) = Arc::try_unwrap(node) { take(&mut node, &mut chain); } } +} pub(crate) fn float_width_of(dtype: DType) -> Option { match dtype { DType::Float(width) => Some(width), DType::Complex(width) => Some(width.into()), _ => None } } +pub(crate) fn saturated(n: u64) -> usize { usize::try_from(n).unwrap_or(usize::MAX) } -/// Whether compact items `x` and `y` match, where `view_matches` compares their dtypes, with floats compared within the tolerance of the wider width. -pub(crate) fn compact_match(x: View<'_>, y: View<'_>, t: Numeric) -> Option { - rustymath::search::view_matches(x, y, crate::search::compared_at(float_width_of(x.dtype()).max(float_width_of(y.dtype())), t).1) +pub(crate) fn compact_match(left: View<'_>, right: View<'_>, numeric: Numeric) -> Option { + rustymath::search::view_matches(left, right, crate::search::compared_at(float_width_of(left.dtype()).max(float_width_of(right.dtype())), numeric).1) } -/// The count `n` as a size, or `usize::MAX` when `usize` can't hold it, which every limit on sizes then rejects. -pub(crate) fn saturated(n: u64) -> usize { usize::try_from(n).unwrap_or(usize::MAX) } +pub(crate) fn read_value(desc: &crate::program::Desc, read: &rustymath::agreement::Read<'_>, checked: bool) -> crate::error::Result { + let mut buffer = Buffer::with_capacity(desc.dtype(), read.plan.len()); + read.read_into(0..read.plan.len(), desc.dtype(), checked, &mut buffer)?; + Value::stored(desc, buffer) +} impl Value { - fn exact_domain(&self) -> Option { - if let Some(carrier) = self.carrier() { return carrier.template().exact_domain(); } + pub fn read(&self, read: crate::program::trace::ReadKind) -> Result { + map_leaves(self, |leaf| match leaf { + Self::Pending(pending) => pending.node.clone().into_storage(pending.output, read), + Self::Function(function) => Ok(Self::Function(function.observed(read)?)), + Self::Operator(operator) => Ok(Self::Operator(Arc::new(operator.observed(read)?))), + Self::Carried { level, carried } => match &**carried { + Carried::Dual { primal, .. } => primal.read(read), + Carried::Batch { .. } => Err(Error::batch_stop(*level)), + }, + Self::Array(array) if matches!(&**array, ArrayData::Compact { buffer: SharedBuffer::View { .. }, .. }) && leaf.view().is_none() => { + let (view, plan) = leaf.physical_read(0).expect("a stored view has a physical read"); + let sources = [rustymath::agreement::Binding::full(view)]; + read_value(&crate::program::Desc::leaf(leaf).unwrap(), &rustymath::agreement::Read { plan: &plan, sources: &sources }, true) + }, + Self::Array(array) if let ArrayData::Segmented { layout, segments } = &**array => { + let Value::Array(leaf) = Value::Array(segments.leaf.clone()).read(read)? else { unreachable!("a stored segment leaf is an array") }; + let prototype = segments.prototype().read(read)?; + if Arc::ptr_eq(&leaf, &segments.leaf) && prototype.identical(segments.prototype()) { return Ok(Self::Array(array.clone())); } + Ok(Self::Array(Arc::new(ArrayData::Segmented { layout: layout.clone(), segments: segments.with_parts(leaf, prototype) }))) + } + _ => Ok(leaf.clone()), + }) + } + pub(crate) fn read_borrowed(&self, read: crate::program::trace::ReadKind) -> Result, Error> { + let value = self.read(read)?; + Ok(if self.identical(&value) { Cow::Borrowed(self) } else { Cow::Owned(value) }) + } + + pub(crate) fn pending(&self) -> Option<&Pending> { if let Self::Pending(p) = self { Some(p) } else { None } } + pub(crate) fn level(&self) -> Option { self.level_with_pending(true) } + pub(crate) fn carried_level(&self) -> Option { self.level_with_pending(false) } + fn level_with_pending(&self, pending: bool) -> Option { + fn array(data: &ArrayData, pending: bool) -> Option { + match data { + ArrayData::Compact { .. } => None, + ArrayData::Segmented { segments, .. } => array(&segments.leaf, pending).max(segments.prototype().level_with_pending(pending)), + ArrayData::Mixed { items, prototype, .. } => items.iter().chain(prototype.iter()).filter_map(|value| value.level_with_pending(pending)).max(), + } + } match self { - Self::Number(n) => Some(n.is_exact()), Self::Array(a) => a.pending().map_or_else(|| a.storage().exact(), |p| p.dtype().exact()), _ => None + Self::Pending(value) => pending.then(|| value.node.level()).flatten(), Self::Carried { level, .. } => Some(*level), + Self::Array(data) => array(data, pending), _ => None, } } - /// This value with each number zero and each character blank, in the same layout. A function or an operator stays as it is. - pub fn zeroed(&self) -> Self { - if let Some(carrier) = self.carrier() { return carrier.zeroed(); } + pub(crate) fn kind(&self) -> Option { match self { - Self::Number(n) if n.as_bool().is_some() => Self::Number(Number::from_bool(false)), Self::Number(n) => Self::Number(n.zero()), - Self::Character(_) => Self::Character(' '), Self::Array(_) => self.zeroed_array(), _ => self.clone() + Self::Number(n) => crate::number::Kind::number(n), Self::Character(_) => Some(crate::number::Kind::stored(DType::Character, false, false)), + Self::Array(array) => array.kind(), Self::Pending(pending) => Some(pending.desc().kind), + Self::Carried { carried, .. } => match &**carried { Carried::Dual { primal, .. } => primal.kind(), Carried::Batch { cell, .. } => Some(cell.kind) }, + _ => None, } } - /// Whether the values are identical: the same kinds, fixed widths, keys, names and renderer, and the same items bit for bit, whatever a number's weakness (`Number::identical`). `¯0` differs from `0`, and a NaN is identical to a NaN with the same bits. Internal checks use it, and `≡` uses `matches`. - pub fn same(&self, other: &Self) -> bool { - match (self.carrier(), other.carrier()) { (Some(a), Some(b)) => return a.same(b), (None, None) => {} _ => return false } - match (self, other) { - (Self::Number(x), Self::Number(y)) => x.identical(y), - (Self::Character(x), Self::Character(y)) => x == y, - (Self::Array(x), Self::Array(y)) => { - Arc::ptr_eq(x, y) || (x.layout == y.layout && same_items(x.items(), y.items(), || (x.storage().prototype(), y.storage().prototype()))) - } - (Self::Function(x), Self::Function(y)) => x == y, - (Self::Operator(x), Self::Operator(y)) => x == y, - _ => false, + pub(crate) fn range(&self) -> Option { + match self { + Self::Number(n) => Some(n.lossless_float().map_or_else(|| n.dtype().map_or(Range::ANY, Range::of_dtype), |n| Range::of(Some(n)))), + Self::Character(_) => None, + Self::Array(array) => match &**array { + ArrayData::Compact { range, .. } => *range, ArrayData::Segmented { segments, .. } => Value::Array(segments.leaf.clone()).range(), _ => None, + }, + Self::Pending(pending) => pending.desc().range, + Self::Carried { carried, .. } => match &**carried { Carried::Dual { primal, .. } => primal.range(), Carried::Batch { cell, .. } => cell.range }, + _ => None, } } - /// Whether the values match, as `≡` and search compare them, within the session's tolerance. - pub(crate) fn matches(&self, other: &Self, span: &crate::Span) -> Result { self.matches_within(other, Numeric::current(), span) } - /// The items of an array that owns compact items, not empty, under a layout with no names or keys. - fn owned_compact(&self) -> Option> { - let Self::Array(a) = self else { return None }; - let Data::Own(storage) = &a.data else { return None }; - (a.layout.plain() && storage.items().len() > 0).then(|| storage.items().view()).flatten() - } - /// Whether the values match, with each pair of numbers within the tolerance of the width they compare at. Numbers match whatever their exactness, and NaN matches NaN. Two axes match as arithmetic pairs them: their names don't conflict, as `rustymath::layout::names_conflict` says, and when both have keys, they hold the same keys in any order. Items along keyed axes compare by key. Functions and operators match when built from matching parts. Renderers take no part. - pub(crate) fn matches_within(&self, other: &Self, t: Numeric, span: &crate::Span) -> Result { - check()?; - // Arrays that own compact items under plain layouts, as most searched cells are, match by shape and items, without the observation and layout checks below. - if let (Some(x), Some(y)) = (self.owned_compact(), other.owned_compact()) { - if self.shape() != other.shape() { return Ok(false); } - if let Some(matched) = compact_match(x, y, t) { return Ok(matched); } - } - use crate::transform::Plain; - match (self.observed()?, other.observed()?) { - (Plain::Array(_), Plain::Array(_)) => (), (Plain::Number(x), Plain::Number(y)) => return x.matches(y, t).map_err(Error::from), - (Plain::Character(x), Plain::Character(y)) => return Ok(x == y), (Plain::Function(x), Plain::Function(y)) => return x.matches(y, t, span), - (Plain::Operator(x), Plain::Operator(y)) => return x.matches(y, t, span), _ => return Ok(false) - } - if !self.layout().matches(other.layout()) { return Ok(false); } - let keyed = self.has_keys() || other.has_keys(); - if self.is_empty() { return self.prototype().matches_within(&other.prototype(), t, span); } - if !keyed { - if let (Some(x), Some(y)) = (self.view(), other.view()) { if let Some(matched) = compact_match(x, y, t) { return Ok(matched); } } + pub fn is_atom(&self) -> bool { + match self { Self::Array(_) => false, Self::Pending(p) => p.desc().atom, + Self::Carried { carried, .. } => match &**carried { Carried::Dual { primal, .. } => primal.is_atom(), Carried::Batch { cell, .. } => cell.atom }, _ => true } + } + pub(crate) fn layout(&self) -> &Layout { + static ATOM: Layout = Layout::EMPTY; + match self { + Self::Array(array) => array.layout(), Self::Pending(p) => &p.desc().layout, + Self::Carried { carried, .. } => match &**carried { Carried::Dual { primal, .. } => primal.layout(), Carried::Batch { cell, .. } => &cell.layout }, + _ => &ATOM, } - let mapping = keyed.then(|| Mapping::new(other.layout(), &(0..self.shape().len()).collect::>(), self.layout(), self.len())); - for (i, x) in self.elements().enumerate() { - let j = mapping.as_ref().map_or(i, |m| m.index(i)); - if !x.matches_within(&other.at(j), t, span)? { return Ok(false); } + } + pub fn shape(&self) -> &[usize] { self.layout().shape() } + pub fn rank(&self) -> usize { self.shape().len() } + pub fn len(&self) -> usize { if self.is_atom() { 1 } else { self.layout().count() } } + pub fn is_empty(&self) -> bool { self.len() == 0 } + pub fn is_unit(&self) -> bool { self.shape().is_empty() } + pub fn is_singleton(&self) -> bool { self.len() == 1 } + pub fn core_layout(&self) -> &rustymath::layout::Layout { &self.layout().core } + pub(crate) fn dtype(&self) -> Option { self.kind().map(crate::number::Kind::dtype) } + pub(crate) fn fixed_width(&self) -> Option { self.kind().and_then(crate::number::Kind::fixed_width) } + pub(crate) fn float_width(&self) -> Option { self.kind().and_then(crate::number::Kind::float_width) } + pub fn is_exact(&self) -> bool { + self.kind().map_or_else(|| match self { + Self::Number(n) => n.is_exact(), Self::Array(a) => match &**a { ArrayData::Mixed { items, prototype, .. } => + items.iter().chain(prototype).all(Self::is_exact), _ => false }, _ => false, + }, crate::number::Kind::is_exact) + } + pub fn is_integral(&self) -> bool { + self.dtype().is_some_and(DType::is_integral) || matches!(self, Self::Number(n) if n.is_integral()) + } + pub(crate) fn holds_values(&self) -> bool { matches!(self, Self::Array(a) if matches!(&**a, ArrayData::Mixed { .. } | ArrayData::Segmented { .. })) } + pub(crate) fn holds_mixed(&self) -> bool { matches!(self, Self::Array(a) if matches!(&**a, ArrayData::Mixed { .. })) } + pub(crate) fn is_simple(&self) -> bool { self.kind().is_some() && !matches!(self, Self::Array(a) if matches!(&**a, ArrayData::Segmented { .. })) } + pub(crate) fn ragged(&self) -> bool { matches!(self, Self::Array(a) if a.segments().is_some()) } + pub fn holds_functions(&self) -> bool { + match self { + Self::Function(_) | Self::Operator(_) => true, + Self::Array(a) => match &**a { + ArrayData::Mixed { items, prototype, .. } => items.iter().chain(prototype).any(Self::holds_functions), + ArrayData::Segmented { segments, .. } => segments.prototype().holds_functions(), + _ => false, + }, + Self::Carried { carried, .. } => match &**carried { Carried::Dual { primal, .. } => primal.holds_functions(), Carried::Batch { whole, .. } => whole.holds_functions() }, + _ => false, } - Ok(true) } pub(crate) fn environment(&self) -> Option { match self { Self::Function(f) => f.environment(), Self::Operator(op) => op.environment(), - Self::Array(a) if a.pending().is_some() || a.carrier().is_some() => None, Self::Array(a) => a.storage().environment().map(usize::from), _ => None + Self::Array(a) => match &**a { ArrayData::Mixed { items, prototype, .. } => items.iter().chain(prototype).filter_map(Self::environment).max(), _ => None }, + _ => None, } } - pub fn is_atom(&self) -> bool { !matches!(self, Self::Array(_)) } - pub fn enclose(&self) -> Result { Self::from_storage(Shape::EMPTY, Storage::one(self)) } - /// This value as a unit: an atom is itself, and an array is enclosed. A 0-cell inside a frame is the unit of its item. - pub(crate) fn unit(self) -> Result { if self.is_atom() { Ok(self) } else { self.enclose() } } - pub(crate) fn storage_id(&self) -> usize { match self { Self::Array(a) => Arc::as_ptr(a) as usize, _ => 0 } } - pub(crate) fn storage(&self) -> Option<&Storage> { match self { Self::Array(a) => Some(a.storage()), _ => None } } - /// The pending call that computes this array's items, while they haven't been computed. - pub(crate) fn pending(&self) -> Option<&Arc> { match self { Self::Array(a) => a.pending(), _ => None } } - /// The pending call of an array that no other array holds, which a fold can read block by block without building it. The array isn't a view. A caller that only borrows the array can still read it after the fold, so the fold must leave the array able to compute its items. - pub(crate) fn unshared_pending(&self) -> Option<&Arc> { - let Self::Array(a) = self else { return None }; - if !matches!(a.data, Data::Node(_)) { return None; } - self.folded().and_then(|(pending, shared)| (!shared).then_some(pending)) - } - /// The pending call whose items a fold of this array reads in order, and whether anything besides this array also holds those items, as a name does: this array's own call, or its base's for a view of all of the base's items. A view lists its base's items in order under its own layout, so a fold that reads the base by the view's shape gives the view's fold. - pub(crate) fn folded(&self) -> Option<(&Arc, bool)> { - let Self::Array(a) = self else { return None }; - let (array, view_shared) = match &a.data { - Data::Of(base, range, _) if range.len() == base.len() => (base, Arc::strong_count(a) != 1), Data::Of(..) => return None, _ => (a, false) - }; - let Data::Node(pending) = &array.data else { return None }; - (!pending.is_forced()).then(|| (pending, view_shared || Arc::strong_count(array) != 1 || Arc::strong_count(pending) != 1)) - } - /// An array of `layout` whose items `pending` computes when they're first read. - pub(crate) fn deferred(layout: Layout, pending: Arc) -> Self { Self::Array(Arc::new(ArrayData { layout, data: Data::Node(pending) })) } - /// The pending array that this value alone holds, taken out of it, as a held value of a pending program holds one (`fusion::held`). `None` for any other value, which drops as usual. - pub(crate) fn into_pending(self) -> Option> { - let Self::Array(a) = self else { return None }; - match Arc::try_unwrap(a) { Ok(ArrayData { data: Data::Node(pending), .. }) => Some(pending), _ => None } - } - /// The dual array of `primal` and `tangent` at level `tag` of `∂` (`derivative`). The tangent has the primal's layout. - pub(crate) fn from_dual(tag: u64, primal: Value, tangent: Value) -> Self { - Self::from_carrier(primal.layout().clone(), Carrier::Dual(Arc::new(crate::derivative::Dual::new(tag, primal, tangent)))) - } - /// A record of linear calls as an array (`linear`). Its items are never computed. - pub(crate) fn from_linear(t: Arc) -> Self { Self::from_carrier(t.layout.clone(), Carrier::Record(t)) } - /// An array of `layout` that holds `carrier` in place of its items. - pub(crate) fn from_carrier(layout: Layout, carrier: Carrier) -> Self { Self::Array(Arc::new(ArrayData { layout, data: Data::Carried(carrier) })) } - /// The running level's data that this value carries: an array's, in place of its items, or a carried atom's (`transform::Carrier`). - #[inline] - pub(crate) fn carrier(&self) -> Option<&Carrier> { match self { Self::Array(a) => a.carrier(), Self::Carried(carrier) => Some(carrier), _ => None } } - /// Computes a pending array's items now. Timing calls it on each result, so that a deferred result isn't dropped unread. - pub(crate) fn force(&self) { if let Self::Array(a) = self { if a.pending().is_some() { a.storage(); } } } - /// An array's items: its own, or a copy where something else shares them. - pub(crate) fn into_storage(self) -> Storage { - let Self::Array(a) = self else { unreachable!("only an array has storage") }; - Arc::try_unwrap(a).map_or_else(|a| ArrayData::owned(a.data.clone()), ArrayData::into_storage) - } - /// Whether this is an array that holds values as items: mixed storage, and no running level's data (`transform::Carrier`). - #[inline] - pub(crate) fn holds_values(&self) -> bool { matches!(self, Self::Array(a) if a.mixed()) } - /// Whether a direct item is a function. A batch answers for its cells, any other carrier for the value that holds its items, and a pending array computes numbers. - #[inline] - pub fn holds_functions(&self) -> bool { - match self { Self::Array(a) => a.holds_functions(), Self::Carried(carrier) => carried_functions(carrier), _ => false } + pub(crate) fn address(&self) -> Option { + match self { + Self::Array(a) => Some(Arc::as_ptr(a) as usize), Self::Function(f) => Some(f.address()), Self::Operator(op) => Some(Arc::as_ptr(op) as usize), + Self::Pending(p) => Some(p.node.address()), Self::Carried { carried, .. } => Some(Arc::as_ptr(carried) as usize), _ => None, + } + } + pub(crate) fn identical(&self, other: &Self) -> bool { + match (self, other) { + (Self::Number(x), Self::Number(y)) => x.identical(y) && x.is_weak() == y.is_weak(), + (Self::Character(x), Self::Character(y)) => x == y, (Self::Pending(x), Self::Pending(y)) => x == y, + _ => self.address().zip(other.address()).is_some_and(|(x, y)| x == y), + } } - /// Whether no item is an array. Compact storage holds none. An empty array answers for its prototype. - pub(crate) fn is_simple(&self) -> bool { - if self.pending().is_some() { return true; } - // A carrier answers for the value that holds its items. A dual's primal can hold arrays as items before a mix. - if let Some(carrier) = self.carrier() { return carrier.template().is_simple(); } - match self.as_items() { Items::Values([]) => self.prototype().is_atom(), Items::Values(items) => items.iter().all(Value::is_atom), _ => true } + pub(crate) fn identity_hash(&self, state: &mut H) { + use std::hash::Hash; + std::mem::discriminant(self).hash(state); + match self { + Self::Number(n) => { n.identity_hash(state); n.is_weak().hash(state); } + Self::Character(c) => c.hash(state), Self::Pending(p) => p.hash(state), + _ => self.address().hash(state), + } } - pub fn from_parts(shape: impl Into, data: Vec, empty_prototype: Value) -> Result { - if data.is_empty() { Self::empty(shape, empty_prototype) } else { Self::new(shape, data) } + pub fn same(&self, other: &Self) -> bool { + if self.identical(other) { return true; } + match (self, other) { + (Self::Number(x), Self::Number(y)) => x.identical(y), (Self::Character(x), Self::Character(y)) => x == y, + (Self::Array(x), Self::Array(y)) => x.layout() == y.layout() && self.len() == other.len() + && self.elements().zip(other.elements()).all(|(x, y)| x.same(&y)) + && (!self.is_empty() || self.prototype().same(&other.prototype())), + _ => false, + } } - /// One item for each position of `shape`: the item itself when `shape` is empty, otherwise an array of them. - pub(crate) fn shaped(shape: &[usize], mut data: Vec, empty_prototype: Value) -> Result { - if shape.is_empty() { data.pop().ok_or_else(|| Kind::Length.into()) } else { Self::from_parts(shape, data, empty_prototype) } + pub(crate) fn matches(&self, other: &Self, span: &crate::Span) -> crate::error::Result { + self.matches_within(other, Numeric::current()).map_err(|error| error.at(span)) + } + pub(crate) fn matches_within(&self, other: &Self, numeric: Numeric) -> crate::error::Result { + let left = self.read(crate::program::trace::ReadKind::Value)?; + let right = other.read(crate::program::trace::ReadKind::Value)?; + if left.is_atom() != right.is_atom() || left.layout() != right.layout() { return Ok(false); } + if let (Some(x), Some(y)) = (left.view(), right.view()) && let Some(equal) = compact_match(x, y, numeric) { return Ok(equal); } + match (&left, &right) { + (Self::Number(x), Self::Number(y)) => x.matches(y, numeric).map_err(crate::Error::domain), + (Self::Character(x), Self::Character(y)) => Ok(x == y), + (Self::Function(x), Self::Function(y)) => x.matches(y, numeric), + (Self::Operator(x), Self::Operator(y)) => x.matches(y, numeric), + (Self::Array(_), Self::Array(_)) => { + for (x, y) in left.elements().zip(right.elements()) { if !x.matches_within(&y, numeric)? { return Ok(false); } } + if left.is_empty() { left.prototype().matches_within(&right.prototype(), numeric) } else { Ok(true) } + } + _ => Ok(false), + } } - /// Nonempty construction of items gathered one at a time, such as an operation's results or a literal list. Each number keeps its exactness. Exact and approximate numbers together stay mixed. Characters share character storage. Other items stay mixed. The prototype comes from the first item when it is first needed. - pub fn new(shape: impl Into, data: Vec) -> Result { - if data.is_empty() { return Err(Kind::Length.into()); } - if data.iter().any(|v| v.carrier().is_some_and(crate::transform::Carrier::joins_at_once)) { - let mut items = Gather::with_widening(&data.iter().collect::>(), data.len(), Widening::Items); - let shape: Shape = shape.into(); - for item in data { items.add(item) } - return items.finish(Layout::from(shape.to_vec()), Value::integer(0)); + pub(crate) fn as_items(&self) -> Items<'_> { + match self { + Self::Array(array) => array.storage(), + Self::Number(n) => match n.view() { Some(View::Integers(ints)) if n.fixed_width().is_some() => Items::Fixed(ints), + Some(view) => Items::Compact(view), None => Items::Values(std::slice::from_ref(self)) }, + Self::Character(c) => Items::Compact(View::Characters(std::slice::from_ref(c))), + Self::Function(_) | Self::Operator(_) => Items::Values(std::slice::from_ref(self)), + Self::Pending(_) | Self::Carried { .. } => panic!("read a value before borrowing its stored items"), } - Self::from_storage(shape, Storage::compact(data, Widening::Items)) } - - pub(crate) fn from_storage(shape: impl Into, data: Storage) -> Result { - let shape = shape.into(); - if generated_len(&shape)? != data.len() { return Err(Kind::Length.into()); } - Ok(ArrayData::array(shape, data)) + pub fn view(&self) -> Option> { self.as_items().view() } + /// Borrows an observed value's storage and physical mapping; never computes pending work. + pub fn physical(&self) -> Option<(View<'_>, Option<&Mapping>)> { self.as_items().physical() } + pub(crate) fn physical_read(&self, source: usize) -> Option<(View<'_>, rustymath::agreement::ReadPlan)> { + if matches!(self, Self::Pending(_) | Self::Carried { .. }) { return None; } + let (storage, map) = self.physical()?; + let plan = rustymath::agreement::ReadPlan::input(source, storage.len()); + Some((storage, match map { Some(map) => plan.remapped(map.clone(), self.len(), rustymath::agreement::Missing::Default), None => plan })) + } + pub fn as_floats(&self) -> Option<&[f64]> { match self.view()? { View::Floats(Floats::F64(v)) => Some(v), _ => None } } + pub fn as_booleans(&self) -> Option<&[bool]> { match self.view()? { View::Booleans(v) => Some(v), _ => None } } + pub fn at(&self, i: usize) -> Value { self.at_ref(i).into_owned() } + pub(crate) fn at_ref(&self, i: usize) -> Cow<'_, Value> { self.as_items().item_ref(i) } + pub(crate) fn element(&self, i: usize) -> Value { self.at(i) } + pub fn elements(&self) -> impl DoubleEndedIterator + ExactSizeIterator + Clone + '_ { (0..self.len()).map(|i| self.at(i)) } + pub(crate) fn items(&self, range: std::ops::Range) -> impl Iterator + '_ { range.map(|i| self.at(i)) } + pub fn as_number(&self) -> Option { self.is_unit().then(|| match self.at(0) { Self::Number(n) => Some(n), _ => None }).flatten() } + pub(crate) fn as_real(&self) -> Option { self.as_number()?.to_float().ok() } + pub(crate) fn boolean(&self) -> Result { + if !self.is_singleton() { return Err(Kind::Length.into()); } + self.as_number().ok_or(Kind::Domain)?.boolean().map_err(|_| Kind::Domain.into()) } - pub fn booleans(shape: impl Into, data: Vec) -> Result { Self::from_storage(shape, Storage::Compact(Buffer::Booleans(data))) } + pub(crate) fn disclose(&self) -> Value { if self.is_empty() { self.prototype() } else { self.at(0) } } - pub fn floats(shape: impl Into, width: FloatWidth, data: impl IntoIterator) -> Result { - Self::from_storage(shape, Storage::Compact(Buffer::Floats(FloatBuf::collect(width, data.into_iter())))) + pub fn integer(n: i64) -> Self { Self::Number(Number::from_integer(n)) } + pub(crate) fn weak_integer(n: i64) -> Self { Self::Number(Number::from_integer(n).marked(true)) } + pub fn number(n: impl TryInto) -> Result { Ok(Self::Number(n.try_into().map_err(|_| Kind::Domain)?)) } + pub fn from_parts(shape: impl Into, items: Vec, empty_prototype: Value) -> Result { + Self::stored_values(shape.into().into(), items, Some(empty_prototype), Widening::Items) } - pub(crate) fn characters(shape: impl Into, data: Vec) -> Result { - Self::from_storage(shape, Storage::Compact(Buffer::Characters(data))) + pub fn new(shape: impl Into, items: Vec) -> Result { + let prototype = items.first().ok_or(Kind::Length)?.zeroed(); + Self::from_parts(shape, items, prototype) } - /// The positions `items`, each below `bound`, written straight to the width that stores them: the narrowest that holds every position below `bound`, or the `int` width under static kinds (`ordinary_width`). - pub(crate) fn positions>(shape: impl Into, bound: usize, items: impl Iterator) -> Result { - with_width!(ordinary_width(Width::below(bound)), T => T::build(shape, items.map(|i| T::from_i64(i.as_())).collect())).ok_or_else(|| Kind::Length.into()) - } - /// Fixed integers of `width`, written straight from `items`. An item that `width` doesn't hold wraps to its low bits. - pub(crate) fn fixed_integers>( - shape: impl Into, width: Width, items: impl Iterator) -> Result { - Self::from_storage(shape, Storage::Compact(with_width!(width, T => T::buffer(items.map(|i| T::from_i64(i.as_())).collect()))).fixed(true)) - } - /// `positions` from a buffer of them. Stored as 32-bit integers, they keep the buffer, with no pass over it. - pub(crate) fn positions_u32(shape: impl Into, bound: usize, data: Vec) -> Result { - match ordinary_width(Width::below(bound)) { - Width::I32 => Self::from_storage(shape, Storage::Compact(Buffer::Integers(IntBuf::I32(bytemuck::allocation::cast_vec(data))))), - _ => Self::positions(shape, bound, data.into_iter()), + pub(crate) fn tuple(items: Vec) -> Self { + Self::Array(Arc::new(ArrayData::Mixed { layout: vec![items.len()].into(), items: items.into_boxed_slice(), prototype: None })) + } + pub(crate) fn stored_values(layout: Layout, items: Vec, prototype: Option, widening: Widening) -> Result { + if element_count(layout.shape())? != items.len() { return Err(Kind::Length.into()); } + if let Some((buffer, kind)) = packed(&items, widening) { + return Ok(Self::Array(Arc::new(ArrayData::Compact { layout, kind, buffer: SharedBuffer::Owned(buffer), range: None }))); } + if let Some(segments) = Segments::of(&items) { return Ok(Self::Array(Arc::new(ArrayData::Segmented { layout, segments }))); } + Ok(Self::Array(Arc::new(ArrayData::Mixed { layout, prototype: items.is_empty().then(|| prototype.expect("an empty array has a prototype")), items: items.into() }))) } - /// Ordinary integers at the width that stores them (`ordinary_width`). - pub fn integers(shape: impl Into, data: Vec) -> Result { Self::from_storage(shape, Storage::narrowed(data)) } - /// The exact integer `n`, as an atom. - pub fn integer(n: i64) -> Self { Value::Number(Number::from_integer(n)) } - /// The exact integer `n` as a weak atom, as the literal `nₓ` writes it: beside a strong number, it takes that number's kind where it fits (NEP 50). - pub(crate) fn weak_integer(n: i64) -> Self { Value::Number(Number::from_integer(n).marked(true)) } - /// The items as a rustymath view, borrowed, when the storage is compact. - pub fn view(&self) -> Option> { self.as_items().view() } - /// The array whose items this unbuilt structural copy reads through an affine map, with the offset of this array's first item and each axis's stride, counted in that array's items. A buffer export lends the items this way, so windows and reversals reach NumPy without being built. - pub fn affine(&self) -> Option<(Value, usize, Vec)> { crate::fusion::affine(self) } - /// The items as a compact buffer: moved with no copy where nothing else holds them, and copied otherwise. A pending array computes them first. Gives the value back when its items aren't compact. - pub fn into_buffer(self) -> Result { - if !matches!(self, Self::Array(_)) { let copy = self.view().map(|v| Buffer::copied(v, 0..v.len())); return copy.ok_or(self); } - if self.view().is_none() { return Err(self); } - Ok(self.into_storage().into_buffer().expect("compact items stay compact")) + pub(crate) fn stored_buffer(layout: Layout, buffer: Buffer, kind: crate::number::Kind, range: Option) -> Result { + if element_count(layout.shape())? != buffer.len() { return Err(Kind::Length.into()); } + Ok(Self::Array(Arc::new(ArrayData::Compact { layout, kind, buffer: SharedBuffer::Owned(buffer), range }))) + } + pub(crate) fn stored(desc: &crate::program::Desc, buffer: Buffer) -> Result { + if desc.segments.is_some() { return Err(Error::planning()); } + let value = Self::stored_buffer(desc.layout.clone(), buffer, desc.kind, desc.range)?; + Ok(if desc.atom { match value.at(0) { Self::Number(number) => Self::Number(number.marked(desc.kind.weak)), value => value } } else { value }) } - /// An array of shape `shape` holding `buffer`. Integer storage takes the width that stores ordinary integers (`ordinary_width`). pub fn from_buffer(shape: impl Into, buffer: Buffer) -> Result { - let storage = match buffer { - Buffer::Integers(IntBuf::U8(v)) => Storage::narrowed(v), Buffer::Integers(IntBuf::I16(v)) => Storage::narrowed(v), - Buffer::Integers(IntBuf::I32(v)) => Storage::narrowed(v), Buffer::Integers(IntBuf::I64(v)) => Storage::narrowed(v), - compact => Storage::Compact(compact) - }; - Self::from_storage(shape, storage) - } - /// An array of `buffer`, as fixed integers of width `fixed` where that's given, and otherwise as `from_buffer` gives it. - pub(crate) fn from_buffer_at(shape: impl Into, buffer: Buffer, fixed: Option) -> Result { - if let Some(width) = fixed { Self::from_storage(shape, Storage::fixed_at(buffer, width)) } else { Self::from_buffer(shape, buffer) } + let kind = crate::number::Kind::stored(buffer.dtype(), false, false); + Self::stored_buffer(shape.into().into(), buffer, kind, None) } - /// A frontend buffer imported at its supplied dtype, without integer narrowing. - pub fn imported_buffer(shape: impl Into, buffer: Buffer) -> Result { - Self::from_storage(shape, Storage::Compact(buffer).fixed(true)) + pub(crate) fn from_buffer_at(shape: impl Into, buffer: Buffer, fixed: Option) -> Result { + let kind = crate::number::Kind::stored(buffer.dtype(), fixed.is_some(), false); + Self::stored_buffer(shape.into().into(), buffer, kind, None) } - /// Frontend items of shape `shape`, normalized by the shared numeric import policy in `data::imported`. + pub fn imported_buffer(shape: impl Into, buffer: Buffer) -> Result { Self::from_buffer(shape, buffer) } pub fn imported(shape: Vec, items: Vec, empty_prototype: Value) -> Result { - crate::data::imported(shape, items, |_| false, empty_prototype) - } - /// The storage of this array for writing. The layout and the storage are copied first when anything else holds them. - fn storage_mut(&mut self) -> &mut Storage { let Self::Array(a) = self else { unreachable!("only an array has storage") }; Arc::make_mut(a).storage_mut() } - pub(crate) fn set_fixed(&mut self, fixed: bool) { self.storage_mut().set_fixed(fixed); } - /// The items for writing in place, when compact storage holds `A`s. The storage is copied first when anything else holds it. - pub(crate) fn items_mut(&mut self) -> Option<&mut [A]> { - if A::slice(self).is_none() { return None; } - match self.storage_mut() { Storage::Compact(b) => A::of_mut(b), Storage::Fixed(b) => A::of_ints_mut(b), Storage::Mixed(..) => None } - } - /// Replaces the item at each offset with the matching item of `source`, or with its only item when it has one, in place. A later offset wins, and a `Steps::FILL` offset writes nothing. A weak number takes this array's width, as in arithmetic (`Gather`). Storage that can't hold an item widens or turns mixed by the rule for writes (`Widening::of_writes`). Under static kinds the whole array takes the compact dtype that holds both kinds. Under dynamic kinds each number keeps its exactness, as a literal of the same items would, and a Boolean stays apart from other numbers. Mixed storage forgets its prototype when item 0 changes, and its environment when a written or overwritten item depends on a frame. A write into a batch, or of one, writes every cell at once (`transform::written`). - pub(crate) fn scatter(&mut self, offsets: &[usize], source: &Value) -> Result<(), crate::transform::Stop> { self.written(Writes::Offsets(offsets), source) } - /// `scatter` at the positions that `steps` reach from `base`, in the order `walk` reads them, without listing them (`Gather::scatter_walk`). - pub(crate) fn scatter_walk(&mut self, base: usize, steps: &[rustymath::gather::Steps], source: &Value) -> Result<(), crate::transform::Stop> { - self.written(Writes::Walk { base, steps }, source) - } - /// The items of `source` written in place where `writes` says, as `scatter` writes them. - fn written(&mut self, writes: Writes<'_>, source: &Value) -> Result<(), crate::transform::Stop> { - // Writes only at fills change nothing, so storage doesn't widen for them. - if writes.only_fills() { return Ok(()); } - if let Some(result) = crate::transform::written(self, source, Some(writes), |t, s| t.written(writes, s)) { return result; } - let data = self.storage_mut(); - if let Storage::Mixed(items, memo) = data { - if !writes.runs_while(&mut |_, run| !run.contains(&0)) { memo.prototype.take(); } - if source.environment().is_some() - || !writes.runs_while(&mut |_, run| run.iter().all(|&o| o == rustymath::gather::Steps::FILL || items[o].environment().is_none())) { - memo.forget_environment(); - } - if memo.knows_function() || matches!(source, Value::Function(_)) || source.holds_functions() { memo.forget_functions(); } - } - let mut gather = Gather::resume(std::mem::replace(data, Storage::Compact(Buffer::Booleans(Vec::new()))), Widening::of_writes()); - gather.scatter(writes, source); - *data = gather.data; - Ok(()) - } - /// Writes `item` at `path` in place, through nested items. The empty path replaces the whole array. A write into a batch, or of one, writes every cell at once (`transform::written`). - pub(crate) fn write_path(&mut self, path: &[usize], item: Value) -> Result<(), crate::transform::Stop> { - let Some((&last, outer)) = path.split_last() else { *self = item; return Ok(()); }; - let dependent = item.environment().is_some(); - let source = if item.is_atom() { item } else { item.enclose().expect("a unit holds any item") }; - self.item_mut(outer, dependent).scatter(&[last], &source) - } - /// The item at `path`, for writing in place. Each level on the way is copied first only when something else holds it. Its memo forgets the prototype when the path goes through item 0, and the environment when `dependent` is set or the item on the path depends on a frame. - pub(crate) fn item_mut(&mut self, path: &[usize], dependent: bool) -> &mut Value { - let mut current = self; - for &i in path { - let Storage::Mixed(items, memo) = current.storage_mut() else { unreachable!("only mixed storage holds arrays") }; - if i == 0 { memo.prototype.take(); } - if dependent || items[i].environment().is_some() { memo.forget_environment(); } - current = &mut items[i]; - } - current - } - /// Appends the major cells of `cells` in place. `cells` has this array's rank and matches its other axes. `names` holds the keys of the new cells when the leading axis has keys or gains them. Positions already there keep their keys, or have none. A Domain error, with the array unchanged, when a new key is already on the leading axis or appears twice. The caller checks that the length is within the limit. - pub(crate) fn append(&mut self, cells: &Value, names: Option<&[Option>]>) -> Result<(), ErrorKind> { - if let Some(result) = crate::transform::written(self, cells, None, |t, c| t.append(c, names)) { return result; } - let Self::Array(a) = self else { unreachable!("only an array grows") }; - let a = Arc::make_mut(a); - a.layout.append(cells.shape()[0], names)?; - let data = a.storage_mut(); - if let Storage::Mixed(items, memo) = data { - if items.is_empty() && !cells.is_empty() { memo.prototype.take(); } - if cells.environment().is_some() { memo.forget_environment(); } - if cells.holds_functions() { memo.forget_functions(); } - } - let mut gather = Gather::resume(std::mem::replace(data, Storage::Compact(Buffer::Booleans(Vec::new()))), Widening::Arrays); - gather.append(cells); - *data = gather.data; - Ok(()) - } - /// Complex results of an operation. When no item has an imaginary part, the array holds floats of the parts' width, as `Number` gives them. - pub(crate) fn complex(shape: impl Into, data: ComplexBuf) -> Result { - /// The real parts of `data` when no item has an imaginary part, else `data`. While every imaginary part is +0, the real parts move to the front of the same buffer, in one pass with no allocation. At the first other imaginary part, the items before it go back, and the rest are checked as `Number` checks them, where -0 also counts as no imaginary part. - fn real_parts(data: Vec>) -> Result, Vec>> { - let n = data.len(); - let positive_zero = |x: F| x == F::zero() && x.is_sign_positive(); - let mut v: Vec = bytemuck::allocation::cast_vec(data); - let Some(k) = (0..n).find(|&i| { !positive_zero(v[2 * i + 1]) || { v[i] = v[2 * i]; false } }) else { - v.truncate(n); - v.shrink_to_fit(); - return Ok(v); - }; - for j in (0..k).rev() { (v[2 * j], v[2 * j + 1]) = (v[j], F::zero()); } - let data: Vec> = bytemuck::allocation::cast_vec(v); - if data[k..].iter().all(|z| z.im == F::zero()) { Ok(data.into_iter().map(|z| z.re).collect()) } else { Err(data) } - } - let storage = - match data { - ComplexBuf::F32(v) => real_parts(v) - .map_or_else(|v| Storage::Compact(Buffer::Complexes(ComplexBuf::F32(v))), |r| Storage::Compact(Buffer::Floats(FloatBuf::F32(r)))), - ComplexBuf::F64(v) => real_parts(v) - .map_or_else(|v| Storage::Compact(Buffer::Complexes(ComplexBuf::F64(v))), |r| Storage::Compact(Buffer::Floats(FloatBuf::F64(r)))), - }; - Self::from_storage(shape, storage) + Self::stored_values(shape.into(), items, Some(empty_prototype), Widening::Arrays) } - /// Whether every number, including those in nested arrays, is exact. Non-finite values in integer storage count as exact. - pub fn is_exact(&self) -> bool { self.exact_domain() == Some(true) } - /// Whether all items are Boolean or exact integers, with no active transformation. - pub fn is_integral(&self) -> bool { - if self.carrier().is_some() { return false; } - if let Some(dtype) = self.dtype() { return dtype.is_integral(); } - match self { - Self::Number(n) => n.is_integral(), - Self::Array(_) => { if self.is_empty() { self.prototype().is_integral() } else { self.elements().all(|v| v.is_integral()) } } _ => false - } - } - /// This value with a weak number made strong (`Number::is_weak`), as an explicit width conversion makes it. - pub(crate) fn strong(self) -> Value { match self { Value::Number(n) => Value::Number(n.marked(false)), v => v } } - - /// Empty arrays require a prototype item; their shape is never inferred from the buffer. The storage is mixed where no compact kind holds the prototype. - pub fn empty(shape: impl Into, prototype: Value) -> Result { - let shape = shape.into(); - if element_count(&shape)? != 0 { return Err(Kind::Length.into()); } - let prototype = prototype.zeroed(); - let data = match item_dtype(&prototype) { - Some(dtype) => Storage::with_capacity(Some(dtype), 0).fixed(prototype.fixed_width().is_some()), - None => Storage::Mixed(Vec::new(), Memo::with_prototype(prototype)), - }; - Ok(ArrayData::array(shape, data)) + pub fn booleans(shape: impl Into, items: Vec) -> Result { Self::from_buffer(shape, Buffer::Booleans(items)) } + pub fn integers(shape: impl Into, items: Vec) -> Result { Self::from_buffer(shape, Buffer::Integers(IntBuf::I64(items))) } + pub fn floats(shape: impl Into, width: FloatWidth, items: impl IntoIterator) -> Result { + Self::from_buffer(shape, Buffer::Floats(FloatBuf::collect(width, items.into_iter()))) } - - pub fn number(n: impl TryInto) -> Result { Ok(Self::Number(n.try_into().map_err(|_| Kind::Domain)?)) } - pub fn is_unit(&self) -> bool { self.shape().is_empty() } - pub fn is_singleton(&self) -> bool { self.len() == 1 } - pub fn shape(&self) -> &[usize] { match self { Self::Array(a) => a.layout.shape(), _ => &[] } } - #[inline] - pub fn len(&self) -> usize { match self { Self::Array(a) => a.layout.count(), _ => 1 } } - #[inline] - pub fn is_empty(&self) -> bool { self.len() == 0 } - /// The items in their storage type. An atom is its own single item. - pub(crate) fn as_items(&self) -> Items<'_> { - match self { - Self::Array(a) => a.items(), - Self::Number(n) => match n.view() { - Some(v @ View::Integers(_)) => Items::Compact(v).fixed(n.fixed_width().is_some()), Some(v) => Items::Compact(v), - None => Items::Values(std::slice::from_ref(self)) - }, - Self::Character(c) => Items::Compact(View::Characters(std::slice::from_ref(c))), - Self::Function(_) | Self::Operator(_) | Self::Carried(_) => Items::Values(std::slice::from_ref(self)), - } + pub(crate) fn complex(shape: impl Into, items: ComplexBuf) -> Result { Self::from_buffer(shape, Buffer::Complexes(items)) } + pub(crate) fn characters(shape: impl Into, items: Vec) -> Result { Self::from_buffer(shape, Buffer::Characters(items)) } + pub(crate) fn positions_buffer(shape: impl Into, _: usize, items: IntBuf) -> Result { Self::from_buffer(shape, Buffer::Integers(items)) } + pub(crate) fn positions_u32(shape: impl Into, bound: usize, items: Vec) -> Result { Self::positions(shape, bound, items.into_iter()) } + pub(crate) fn positions>(shape: impl Into, bound: usize, items: impl Iterator) -> Result { + let width = Width::below(bound); + with_width!(width, T => Self::from_buffer(shape, ::buffer(items.map(|n| n.as_() as T).collect()))) } - /// The compact kind of the items, as a kernel reads them. An atom takes the narrowest kind that holds it. - pub(crate) fn dtype(&self) -> Option { match self { Self::Array(a) => a.dtype(), atom => item_dtype(atom) } } - pub(crate) fn fixed_width(&self) -> Option { match self { Self::Number(n) => n.fixed_width(), Self::Array(a) => a.fixed_width(), _ => None } } - pub(crate) fn ordinary(&self) -> Self { - match self { - Self::Number(n) => Self::Number(n.ordinary()), - Self::Array(a) if a.fixed_width().is_some() => ArrayData::part(a, 0..a.len(), a.layout.clone(), false), _ => self.clone() - } + pub(crate) fn fixed_integers>(shape: impl Into, width: Width, items: impl Iterator) -> Result { + with_width!(width, T => Self::from_buffer_at(shape, ::buffer(items.map(|n| n.as_() as T).collect()), Some(width))) } - /// This array as fixed integers of `width`, sharing its items, where it holds integers of `width`. - #[inline] - pub(crate) fn as_fixed(&self, width: Width) -> Option { - let Self::Array(a) = self else { return None }; - (self.dtype() == Some(DType::Integer(width))).then(|| ArrayData::part(a, 0..a.len(), a.layout.clone(), true)) - } - /// This value's numbers as fixed integers of `width`, each converted as `Converts` converts it, with floats floored within the tolerance that `numeric` gives their width. Compact integers of `width` become fixed without a copy (`as_fixed`). Characters, functions and complex numbers are DOMAIN. - pub(crate) fn to_fixed(&self, width: Width, numeric: Numeric) -> Result { - if let Self::Number(n) = self { return Ok(Self::Number(n.to_fixed(width, numeric)?)); } - if !matches!(self, Self::Array(_)) { return Err(Kind::Domain.into()); } - if let Some(fixed) = self.as_fixed(width) { return Ok(fixed); } - let Some(buffer) = self.as_items().view().and_then(|v| with_width!(width, T => crate::element::converted::(v, numeric).map(T::buffer))) else { - let values = self.elements().map(|v| v.to_fixed(width, numeric)).collect::, _>>()?; - return self.layout().collect(values, self.prototype().to_fixed(width, numeric)?); - }; - Self::from_storage(self.shape(), Storage::Compact(buffer).fixed(true)).and_then(|v| v.with_layout(self.layout().clone())) - } - /// This value's numbers as integers of `width` (`to_fixed`), which are fixed where `numeric` has a fixed width and ordinary otherwise, as a call under `numeric` gives them. - pub(crate) fn to_integers(&self, width: Width, numeric: Numeric) -> Result { - let fixed = self.to_fixed(width, numeric)?; - Ok(if numeric.fixed.is_some() { fixed } else { fixed.ordinary() }) - } - /// `self` as a call under `t` gives it: compact integers wrapped as `Number::wrapped` wraps each one. Other items are unchanged. - pub(crate) fn wrapped(self, t: Numeric) -> Result { self.wrapped_at(t.integer_width(), t) } - /// `wrapped` at `width`: where `t` wraps, compact integers become their low bits at `width`, as fixed integers where `t` has a fixed width. Ordinary integers that `width` holds, and integers already fixed at `width`, are unchanged. - pub(crate) fn wrapped_at(self, width: Width, t: Numeric) -> Result { - match self.dtype() { - Some(DType::Integer(w)) if t.wrapping() && match t.fixed { Some(_) => self.fixed_width() != Some(width), None => w.join(width) != width } => - { self.to_integers(width, t) } _ => Ok(self) + pub fn empty(shape: impl Into, prototype: Value) -> Result { + let shape = shape.into(); if element_count(&shape)? != 0 { return Err(Kind::Length.into()); } + if prototype.is_atom() && let Some(kind) = prototype.kind() { + return Self::stored_buffer(shape.into(), Buffer::with_capacity(kind.dtype(), 0), kind.strong(), Some(Range::EMPTY)); } + Self::stored_values(shape.into(), Vec::new(), Some(prototype), Widening::Items) } - /// The width of the floats that hold the items, or of the parts of the complex numbers that do, when compact storage holds them or the value is such an atom. - pub(crate) fn float_width(&self) -> Option { float_width_of(self.dtype()?) } - /// The numbers of this value as floats of `width`, each rounded once, with the arrays nested in it converted too. Complex numbers take parts of `width`, and 32-bit parts for 16 bits. Floats already of `width`, pending ones included, stay as they are, with no copy. Characters and functions are DOMAIN. - pub(crate) fn to_float_width(&self, width: FloatWidth) -> Result { - if let Self::Number(n) = self { return n.to_float_width(width).map(Self::Number).map_err(|_| Kind::Domain.into()); } - if !matches!(self, Self::Array(_)) { return Err(Kind::Domain.into()); } - if self.dtype() == Some(DType::Float(width)) { return Ok(self.clone()); } - match self.view() { - Some(View::Booleans(_) | View::Integers(_) | View::Floats(_)) => { - let floats = with_float_width!(width, F => F::build(self.shape(), crate::element::read_as::(self).ok_or(Kind::Domain)?.into_owned())); - floats.ok_or(Kind::Domain)?.with_layout(self.layout().clone()) - } - Some(View::Complexes(z)) => { - let z = Self::complex(self.shape(), ComplexBuf::collect(width.into(), z.widened().iter().copied()))?; - z.with_layout(self.layout().clone()) - } - _ => { - self.layout().collect(self.elements().map(|e| e.to_float_width(width)).collect::, _>>()?, self.prototype().to_float_width(width)?) + + pub(crate) fn stored_part(array: &Arc, range: std::ops::Range, layout: Layout) -> Result { + if range.end > array.len() || range.len() != element_count(layout.shape())? { return Err(Kind::Length.into()); } + match &**array { + ArrayData::Compact { kind, range: bounds, .. } => { + let outer = Mapping::Steps { base: range.start, steps: vec![rustymath::gather::Steps::along(range.len(), 1)].into() }; + let (base, map) = ArrayData::physical(array); + let map = match map { None => outer, Some(map) => map.composed(&outer, array.layout().shape(), layout.shape(), true).expect("a stored view composes") }; + Ok(Self::Array(Arc::new(ArrayData::Compact { layout, kind: *kind, buffer: SharedBuffer::View { base: base.clone(), map }, range: *bounds }))) } + ArrayData::Mixed { items, prototype, .. } => Self::stored_values(layout, items[range].to_vec(), prototype.clone(), Widening::Items), + ArrayData::Segmented { segments, .. } => Self::stored_values(layout, segments.segs().slice(range).values().collect(), Some(segments.prototype().clone()), Widening::Items), } } - /// This value's numbers converted to `to`: fixed integers of a width (`to_fixed`), or floats of a width, where complex numbers keep complex parts of that width (`to_float_width`). The result is strong. Any other `to` is DOMAIN. - pub(crate) fn converted(&self, to: DType, numeric: Numeric) -> Result { - match to { - DType::Integer(width) => self.to_fixed(width, numeric), DType::Float(width) => self.to_float_width(width).map(Value::strong), - DType::Complex(width) => self.to_float_width(width.into()).map(Value::strong), _ => Err(Kind::Domain.into()) - } - } - /// The value that answers, in this one's place, the queries that read no items: a dual's primal, a record's zero or a batch's first cell, through every carrier (`Carrier::template`), or else the value itself. - pub(crate) fn template(&self) -> Value { - let mut value = self.clone(); - while let Some(inner) = value.carrier().map(|carrier| carrier.template().clone()) { value = inner; } - value - } - /// The dtype of the items a reader sees: the `dtype` of `template`, so that a dual answers for its primal and a batch for its cells. `dtype` gives `None` for an array that carries a level's data, so that kernels never read its storage as compact. - pub(crate) fn template_dtype(&self) -> Option { self.template().dtype() } - /// The name of the storage that holds the items under the settings `numeric`, as `•storage` gives it: rustymath's name for a compact dtype, except for integers. Under dynamic kinds, which choose an integer's width from its value, integers of any width are `"integer"`. Under static kinds an integer names the width it computes at, because the width is part of its kind there. It reads no items: a pending array answers from its call's dtype, and a dual from its primal. An atom gives its own kind. - pub(crate) fn storage_kind(&self, numeric: Numeric) -> &'static str { - let held = self.template(); - match (&held, held.dtype()) { - (_, Some(dtype)) if held.fixed_width().is_some() => dtype.name(), - (_, Some(dtype @ DType::Integer(_))) if numeric.is_static() => numeric.kind(dtype).name(), (_, Some(DType::Integer(_))) => "integer", - (_, Some(dtype)) => dtype.name(), (Self::Number(_), None) => "rational", (Self::Function(_), None) => "function", _ => "mixed" - } - } - /// The integer width a host export must convert to, or `None` when its storage already has the export dtype. Ordinary dynamic integers export as i64. Fixed integers keep their width, and static ordinary integers use their computation width. This reads metadata without computing pending items. - pub fn export_widening(&self) -> Option { - if self.fixed_width().is_some() { return None; } - let stored @ DType::Integer(storage_width) = self.dtype()? else { return None }; - let numeric = Numeric::current(); - let target = if numeric.is_static() { numeric.kind(stored) } else { DType::Integer(Width::I64) }; - let DType::Integer(width) = target else { return None }; - let storage_width = match self { Self::Number(n) => n.integer_items()?.tag(), _ => storage_width }; - (width != storage_width).then_some(width) - } - /// Compact integer items converted to their host export dtype. `None` where an export can use the existing storage. - pub fn widened_for_export(&self) -> Option { - let width = self.export_widening()?; - let View::Integers(ints) = self.view()? else { return None }; - Some(with_width!(width, T => ::buffer(crate::element::cast::(ints).into_owned()))) - } - /// Whether the array reads another array's items, as a view. - pub(crate) fn is_view(&self) -> bool { matches!(self, Self::Array(a) if a.is_view()) } - /// The bytes of the allocations that this value keeps alive: its items, a view's base, the arrays that a pending array reads, a dual's parts and a record's nodes. A shared allocation counts once. An atom that holds no array gives 0. - pub(crate) fn footprint(&self) -> usize { - // A value never reaches its own allocation again, so an array's count starts below it, and a plain array records nothing. - match self { Self::Array(a) => a.as_ref().footprint_in(&mut Seen::default()), _ => self.footprint_in(&mut Seen::default()) } - } - pub fn as_floats(&self) -> Option<&[f64]> { match self.as_items() { Items::Compact(View::Floats(Floats::F64(v))) => Some(v), _ => None } } - pub fn as_integers(&self) -> Option<&[i64]> { match self.as_items().ints() { Some(Ints::I64(v)) => Some(v), _ => None } } - pub fn as_booleans(&self) -> Option<&[bool]> { match self.as_items() { Items::Compact(View::Booleans(v)) => Some(v), _ => None } } - pub fn at(&self, i: usize) -> Value { self.at_ref(i).into_owned() } - /// Item `i`, or `fill` as an item of this value where `i` is `Steps::FILL` (`fill_item`). A pending array gives the item without computing the rest. - pub(crate) fn at_or_fill(&self, i: usize, fill: Option<&Value>) -> Value { - if i == rustymath::gather::Steps::FILL { self.fill_item(fill) } else { crate::fusion::item(self, i) } - } - /// The item that a read of this value gives where it reaches no item: the validated scalar `fill`'s item, as an item of this value's kind, or this value's prototype where there's none. - pub(crate) fn fill_item(&self, fill: Option<&Value>) -> Value { fill.map_or_else(|| self.prototype(), |fill| fill.at(0)) } - /// The native fill that `fill`, a validated scalar, gives at this value's compact dtype. `None` for mixed storage, which reads `fill` itself. - pub(crate) fn native_fill(&self, fill: &Value) -> Option> { - let dtype = self.template_dtype()?; - Some(Arc::new(Fill::new(fill.view()?, dtype).expect("a validated scalar fill"))) - } - /// The scalar that holds the native `fill`, the inverse of `native_fill`. `fixed` marks integers of a fixed width. - pub(crate) fn from_fill(fill: &Arc, fixed: bool) -> Self { - Self::from_storage(Shape::EMPTY, Storage::Compact(Buffer::copied(fill.view(), 0..1)).fixed(fixed)).expect("one fill item is a scalar") - } - /// The item that a read through `map` gives where it reaches no item: the native fill that `map` chose, or else this value's prototype (`fill_item`). - pub(crate) fn mapped_fill(&self, map: &Mapping) -> Self { - self.fill_item(map.chosen().map(|fill| Self::from_fill(fill, self.fixed_width().is_some())).as_ref()) - } - /// `fill` held in a scalar of `dtype` that keeps its value, with the dtype that holds it. An integer `dtype` that isn't `fixed` widens under dynamic kinds to the narrowest width that holds the fill, as ordinary integers do. Mixed storage, a `dtype` of `None`, takes any value. A DOMAIN error where no dtype holds the fill. - pub(crate) fn fill_of(fill: &Value, dtype: Option, fixed: bool) -> Result<(Option, Value), Error> { - let Some(mut to) = dtype else { return Ok((None, fill.enclose().described("invalid fill")?)) }; - let lost = || Error::domain("the fill doesn't keep its value as the data's type"); - let one = Layout::from([1]).filled(fill); - let own = one.view().ok_or_else(lost)?; - let ordinary = !fixed && !Numeric::current().is_static(); - loop { - match Buffer::kept(own, to) { - Ok(item) => return Ok((Some(to), Self::from_storage(Shape::EMPTY, Storage::Compact(item).fixed(fixed)).described("invalid fill")?)), - Err(_) => match to { DType::Integer(w) if ordinary => to = DType::Integer(w.wider().ok_or_else(lost)?), _ => return Err(lost()) }, - } - } + pub(crate) fn part(&self, range: std::ops::Range, layout: impl Into) -> crate::error::Result { + let layout = layout.into(); + if let Self::Array(array) = self { return Ok(Self::stored_part(array, range, layout)?); } + let map = Mapping::from(vec![rustymath::gather::Steps::along(range.len(), 1)]).with_steps(range.start, + vec![rustymath::gather::Steps::along(range.len(), 1)]); + crate::fusion::copied(self, layout, map, false, None) } - /// `fill` as an item of the type that mix under `widening` gives this array's items (`fill_of`), for the items that mix pads. - pub(crate) fn mixed_fill(&self, fill: &Value, widening: Widening) -> Result { - let cells: Vec<_> = self.elements().collect(); - let kind = Gather::joined_kind(&cells.iter().collect::>(), widening).unwrap_or_default(); - Ok(Self::fill_of(fill, kind.dtype, kind.fixed.is_some())?.1) - } - /// Item `i`, borrowed where this value holds it as a value. - pub(crate) fn at_ref(&self, i: usize) -> Cow<'_, Value> { - if let Self::Array(a) = self { if let Some(carrier) = a.carrier() { return Cow::Owned(carrier.at(i)); } } - match self { Self::Array(a) => a.items().item_ref(i), _ => { assert_eq!(i, 0); Cow::Borrowed(self) } } - } - /// Item `i` as bulk iteration reads it (`elements`, `items`, `Cells::collect`): `at`'s item, except where a carrier gives another (`Carrier::element`), so that reading every item of a traced array adds no step for each item. - pub(crate) fn element(&self, i: usize) -> Value { - if let Self::Array(a) = self && let Some(carrier) = a.carrier() { return carrier.element(i); } - self.at(i) - } - pub fn elements(&self) -> impl DoubleEndedIterator + ExactSizeIterator + Clone + '_ { (0..self.len()).map(|i| self.element(i)) } - /// Whether every item is a number. Compact numeric storage answers without reading its items. - pub(crate) fn all_numbers(&self) -> bool { - match self.as_items() { - Items::Compact(View::Characters(c)) => c.is_empty(), Items::Values(v) => v.iter().all(|e| matches!(e, Value::Number(_))), _ => true - } + pub(crate) fn relaid(&self, layout: Layout) -> crate::error::Result { + if layout.count() != self.len() { return Err(Kind::Length.into()); } + self.part(0..self.len(), layout) } - pub(crate) fn items(&self, range: std::ops::Range) -> impl Iterator + '_ { range.map(|i| self.element(i)) } - /// The items in `range`, as an array laid out by `layout`. Compact items stay in this array's storage, as a view of their range (`ArrayData::part`). A carried array gives its carrier's copy of the range (`Carrier::copied`): one node of a record, or one step of a trace. Mixed items are copied, because a mixed array's memo answers for all its items. - pub(crate) fn part(&self, range: std::ops::Range, layout: impl Into) -> Result { - if let Self::Array(a) = self { - if let Some(carrier) = a.carrier() { - return carrier.copied(layout.into(), &Mapping::Steps { base: range.start, steps: vec![Steps::along(range.len(), 1)].into() }, None); - } - if a.items().view().is_some() { return Ok(ArrayData::part(a, range, layout.into(), a.fixed_width().is_some())); } - } - let mut data = Gather::new(&[self], range.len()); - data.extend(self, range); - data.finish(layout.into(), || self.prototype()) + pub(crate) fn with_shape(&self, shape: impl Into) -> crate::error::Result { self.relaid(self.layout().resized(shape)) } + pub(crate) fn with_layout(self, layout: Layout) -> crate::error::Result { + if self.layout() == &layout { return Ok(self); } + self.relaid(layout.unique_names()) } - /// An array's prototype. Compact storage gives its kind's zero. An empty mixed array keeps a stored prototype. Other arrays fill their first item on first request and keep the result. - pub fn prototype(&self) -> Value { - let Self::Array(a) = self else { return self.zeroed() }; - if let Some(carrier) = a.carrier() { return carrier.template().prototype(); } - match a.pending() { - // A view that reads fixed pending items as ordinary integers has an ordinary prototype. - Some(p) => { if a.fixed_width().is_some() { p.prototype() } else { p.prototype().ordinary() } } - None => a.items().prototype().unwrap_or_else(|| a.storage().prototype()), - } + pub fn with_core_layout(self, layout: rustymath::layout::Layout) -> crate::error::Result { + let renderer = self.renderer().cloned(); self.with_layout(Layout::kept(layout, renderer)) } + pub fn enclose(&self) -> crate::error::Result { crate::primitive::Operation::Join(crate::primitive::Notation::Strand).call(&[self.clone()], 0)?.with_shape([]) } + pub(crate) fn unit(self) -> crate::error::Result { if self.is_atom() { Ok(self) } else { self.enclose() } } - pub(crate) fn with_shape(&self, shape: impl Into) -> Result { - let shape = shape.into(); - if *shape == *self.shape() && !self.is_atom() { return Ok(self.clone()); } - self.relaid(shape.into()) - } - /// The array viewed with `layout`, which has as many items in any shape. The view shares this array's items. A name that `layout` gives more than one axis is dropped from all of them, as `with_layout` drops it. A carried value, atom or array, is viewed through its carrier (`Carrier::view`). - pub(crate) fn relaid(&self, layout: Layout) -> Result { - if element_count(layout.shape())? != self.len() { return Err(Kind::Length.into()); } + pub fn prototype(&self) -> Value { match self { - Self::Array(a) => Ok(ArrayData::view(a, layout.unique_names())), Self::Carried(c) => Ok(c.view(layout)), - _ => Self::new(layout.shape().to_vec(), vec![self.clone()])?.with_layout(layout) + Self::Number(n) => Self::Number(n.zero()), Self::Character(_) => Self::Character(' '), + Self::Array(array) => array.prototype(), Self::Pending(p) => p.desc().zero(), + Self::Carried { carried, .. } => match &**carried { Carried::Dual { primal, .. } => primal.prototype(), Carried::Batch { cell, .. } => cell.zero() }, + _ => Self::integer(0), } } - /// The array with no names on its frame, the axes before its cells of rank `rank`, as a view. - pub(crate) fn unnamed_frame(self, rank: usize) -> Self { - let (layout, frame) = (self.layout(), self.shape().len() - rank); - if !layout.frame_named(frame) { return self; } - let names = (0..layout.shape().len()).map(|a| if a < frame { None } else { layout.name(a).cloned() }).collect(); - let layout = layout.clone().inherit_names(names); - self.relaid(layout).expect("names leave the items") + pub(crate) fn innermost_prototype(&self) -> Value { + let mut value = self.prototype(); while matches!(value, Self::Array(_)) { value = value.prototype(); } value + } + pub fn zeroed(&self) -> Value { + if self.is_atom() { return self.prototype(); } + if let Some(kind) = self.kind() { + let prototype = self.prototype(); + let buffer = Buffer::copied(prototype.view().expect("a compact prototype"), 0..1); + let mut out = Buffer::with_capacity(kind.dtype(), self.len()); + for _ in 0..self.len() { out.extend_from(buffer.view(), 0..1); } + return Self::stored_buffer(self.layout().clone(), out, kind, Some(Range::of(Some(0.0)))).expect("zero items retain the layout"); + } + let items = self.elements().map(|v| v.zeroed()).collect(); + Self::stored_values(self.layout().clone(), items, Some(self.prototype()), Widening::Items).expect("zeroed items retain the layout") } pub fn axis_names(&self) -> &[Option>] { self.layout().names() } pub fn axis_name(&self, axis: usize) -> Option<&Arc> { self.layout().name(axis) } - pub fn with_axis_names(self, names: Vec>>) -> Result { - let layout = self.layout().clone().with_names(names)?; - self.with_layout(layout) - } - /// `self`, a vector of positions along `source`'s leading axis, with that axis's name. - pub(crate) fn along_first_of(self, source: &Value) -> Result { self.with_axis_names(vec![source.axis_name(0).cloned()]) } pub fn keys(&self, axis: usize) -> Option<&Arc> { self.layout().keys(axis) } pub fn has_keys(&self) -> bool { self.layout().has_keys() } - pub(crate) fn renderer(&self) -> Option<&crate::Function> { self.layout().renderer() } - /// The value displayed through `renderer`. An atom becomes a scalar, because only an array has a layout to hold the renderer. - pub(crate) fn with_renderer(self, renderer: crate::Function) -> Result { + pub(crate) fn renderer(&self) -> Option<&Function> { self.layout().renderer() } + pub fn with_axis_names(self, names: Vec>>) -> crate::error::Result { let layout = self.layout().clone().with_names(names)?; self.with_layout(layout) } + pub(crate) fn with_keys(self, keys: Vec>>) -> crate::error::Result { let layout = self.layout().clone().with_keys(keys)?; self.with_layout(layout) } + pub(crate) fn with_renderer(self, renderer: Function) -> crate::error::Result { let value = if self.is_atom() { self.enclose()? } else { self }; - let layout = value.layout().clone().with_renderer(Some(renderer)); - value.with_layout(layout) - } - pub(crate) fn layout(&self) -> &Layout { static ATOM: Layout = Layout::EMPTY; match self { Self::Array(a) => &a.layout, _ => &ATOM } } - /// The array's shape, axis keys and names, shared with numerical array frontends. - pub fn core_layout(&self) -> &rustymath::layout::Layout { self.layout() } - /// Keeps the items and renderer under `layout`; the item count must stay unchanged. - pub fn with_core_layout(self, layout: rustymath::layout::Layout) -> Result { - let renderer = self.renderer().cloned(); - self.with_layout(Layout::kept(layout, renderer)) - } - pub(crate) fn with_layout(self, layout: Layout) -> Result { - let layout = layout.unique_names(); - if layout.shape() != self.shape() { return Err(Kind::Length.into()); } - if &layout == self.layout() { return Ok(self); } - let Self::Array(mut a) = self else { return Err(Kind::Domain.into()); }; - // A carrier's parts keep its layout, so a carried array takes a new layout through its view. - if !matches!(a.data, Data::Carried(_)) { if let Some(data) = Arc::get_mut(&mut a) { data.layout = layout; return Ok(Self::Array(a)); } } - Ok(ArrayData::view(&a, layout)) - } - pub(crate) fn with_keys(self, keys: Vec>>) -> Result { - let layout = self.layout().clone().with_keys(keys)?; - self.with_layout(layout) - } - /// The array without its position keys. Its axis names stay. - pub(crate) fn unkeyed(&self) -> Self { - let Self::Array(a) = self else { return self.clone(); }; - if !a.layout.has_keys() { return self.clone(); } - ArrayData::view(a, a.layout.clone().with_keys(vec![]).unwrap()) + let layout = value.layout().clone().with_renderer(Some(renderer)); value.with_layout(layout) } - - /// The major cells, in order. A unit, which has no leading axis, is a RANK error. - pub fn major_cells(&self) -> Result, ErrorKind> { self.cells(self.shape().len().checked_sub(1).ok_or(Kind::Rank)?)?.collect() } - pub(crate) fn cells(&self, rank: usize) -> Result, ErrorKind> { - let split = self.shape().len().checked_sub(rank).ok_or(Kind::Rank)?; - let count = generated_len(&self.shape()[..split])?; - let size = element_count(&self.shape()[split..])?; - Ok(Cells { array: self, split, count, size, cell_layout: self.layout().axes(split..self.shape().len()) }) + pub(crate) fn along_first_of(self, source: &Value) -> crate::error::Result { self.with_axis_names(vec![source.axis_name(0).cloned()]) } + pub(crate) fn unkeyed(&self) -> crate::error::Result { + let layout = Layout::kept(self.layout().core.clone().with_keys(vec![None; self.rank()])?, self.renderer().cloned()); + self.clone().with_layout(layout) } - - pub fn as_number(&self) -> Option { if !self.is_unit() { return None; } match self.at(0) { Value::Number(n) => Some(n), _ => None } } - /// The one number of this unit as an `f64`. `None` for any other value, a complex number, or an exact number outside the float range. - pub(crate) fn as_real(&self) -> Option { self.as_number()?.to_float().ok() } - - /// The one item of this singleton as a Boolean, as an observer reads it: LENGTH for any other count of items, and DOMAIN for an item that isn't 0 or 1. - pub(crate) fn boolean(&self) -> Result { - if !self.is_singleton() { return Err(Kind::Length.into()); } - match self.at(0).observed()? { crate::transform::Plain::Number(n) => n.boolean().map_err(|_| Kind::Domain.into()), _ => Err(Kind::Domain.into()) } + pub(crate) fn unnamed_frame(self, rank: usize) -> crate::error::Result { + let mut names = self.axis_names().to_vec(); let frame = self.rank() - rank; names[..frame].fill(None); self.with_axis_names(names) } - - pub(crate) fn disclose(&self) -> Value { self.elements().next().unwrap_or_else(|| self.prototype().clone()) } - - /// Assemble cells by trailing-axis agreement, padding each with its own fill. Each number keeps its exactness, as items gathered one at a time do, whether written in brackets or computed one cell at a time. - pub(crate) fn assemble(frame: &[usize], cells: &[Self], empty_cell: &Self) -> Result { - Layout::from(frame.to_vec()).assemble(cells, empty_cell) - } - /// The items of this array assembled into one array by trailing-axis agreement, as `assemble` does. A simple array is unchanged. `widening` says how numbers of different dtypes share compact storage: mix joins arrays, and Rank assembles the results it computed one cell at a time. An item pads with `chosen`'s item, or with its own prototype where there's none (`fill_item`). - pub(crate) fn mix(&self, widening: Widening, chosen: Option<&Value>) -> Result { - if !self.is_atom() && self.is_simple() { return Ok(self.clone()); } - let cells: Vec<_> = self.elements().collect(); - Self::assemble_layout(self.layout(), &cells, &self.prototype(), widening, chosen) - } - fn assemble_layout(frame: &Layout, cells: &[Self], empty_cell: &Self, widening: Widening, chosen: Option<&Value>) -> Result { - if element_count(frame.shape())? != cells.len() { return Err(Kind::Length.into()); } - if cells.is_empty() { return frame.concat(empty_cell.layout()).collect(vec![], empty_cell.prototype()); } - let cell_layout = Layout::assembled(cells.iter().map(Value::layout))?; - let cell_shape = cell_layout.shape(); - let rank = cell_shape.len(); - let shape = [frame.shape(), cell_shape].concat(); - let len = generated_len(&shape)?; - let cell_len = element_count(cell_shape)?; - let mut data = Gather::with_widening(&cells.iter().collect::>(), len, widening); - for cell in cells { - if cell.shape() == cell_shape { data.extend(cell, 0..cell_len); continue; } - let pad = rank - cell.shape().len(); - let fill = cell.fill_item(chosen); - for i in 0..cell_len { - let (mut rest, mut source, mut stride, mut inside) = (i, 0, 1, true); - for axis in (0..rank).rev() { - let coord = rest % cell_shape[axis]; - rest /= cell_shape[axis]; - let size = if axis < pad { 1 } else { cell.shape()[axis - pad] }; - inside &= coord < size; - if inside { source += coord * stride; } - stride = stride.saturating_mul(size); - } - if inside { data.push(cell, source) } else { data.fill(&fill, 1) } - } + pub(crate) fn is_view(&self) -> bool { matches!(self, Self::Array(array) if matches!(&**array, ArrayData::Compact { buffer: SharedBuffer::View { .. }, .. })) } + pub(crate) fn footprint(&self) -> usize { self.footprint_in(&mut Seen::default()) } + pub fn export_widening(&self) -> Option { + if self.fixed_width().is_some() { return None; } + let DType::Integer(stored) = self.dtype()? else { return None }; + let width = Numeric::current().integer_width(); (width.bytes() > stored.bytes()).then_some(width) + } + pub fn widened_for_export(&self) -> Option { + let dtype = DType::Integer(self.export_widening()?); + let (view, map) = self.physical()?; + Buffer::cast_mapped(view, map.unwrap_or(&Mapping::Linear(1)), self.len(), dtype).ok() + } + pub fn into_buffer(self) -> Result { + if let Self::Array(array) = self { + return match Arc::try_unwrap(array) { + Ok(ArrayData::Compact { buffer: SharedBuffer::Owned(buffer), .. }) => Ok(buffer), + Ok(array) => { let value = Self::Array(Arc::new(array)); value.view().map(|v| Buffer::copied(v, 0..v.len())).ok_or(value) } + Err(array) => { let value = Self::Array(array); value.view().map(|v| Buffer::copied(v, 0..v.len())).ok_or(value) } + }; } - data.finish(frame.concat(&cell_layout), || cells[0].prototype()) + self.view().map(|v| Buffer::copied(v, 0..v.len())).ok_or(self) } - - fn zeroed_array(&self) -> Self { - let Self::Array(a) = self else { unreachable!() }; - Self::Array(Arc::new(ArrayData { data: Data::Own(match (a.storage(), a.items()) { - (Storage::Mixed(v, memo), _) if v.is_empty() => Storage::Mixed(vec![], memo.clone()), - (Storage::Mixed(v, _), _) => Storage::compact(v.iter().map(Value::zeroed).collect(), Widening::Items), - (_, Items::Compact(View::Booleans(_))) => Storage::Compact(Buffer::Booleans(vec![false; a.len()])), - (_, Items::Compact(View::Integers(_))) => { - Storage::Compact(with_width!(ordinary_width(Width::U8), T => T::buffer(vec![T::from_i64(0); a.len()]))) - } - (_, Items::Fixed(v)) => { - Storage::Fixed(with_width!(v.tag(), T => match T::buffer(vec![T::from_i64(0); a.len()]) { Buffer::Integers(v) => v, _ => unreachable!() })) - } - (_, Items::Compact(View::Floats(v))) => Storage::Compact(Buffer::Floats(FloatBuf::collect(v.tag(), std::iter::repeat_n(0.0, a.len())))), - (_, Items::Compact(View::Complexes(z))) => { - Storage::Compact(Buffer::Floats(FloatBuf::collect(z.tag().into(), std::iter::repeat_n(0.0, a.len())))) - } - (_, Items::Compact(View::Characters(_))) => Storage::Compact(Buffer::Characters(vec![' '; a.len()])), - (_, Items::Values(_)) => unreachable!("mixed storage fills above"), - }), layout: a.layout.clone() })) + pub(crate) fn converted(&self, to: DType, _: Numeric, fixed: bool) -> crate::error::Result { + let mark = crate::number::Mark::Type { dtype: to, fixed }; + mark.conversion().call(&[self.clone()], 0) } + pub(crate) fn to_fixed(&self, width: Width, numeric: Numeric) -> crate::error::Result { self.converted(DType::Integer(width), numeric, true) } + pub(crate) fn to_float_width(&self, width: FloatWidth) -> crate::error::Result { self.converted(DType::Float(width), Numeric::current(), false) } + pub(crate) fn wrapped(&self, numeric: Numeric) -> crate::error::Result { self.wrapped_at(numeric.integer_width(), numeric) } + pub(crate) fn wrapped_at(&self, width: Width, numeric: Numeric) -> crate::error::Result { self.converted(DType::Integer(width), numeric, true) } + pub(crate) fn ordinary(&self) -> Self { + match self { + Self::Number(n) => Self::Number(n.ordinary()), Self::Array(array) => match &**array { + ArrayData::Compact { layout, kind, buffer, range } => Self::Array(Arc::new(ArrayData::Compact { layout: layout.clone(), + kind: crate::number::Kind::stored(kind.dtype(), false, kind.weak), buffer: buffer.clone(), range: *range })), + _ => self.clone(), + }, _ => self.clone(), + } + } + pub(crate) fn strong(&self) -> Self { + match self { Self::Number(n) => Self::Number(n.clone().marked(false)), _ => self.clone() } + } + pub(crate) fn cells(&self, rank: usize) -> Result, ErrorKind> { + let split = self.rank().checked_sub(rank).ok_or(Kind::Rank)?; + let shape = &self.shape()[split..]; let size = shape.iter().product(); let count = self.shape()[..split].iter().product(); + Ok(Cells { array: self, split, count, size, cell_layout: self.layout().axes(split..self.rank()) }) + } + pub fn major_cells(&self) -> crate::error::Result> { self.cells(self.rank().checked_sub(1).ok_or(Kind::Rank)?)?.collect() } } -impl fmt::Display for Value { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&crate::display::plain(self, Elide { display: true, ..Elide::NONE })) } -} -/// A function value, as display shows it. -impl fmt::Display for crate::Function { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "⟨{}⟩", self.bpl()) } } diff --git a/src/array/storage.rs b/src/array/storage.rs index 73ac318f..d9290b64 100644 --- a/src/array/storage.rs +++ b/src/array/storage.rs @@ -1,669 +1,287 @@ -//! Array storage: rustymath's compact buffers or mixed values, the borrowed `Items` view, BPL's promotion rules for compact dtypes, and `Gather`, which builds new storage from the items of existing arrays. `Gather::walk` reads them through rustymath's `gather::Steps`. - -use super::*; -pub(crate) use rustymath::gather::Steps; +//! Borrowed stored items and the one policy for joining separately computed results. +use super::{ArrayData, Layout, Number, SharedBuffer, Value}; +use crate::{ErrorKind, number::{Kind as LogicalKind, Kinds, real}}; use rustymath::{ - buffer::{Buffer, ComplexBuf, Complexes, DType, Element, FloatBuf, Floats, IntBuf, Ints, Source, View, Width}, error::Kind, - with_complexes, with_floats, with_ints, with_width + agreement::Mapping, buffer::{Buffer, ComplexBuf, DType, FloatBuf, FloatWidth, Floats, IntBuf, Ints, View, Width}, + error::Kind, search::{ClassNumbers, Grouping, Ranges}, with_ints, }; -use std::sync::{OnceLock, atomic::{AtomicU8, AtomicU32, Ordering::Relaxed}}; +use std::{borrow::Cow, sync::Arc}; -/// An array's items: a rustymath buffer, whose items share one dtype, or mixed values. Mixed storage keeps each item's dtype. It also remembers what it works out about its items. -#[derive(Clone, Debug)] -pub(crate) enum Storage { Compact(Buffer), Fixed(IntBuf), Mixed(Vec, Memo) } +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum Widening { Arrays, Items } -/// What mixed storage works out about its items on first request. The prototype comes from the first item, and an empty array stores it instead. `environment` and `functions` hold 0 until their fact is first requested. A fact depends only on the items. Relaxed loads and stores are enough, because threads that work out the same fact at the same time store the same value. -#[derive(Debug, Default)] -pub(crate) struct Memo { - pub(super) prototype: OnceLock>, - /// The innermost frame that any item depends on: 1 for none, or the frame's index plus 2. Frame indices stay below the call-depth limit of 20,000. - environment: AtomicU32, - /// Whether any item is a function: 1 for no, 2 for yes. - functions: AtomicU8, -} +impl Widening { + pub(crate) fn of_results(kinds: Kinds) -> Self { if kinds == Kinds::Static { Self::Arrays } else { Self::Items } } -impl Clone for Memo { - fn clone(&self) -> Self { - Self { prototype: self.prototype.clone(), environment: AtomicU32::new(self.environment.load(Relaxed)), - functions: AtomicU8::new(self.functions.load(Relaxed)) } + pub(crate) fn join(self, left: LogicalKind, right: LogicalKind) -> Option { + if self == Self::Items && left.is_exact() != right.is_exact() && !left.weak && !right.weak { return None; } + left.join(right) } } -impl Memo { - /// The memo of an empty array, which stores the array's prototype. - pub(super) fn with_prototype(prototype: Value) -> Self { Self { prototype: OnceLock::from(Box::new(prototype)), ..Self::default() } } - /// Forgets the environment, for a write that may change it. - pub(super) fn forget_environment(&mut self) { *self.environment.get_mut() = 0; } - /// Forgets whether an item is a function, for a write that may change it. - pub(super) fn forget_functions(&mut self) { *self.functions.get_mut() = 0; } - /// Whether the memo has found a function among the items. - pub(super) fn knows_function(&self) -> bool { self.functions.load(Relaxed) == 2 } -} - -/// Whether `x` and `y` hold identical items, as `Value::same` compares them: compact items of one dtype and fixed width bit for bit, values by `Value::same`, and empty items by the prototypes that `prototypes` gives. -pub(super) fn same_items(x: Items, y: Items, prototypes: impl FnOnce() -> (Value, Value)) -> bool { - match (x, y) { - (x, y) if let (Some(a), Some(b)) = (x.view(), y.view()) => x.fixed_width() == y.fixed_width() && a.identical(b), - (Items::Values(a), Items::Values(b)) if !a.is_empty() => a.len() == b.len() && a.iter().zip(b).all(|(p, q)| p.same(q)), - _ if x.len() != y.len() => false, _ if x.len() == 0 => { let (p, q) = prototypes(); p.same(&q) } _ => (0..x.len()).all(|i| x.item(i).same(&y.item(i))) - } +pub(crate) fn joined_kind(items: &[Value], widening: Widening) -> Option { + if !items.iter().all(Value::is_atom) { return None; } + let mut kinds = items.iter().map(Value::kind); + let first = kinds.next()??; + kinds.try_fold(first, |kind, next| widening.join(kind, next?)).map(LogicalKind::strong) } -/// How numbers of different dtypes share compact storage. Compact arrays joined into one widen exact integers to floats, because compact storage holds one dtype, and they read Booleans as the narrowest integers. Items gathered one at a time keep each number's kind: exact and approximate numbers stay apart, and a Boolean stays apart from other numbers. That covers items written in brackets, items computed by an operation and, under dynamic kinds, items written into an existing array (`of_writes`). Among such items only floats widen, to complex. That changes no value, because a complex number with no imaginary part reads back as a float. -#[derive(Clone, Copy, PartialEq)] -pub(crate) enum Widening { Arrays, Items } -impl Widening { - /// How a write into an existing array joins its kind with the kinds written. Under static kinds the whole array takes the compact dtype that holds both, as arrays joined into one do (`Arrays`), so that its type never depends on which items a write reaches. Under dynamic kinds each number keeps its own kind (`Items`). Kinds that no compact dtype holds, such as characters among numbers, leave the array mixed under both. - pub(crate) fn of_writes() -> Self { if crate::number::Numeric::current().is_static() { Self::Arrays } else { Self::Items } } +pub(crate) fn packed(items: &[Value], widening: Widening) -> Option<(Buffer, LogicalKind)> { + if !items.iter().all(|value| matches!(value, Value::Number(_) | Value::Character(_))) { return None; } + let mut kind = joined_kind(items, widening)?; + if matches!(kind.element, crate::number::Element::Ordinary(_)) { + let (lo, hi) = items.iter().try_fold((i64::MAX, i64::MIN), |(lo, hi), value| { + let n = value.as_number()?.as_integer()?; Some((lo.min(n), hi.max(n))) + })?; + kind = LogicalKind::stored(DType::Integer(Width::narrowest(lo, hi)), false, false); + } + let number = |value: &Value| match value { Value::Number(n) => Some(n), _ => None }; + let buffer = match kind.dtype() { + DType::Boolean => Buffer::Booleans(items.iter().map(|v| number(v)?.as_bool()).collect::>()?), + DType::Integer(width) => { + let values = items.iter().map(|v| number(v)?.as_integer()).collect::>>()?; + rustymath::with_width!(width, T => ::buffer(values.into_iter().map(|n| n as T).collect())) + } + DType::Float(width) => Buffer::Floats(FloatBuf::collect(width, items.iter() + .map(|v| number(v)?.to_float().ok()).collect::>>()?.into_iter())), + DType::Complex(width) => Buffer::Complexes(ComplexBuf::collect(width, items.iter() + .map(|v| number(v)?.to_complex().ok()).collect::>>()?.into_iter())), + DType::Character => Buffer::Characters(items.iter().map(|v| if let Value::Character(c) = v { Some(*c) } else { None }).collect::>()?), + }; + Some((buffer, kind)) } -/// BPL's rules for compact dtypes: whether a dtype's numbers are exact, and which dtype holds the numbers of two dtypes. -pub(crate) trait Promotion: Sized { - /// Whether this dtype holds exact numbers: `None` for characters, which aren't numbers. - fn exact(self) -> Option; - /// The dtype that holds the items of both dtypes under `widening`. Characters and numbers share none. Integers of two widths share the narrowest width that holds both. Exact integers stay exact beside an approximate item, so they share no compact dtype with it. - fn join(self, other: Self, widening: Widening) -> Option; - /// The dtype that a result too wide for this one tries next. Booleans that a function doesn't take read as unsigned bytes, which hold every sum and product of two of them. - fn wider(self) -> Option; -} -impl Promotion for DType { - #[inline] - fn exact(self) -> Option { (self != Self::Character).then_some(self.is_integral()) } - fn join(self, other: Self, widening: Widening) -> Option { - use DType::*; - if self == other { return Some(self); } - match (self, other) { (Character, _) | (_, Character) => return None, (Integer(a), Integer(b)) => return Some(Integer(a.join(b))), _ => () } - // Joined compact arrays read Booleans as the narrowest integers. Items gathered one at a time keep a Boolean apart from other numbers. - if self == Boolean || other == Boolean { if widening == Widening::Items { return None; } return Some(if self == Boolean { other } else { self }); } - if widening == Widening::Items && (self.exact() == Some(true) || other.exact() == Some(true)) { return None; } - self.common(other) - } - fn wider(self) -> Option { - match self { Self::Boolean => Some(Self::Integer(Width::U8)), Self::Integer(width) => width.wider().map(Self::Integer), _ => None } - } +pub(crate) fn ragged_rank(items: &[Value]) -> Option { + let first = items.first()?; + if first.is_atom() || first.kind().is_none() { return None; } + let rank = first.rank(); + items.iter().all(|v| !v.is_atom() && v.kind() == first.kind() && v.rank() == rank && v.layout().plain()).then_some(rank) } -/// The width at which static kinds store and compute ordinary integers that `width` holds: the `int` width, or `width` where that's wider. `None` under dynamic kinds. -#[inline] -pub(crate) fn static_width(width: Width) -> Option { crate::settings::int_storage().map(|int| int.join(width)) } -/// The width that stores ordinary integers that `width` holds: `width` itself, the narrowest that holds their values, under dynamic kinds, and `static_width` under static kinds. -#[inline] -pub(crate) fn ordinary_width(width: Width) -> Width { static_width(width).unwrap_or(width) } -/// The compact dtype that holds `item`, if any does (`Number::dtype`). -pub(crate) fn item_dtype(item: &Value) -> Option { - match item { Value::Number(n) => n.dtype(), Value::Character(_) => Some(DType::Character), _ => None } -} -/// Whether `item` is a weak number (`Number::is_weak`). -fn is_weak(item: &Value) -> bool { matches!(item, Value::Number(n) if n.is_weak()) } -/// Whether `a` and `b` are items of one representation, such as two 64-bit floats, two exact integers or two characters (`Number::same_variant`). -fn same_variant(a: &Value, b: &Value) -> bool { - match (a, b) { (Value::Number(x), Value::Number(y)) => x.same_variant(y), _ => std::mem::discriminant(a) == std::mem::discriminant(b) } +fn disjoint(offsets: &[usize], sizes: impl Iterator) -> bool { + let mut ranges: Vec<_> = offsets.iter().copied().zip(sizes).filter(|&(_, n)| n != 0).map(|(a, n)| a..a + n).collect(); + ranges.sort_unstable_by_key(|r| r.start); + ranges.windows(2).all(|r| r[0].end <= r[1].start) } -/// The compact dtype that holds every item of `data` under `widening`, if one does. Ordinary integers take the width that stores them all (`ordinary_width`). A weak number beside strong floats or complex numbers takes their width, wherever it sits (`plan::Kind::join`). -pub(super) fn items_kind(data: &[Value], widening: Widening) -> Option<(DType, bool)> { - let first = data.first()?; - let first_dtype = item_dtype(first)?; - // Comparing each item's variant with the first item's is cheap enough for copies of mixed storage that stay mixed. Only items from the first one of another variant on need their dtypes joined. - let run = data.iter().position(|v| !same_variant(v, first)).unwrap_or(data.len()); - let head = match first_dtype { - DType::Integer(_) if first.fixed_width().is_none() => { - let (lo, hi) = data[..run].iter().filter_map(|v| if let Value::Number(n) = v { n.as_integer() } else { None }) - .fold((i64::MAX, i64::MIN), |(lo, hi), i| (lo.min(i), hi.max(i))); - DType::Integer(ordinary_width(Width::narrowest(lo, hi))) - } - dtype => dtype, - }; - let weak = data[..run].iter().all(is_weak); - let prefix = crate::plan::Kind::stored(Some(head), first.fixed_width(), weak); - let joined = crate::plan::Kind::join([prefix].into_iter().chain(data[run..].iter().map(crate::plan::Kind::item)), widening).ok()?; - joined.dtype.map(|dtype| (dtype, joined.fixed.is_some())) +#[derive(Clone, Debug)] +pub(crate) struct Segments { + pub(crate) leaf: Arc, + offsets: Arc<[usize]>, + shapes: Arc<[usize]>, + item_rank: usize, + prototype: Value, + classes: Option>, } -/// Runs `$body` with `$d` bound to the target buffer, `$s` to the source items and `$f` to the conversion of a source item for the target, when the target's compact dtype holds the source's items. Gives whether it ran. Each line lists the sources that one target takes and how it converts them. Numbers widen from Boolean through the integer widths to the float widths to the complex widths. A narrower float or complex buffer receives wider items only when a conversion asks for that width. A narrower integer line takes a 64-bit source only for an atom, whose value `Gather::follow` makes sure the buffer's width holds. A conversion between items of one type is the identity. With `|$d, $s|` and no `$f`, the macro runs `$body` only where the source and target hold one type. -macro_rules! copy_into { - (@to Boolean $d:ident) => { Storage::Compact(Buffer::Booleans($d)) }; - (@to F16 $d:ident) => { Storage::Compact(Buffer::Floats(FloatBuf::F16($d))) }; - (@to F32 $d:ident) => { Storage::Compact(Buffer::Floats(FloatBuf::F32($d))) }; - (@to F64 $d:ident) => { Storage::Compact(Buffer::Floats(FloatBuf::F64($d))) }; - (@to C32 $d:ident) => { Storage::Compact(Buffer::Complexes(ComplexBuf::F32($d))) }; - (@to C64 $d:ident) => { Storage::Compact(Buffer::Complexes(ComplexBuf::F64($d))) }; - (@to Character $d:ident) => { Storage::Compact(Buffer::Characters($d)) }; - (@to $width:ident $d:ident) => { Storage::Compact(Buffer::Integers(IntBuf::$width($d))) | Storage::Fixed(IntBuf::$width($d)) }; - (@from Booleans $s:ident) => { Items::Compact(View::Booleans($s)) }; - (@from F16 $s:ident) => { Items::Compact(View::Floats(Floats::F16($s))) }; - (@from F32 $s:ident) => { Items::Compact(View::Floats(Floats::F32($s))) }; - (@from F64 $s:ident) => { Items::Compact(View::Floats(Floats::F64($s))) }; - (@from C32 $s:ident) => { Items::Compact(View::Complexes(Complexes::F32($s))) }; - (@from C64 $s:ident) => { Items::Compact(View::Complexes(Complexes::F64($s))) }; - (@from Characters $s:ident) => { Items::Compact(View::Characters($s)) }; - (@from $width:ident $s:ident) => { Items::Compact(View::Integers(Ints::$width($s))) | Items::Fixed(Ints::$width($s)) }; - (@run $body:expr, $f:ident, $conversion:expr) => {{ - let $f = $conversion; - $body; - true - }}; - (@lines $scrutinee:expr, $d:ident, $s:ident, $f:ident, $body:expr; $($to:ident: $($from:ident),+ => $conversion:expr;)+) => { - match $scrutinee { - $($((copy_into!(@to $to $d), copy_into!(@from $from $s)) => copy_into!(@run $body, $f, $conversion),)+)+ - _ => false, - } - }; - (@same $scrutinee:expr, $d:ident, $s:ident, $body:expr; $($to:ident $from:ident),+) => { - match $scrutinee { - $((copy_into!(@to $to $d), copy_into!(@from $from $s)) => { $body; true },)+ - _ => false, - } - }; - ($target:expr, $source:expr, |$d:ident, $s:ident, $f:ident| $body:expr) => { - copy_into!(@lines ($target, $source), $d, $s, $f, $body; - Boolean: Booleans => Source::::read; U8: Booleans, U8, I64 => |x| u8::from_i64(i64::from(x)); - I16: Booleans, U8, I16, I64 => |x| i16::from_i64(i64::from(x)); I32: Booleans, U8, I16, I32, I64 => |x| i32::from_i64(i64::from(x)); - I64: Booleans, U8, I16, I32, I64 => i64::from; F64: Booleans, U8, I16, I32, I64, F16, F32, F64 => Source::::read; - F32: Booleans, U8, I16, I32, I64, F16, F32, F64 => Source::::read; - F16: Booleans, U8, I16, I32, I64, F16, F32, F64 => Source::::read; - C64: Booleans, U8, I16, I32, I64, F16, F32, F64, C32, C64 => Source::::read; - C32: Booleans, U8, I16, I32, I64, F16, F32, F64, C32, C64 => Source::::read; Character: Characters => Source::::read; - ) - }; - ($target:expr, $source:expr, |$d:ident, $s:ident| $body:expr) => { - copy_into!(@same ($target, $source), $d, $s, $body; - Boolean Booleans, U8 U8, I16 I16, I32 I32, I64 I64, F16 F16, F32 F32, F64 F64, C32 C32, C64 C64, Character Characters) - }; -} +#[derive(Clone, Debug)] +pub(crate) struct Classes { pub(crate) values: Value, pub(crate) numbers: Value, pub(crate) count: usize } -impl Storage { - pub(crate) fn mixed(data: Vec) -> Self { Self::Mixed(data, Memo::default()) } - pub(crate) fn fixed_width(&self) -> Option { match self { Self::Fixed(v) => Some(v.view().tag()), _ => None } } - /// The items' dtype, read from the buffer with no view of the items. `None` for mixed storage. - pub(crate) fn dtype(&self) -> Option { - match self { Self::Compact(b) => Some(b.dtype()), Self::Fixed(v) => Some(DType::Integer(v.view().tag())), Self::Mixed(..) => None } - } - pub(crate) fn fixed(self, fixed: bool) -> Self { - match (self, fixed) { - (Self::Compact(Buffer::Integers(v)), true) => Self::Fixed(v), (Self::Fixed(v), false) => Self::Compact(Buffer::Integers(v)), (storage, _) => storage - } - } - pub(crate) fn fixed_at(buffer: Buffer, width: Width) -> Self { - match buffer { - Buffer::Integers(v) if v.view().tag() == width => Self::Fixed(v), - Buffer::Integers(v) => { - Self::Compact(with_width!(width, T => with_ints!(v.view(), |v| T::buffer(v.iter().map(|&n| T::from_i64(n.into())).collect())))).fixed(true) - } - buffer => Self::Compact(buffer), - } - } - pub(super) fn set_fixed(&mut self, fixed: bool) { - if self.fixed_width().is_some() == fixed { return; } - let storage = std::mem::replace(self, Self::Compact(Buffer::Booleans(Vec::new()))); - *self = storage.fixed(fixed); - } - pub(super) fn into_buffer(self) -> Option { match self { Self::Compact(b) => Some(b), Self::Fixed(b) => Some(Buffer::Integers(b)), _ => None } } - /// The ordinary integers `data` at the width that stores them (`ordinary_width`). Under dynamic kinds that's the narrowest width that holds them, from their minimum and maximum, which rustymath finds together in one pass at the width of `T`. Under static kinds the `int` width holds them with no pass, unless `T` is wider. - pub(crate) fn narrowed(data: Vec) -> Self where rustymath::reduce::Max: rustymath::reduce::Reduction, - rustymath::reduce::Min: rustymath::reduce::Reduction { - let int = crate::settings::int_storage(); - if let Some(int) = int && int.join(T::WIDTH) == int { return Self::within(data, int); } - let (min, max) = rustymath::reduce::extremes(&data); - Self::within(data, ordinary_width(Width::narrowest(min, max))) - } - /// The integers `data` at `width`, which holds them all. Integers that have the width already keep their buffer. - pub(crate) fn within(data: Vec, width: Width) -> Self { - Self::Compact(if width == T::WIDTH { T::buffer(data) } - else { with_width!(width, U => U::buffer(data.iter().map(|&n| U::from_i64(n.to_i64())).collect())) }) - } - /// Copies `range` of `source` when it has this storage's dtype, and gives whether it could. - fn copy_slice(&mut self, source: Items, range: std::ops::Range) -> bool { - match (self, source.view()) { - (Self::Compact(d), Some(s)) => d.extend_from(s, range), (Self::Fixed(d), Some(View::Integers(s))) => d.extend_from(s, range), _ => false - } - } - pub(super) fn with_capacity(dtype: Option, capacity: usize) -> Self { - match dtype { Some(dtype) => Self::Compact(Buffer::with_capacity(dtype, capacity)), None => Self::mixed(Vec::with_capacity(capacity)) } - } - pub(crate) fn items(&self) -> Items<'_> { - match self { Self::Compact(b) => Items::Compact(b.view()), Self::Fixed(b) => Items::Fixed(b.view()), Self::Mixed(v, _) => Items::Values(v) } - } - pub(super) fn len(&self) -> usize { self.items().len() } - /// A copy of `items`. - pub(super) fn copy_of(items: Items) -> Self { - match items { - Items::Values(v) => Self::mixed(v.to_vec()), - items => Self::Compact(Buffer::copied(items.view().unwrap(), 0..items.len())).fixed(items.fixed_width().is_some()), - } - } - fn capacity(&self) -> usize { match self { Self::Compact(b) => b.capacity(), Self::Fixed(b) => b.capacity(), Self::Mixed(v, _) => v.capacity() } } - pub(super) fn item(&self, i: usize) -> Value { self.items().item(i) } - /// Compact storage gives its dtype's zero. An empty mixed array keeps a stored prototype. A nonempty one fills its first item on first request and keeps the result. - pub(super) fn prototype(&self) -> Value { - if let Self::Mixed(d, memo) = self { return memo.prototype.get_or_init(|| Box::new(d[0].zeroed())).as_ref().clone(); } - self.items().prototype().expect("mixed storage gives its prototype above") +#[derive(Clone, Copy)] +pub(crate) enum ItemGroups<'a> { Ranges(Ranges<'a>), Classes(usize, Ints<'a>) } + +impl Grouping for ItemGroups<'_> { + fn len(&self) -> usize { match *self { Self::Ranges(r) => r.len(), Self::Classes(count, _) => count } } + fn fold>(&self, data: &[T]) -> (Vec, bool) { + match *self { Self::Ranges(r) => r.fold::(data), Self::Classes(count, groups) => with_ints!(groups, |g| ClassNumbers { count, numbers: g }.fold::(data)) } } - /// Whether every number, including those in nested arrays, is exact. `None` when there are no numbers. An empty mixed array answers for its prototype. - pub(super) fn exact(&self) -> Option { - match self.items() { - Items::Values([]) => self.prototype().exact_domain(), Items::Values(d) => d.iter().filter_map(Value::exact_domain).reduce(|a, b| a && b), - items => items.dtype().and_then(DType::exact) - } + fn scan>(&self, data: &[T]) -> (Vec, bool) { + match *self { Self::Ranges(r) => r.scan::(data), Self::Classes(count, groups) => with_ints!(groups, |g| ClassNumbers { count, numbers: g }.scan::(data)) } } - /// The innermost frame that any item depends on. Mixed storage works it out on first request. An empty array answers for its prototype. - pub(super) fn environment(&self) -> Option { - let Self::Mixed(items, memo) = self else { return None }; - match memo.environment.load(Relaxed) { - 0 => { - let deepest = if items.is_empty() { self.prototype().environment() } else { items.iter().filter_map(Value::environment).max() }; - memo.environment.store(deepest.map_or(1, |i| i as u32 + 2), Relaxed); - deepest.map(|i| i as u16) - } - known => known.checked_sub(2).map(|i| i as u16), - } +} + +impl Segments { + pub(crate) fn new(leaf: Arc, offsets: Vec, shapes: Vec, rank: usize, prototype: Value) -> Self { + debug_assert_eq!(shapes.len(), offsets.len() * rank); + Self { leaf, offsets: offsets.into(), shapes: shapes.into(), item_rank: rank, prototype, classes: None } } - /// Whether an item is a function. Compact storage holds none, and mixed storage looks on first request. - pub(super) fn holds_functions(&self) -> bool { - let Self::Mixed(items, memo) = self else { return false }; - match memo.functions.load(Relaxed) { - 0 => { - let holds = items.iter().any(|v| matches!(v, Value::Function(_))); - memo.functions.store(1 + u8::from(holds), Relaxed); - holds - } - known => known == 2, - } + + pub(crate) fn packed(leaf: Arc, shapes: Vec, rank: usize, prototype: Value) -> Self { + let count = if rank == 0 { leaf.len() } else { shapes.len() / rank }; + let mut offset = 0; + let offsets = (0..count).map(|i| { + let start = offset; offset += shapes[i * rank..(i + 1) * rank].iter().product::(); start + }).collect(); + Self::new(leaf, offsets, shapes, rank, prototype) } - /// Compact storage of the dtype that `widening` gives the items (`items_kind`), when there is one. Other items stay mixed, and a weak number among them becomes strong, as an array's items are. - pub(super) fn compact(mut data: Vec, widening: Widening) -> Self { - // Collecting from `data.iter()` sizes the compact buffer exactly. Consuming `data` would reuse its larger allocation. - match items_kind(&data, widening) { - Some((dtype, fixed)) => Self::of_dtype(dtype, &data, fixed), - None => { for v in &mut data { if let Value::Number(n) = v { n.mark(false); } } Self::mixed(data) } + + pub(crate) fn of(items: &[Value]) -> Option { + let rank = ragged_rank(items)?; + let Value::Array(first) = &items[0] else { return None }; + let (base, _) = ArrayData::physical(first); + let mut offsets = Vec::with_capacity(items.len()); + let mut shapes = Vec::with_capacity(items.len() * rank); + for item in items { + let Value::Array(array) = item else { return None }; + let (own, map) = ArrayData::physical(array); + if !Arc::ptr_eq(base, own) { return None; } + let start = match map { + None | Some(Mapping::Linear(1)) => 0, + Some(map) if item.len() == 1 => map.get(0)?, + Some(map) => map.run(0, item.len()).filter(|&(_, step)| step == 1)?.0, + }; + offsets.push(start); shapes.extend_from_slice(item.shape()); } + Some(Self::new(base.clone(), offsets, shapes, rank, items[0].prototype())) } - /// One item, in the compact storage of its own dtype when it has one, as `compact` would hold it. An ordinary integer takes the width that stores it (`ordinary_width`). - pub(super) fn one(item: &Value) -> Self { - let fixed = item.fixed_width().is_some(); - match item_dtype(item) { - Some(DType::Integer(width)) if !fixed => Self::of_dtype(DType::Integer(ordinary_width(width)), std::slice::from_ref(item), false), - Some(dtype) => Self::of_dtype(dtype, std::slice::from_ref(item), fixed), None => Self::mixed(vec![item.clone().strong()]) - } + + pub(crate) fn with_classes(self, classes: Classes) -> Self { Self { classes: Some(Arc::new(classes)), ..self } } + pub(crate) fn segs(&self) -> Segs<'_> { Segs { owner: self, start: 0, len: self.offsets.len() } } + pub(crate) fn prototype(&self) -> &Value { &self.prototype } + pub(crate) fn item_rank(&self) -> usize { self.item_rank } + pub(crate) fn with_parts(&self, leaf: Arc, prototype: Value) -> Self { Self { leaf, prototype, ..self.clone() } } +} + +#[derive(Clone, Copy)] +pub(crate) struct Segs<'a> { owner: &'a Segments, start: usize, len: usize } + +impl<'a> Segs<'a> { + pub(crate) fn len(self) -> usize { self.len } + pub(crate) fn rank(self) -> usize { self.owner.item_rank } + pub(crate) fn shape(self, i: usize) -> &'a [usize] { + let rank = self.rank(); let start = (self.start + i) * rank; &self.owner.shapes[start..start + rank] + } + pub(crate) fn shapes(self) -> &'a [usize] { + let rank = self.rank(); &self.owner.shapes[self.start * rank..(self.start + self.len) * rank] + } + pub(crate) fn range(self, i: usize) -> std::ops::Range { + let start = self.owner.offsets[self.start + i]; start..start + self.shape(i).iter().product::() + } + pub(crate) fn ranges(self) -> impl Iterator> + 'a { (0..self.len).map(move |i| self.range(i)) } + pub(crate) fn slice(self, range: std::ops::Range) -> Self { Self { start: self.start + range.start, len: range.len(), ..self } } + pub(crate) fn leaf_value(self) -> Value { Value::Array(self.owner.leaf.clone()) } + pub(crate) fn leaf_items(self) -> Items<'a> { self.owner.leaf.storage() } + pub(crate) fn leaf_dtype(self) -> Option { self.owner.leaf.kind().map(LogicalKind::dtype) } + pub(crate) fn fixed(self) -> bool { self.owner.leaf.kind().and_then(LogicalKind::fixed_width).is_some() } + pub(crate) fn disjoint(self) -> bool { disjoint(&self.owner.offsets[self.start..self.start + self.len], (0..self.len).map(|i| self.range(i).len())) } + pub(crate) fn prototype(self) -> &'a Value { &self.owner.prototype } + pub(crate) fn values(self) -> impl ExactSizeIterator + 'a { (0..self.len()).map(move |i| self.item(i)) } + + fn item(self, i: usize) -> Value { + Value::stored_part(&self.owner.leaf, self.range(i), Layout::from(self.shape(i))).expect("a segment fits its stored leaf") } - /// Compact storage of `dtype` for `data`, whose items all have that dtype or one it holds. - fn of_dtype(dtype: DType, data: &[Value], fixed: bool) -> Self { - Self::Compact(match dtype { - DType::Boolean => Buffer::Booleans(data.iter().map(|e| number(e).unwrap().as_bool().unwrap()).collect()), - // The widest of the items' own widths holds every item. - DType::Integer(width) => { - with_width!(width, T => T::buffer(data.iter().map(|e| T::from_i64(number(e).unwrap().as_integer().unwrap())).collect())) - } - DType::Float(width) => Buffer::Floats(FloatBuf::collect(width, data.iter().map(|e| number(e).unwrap().to_float().unwrap()))), - DType::Complex(width) => Buffer::Complexes(ComplexBuf::collect(width, data.iter().map(|e| number(e).unwrap().to_complex().unwrap()))), - DType::Character => Buffer::Characters(data.iter().map(|e| if let Value::Character(c) = e { *c } else { unreachable!() }).collect()), - }).fixed(fixed) + + pub(crate) fn stacked(self, frame: &Layout) -> Option { + let shape = if self.len == 0 { self.prototype().shape() } else { self.shape(0) }; + if (0..self.len).any(|i| self.shape(i) != shape) { return None; } + let start = self.ranges().next().map_or(0, |r| r.start); + let per: usize = shape.iter().product(); + if self.ranges().enumerate().any(|(i, r)| r.start != start + i * per) { return None; } + Value::stored_part(&self.owner.leaf, start..start + self.len * per, frame.concat(&Layout::from(shape))).ok() } -} -impl Footprint for Storage { - /// The bytes of these items: a compact buffer's allocation, or a mixed array's values with what each one keeps alive. - fn footprint_in(&self, seen: &mut Seen) -> usize { - match self { - Self::Compact(buffer) => buffer.capacity() * buffer.dtype().bytes(), - Self::Fixed(buffer) => buffer.capacity() * DType::Integer(buffer.view().tag()).bytes(), - Self::Mixed(values, _) => values.capacity() * size_of::() + values.iter().map(|v| v.footprint_in(seen)).sum::(), + pub(crate) fn grouping(self) -> (Value, ItemGroups<'a>) { + if let Some(classes) = self.owner.classes.as_ref().filter(|_| self.start == 0 && self.len == self.owner.offsets.len()) { + return (classes.values.clone(), ItemGroups::Classes(classes.count, classes.numbers.as_items().ints().expect("stored class numbers"))); } + (self.leaf_value(), ItemGroups::Ranges(Ranges { starts: &self.owner.offsets[self.start..self.start + self.len], lens: self.shapes() })) } + + pub(crate) fn by_classes(self) -> bool { self.owner.classes.is_some() } } -/// The items of a value, borrowed in their storage type: a rustymath view of compact storage, or mixed values. Reading items through this view avoids building a `Value` for each one. #[derive(Clone, Copy)] -pub(crate) enum Items<'a> { Compact(View<'a>), Fixed(Ints<'a>), Values(&'a [Value]) } - -/// The real number `z` holds, when its imaginary part is zero. -fn real_part>(z: &Complex) -> Result { if z.im.into() == 0.0 { Ok(z.re.into()) } else { Err(Kind::Domain.into()) } } +pub(crate) enum Items<'a> { + Compact(View<'a>), + Fixed(Ints<'a>), + Values(&'a [Value]), + Segmented(Segs<'a>), + Mapped { source: &'a ArrayData, map: &'a Mapping, start: usize, len: usize }, +} impl<'a> Items<'a> { - pub(crate) fn view(self) -> Option> { - match self { Self::Compact(v) => Some(v), Self::Fixed(v) => Some(View::Integers(v)), Self::Values(_) => None } + pub(crate) fn physical(self) -> Option<(View<'a>, Option<&'a Mapping>)> { + match self { + Self::Compact(view) => Some((view, None)), Self::Fixed(ints) => Some((View::Integers(ints), None)), + Self::Mapped { source, map, start: 0, .. } => Some((source.storage().view()?, Some(map))), + _ => None, + } } - pub(crate) fn fixed_width(self) -> Option { match self { Self::Fixed(v) => Some(v.tag()), _ => None } } - pub(crate) fn fixed(self, fixed: bool) -> Self { - match (self, fixed) { - (Self::Compact(View::Integers(v)), true) => Self::Fixed(v), (Self::Fixed(v), false) => Self::Compact(View::Integers(v)), (items, _) => items + + pub(crate) fn view(self) -> Option> { + match self { + Self::Compact(view) => Some(view), Self::Fixed(ints) => Some(View::Integers(ints)), + Self::Mapped { source, map, start, len } => { + if len == 0 { return Some(source.storage().view()?.slice(0..0)); } + if matches!(map, Mapping::Linear(1)) { return Some(source.storage().view()?.slice(start..start + len)); } + if len == 1 { let base = map.get(start)?; return Some(source.storage().view()?.slice(base..base + 1)); } + let (base, step) = map.run(start, len)?; + (step == 1).then(|| source.storage().view().map(|v| v.slice(base..base + len))).flatten() + } + _ => None, } } - pub(crate) fn prototype(self) -> Option { - Some(match self { - Self::Fixed(v) => Value::Number(Number::fixed(0, v.tag())), Self::Compact(View::Booleans(_)) => Value::Number(Number::from_bool(false)), - Self::Compact(View::Integers(_)) => Value::integer(0), Self::Compact(View::Floats(v)) => Value::Number(Number::float(0.0, v.tag())), - Self::Compact(View::Complexes(v)) => Value::Number(Number::float(0.0, v.tag().into())), Self::Compact(View::Characters(_)) => Value::Character(' '), - Self::Values(_) => return None - }) + + pub(crate) fn len(self) -> usize { + match self { Self::Compact(v) => v.len(), Self::Fixed(v) => v.len(), Self::Values(v) => v.len(), + Self::Segmented(s) => s.len(), Self::Mapped { len, .. } => len } } - /// The dtype of these items, or `None` for mixed ones. - pub(super) fn dtype(&self) -> Option { self.view().map(View::dtype) } - pub(crate) fn len(&self) -> usize { match self { Self::Compact(v) => v.len(), Self::Fixed(v) => v.len(), Self::Values(v) => v.len() } } - /// The items in `range`, borrowed with no copy. pub(crate) fn slice(self, range: std::ops::Range) -> Self { match self { - Self::Values(v) => Self::Values(&v[range]), items => Self::Compact(items.view().unwrap().slice(range)).fixed(items.fixed_width().is_some()) + Self::Compact(v) => Self::Compact(v.slice(range)), + Self::Fixed(v) => { let View::Integers(v) = View::Integers(v).slice(range) else { unreachable!() }; Self::Fixed(v) } + Self::Values(v) => Self::Values(&v[range]), Self::Segmented(s) => Self::Segmented(s.slice(range)), + Self::Mapped { source, map, start, .. } => Self::Mapped { source, map, start: start + range.start, len: range.len() }, } } - #[inline] + pub(crate) fn item(self, i: usize) -> Value { match self { - Self::Fixed(v) => Value::Number(Number::fixed(v.get(i), v.tag())), Self::Compact(View::Booleans(v)) => Value::Number(Number::from_bool(v[i])), Self::Compact(View::Integers(v)) => Value::integer(v.get(i)), - Self::Compact(View::Floats(v)) => Value::Number(Number::float(with_floats!(v, |x| f64::from(x[i])), v.tag())), - // An item with no imaginary part is a widened real, except under static kinds. - Self::Compact(View::Complexes(v)) => { - Value::Number(Number::complex_in_session(with_complexes!(v, |x| Complex64::new(x[i].re.into(), x[i].im.into())), v.tag().into())) - } + Self::Fixed(v) => Value::Number(Number::fixed(v.get(i), v.tag())), + Self::Compact(View::Floats(v)) => Value::Number(Number::float(v.get(i), v.tag())), + Self::Compact(View::Complexes(v)) => Value::Number(Number::complex_kept(v.get(i), v.tag().into())), Self::Compact(View::Characters(v)) => Value::Character(v[i]), - Self::Values(v) => v[i].clone(), + Self::Values(v) => v[i].clone(), Self::Segmented(s) => s.item(i), + Self::Mapped { source, map, start, .. } => map.get(start + i).map_or_else(|| source.prototype(), |i| source.storage().item(i)), } } - /// Item `i`, borrowed where the items are values. - #[inline] - pub(super) fn item_ref(self, i: usize) -> Cow<'a, Value> { match self { Self::Values(v) => Cow::Borrowed(&v[i]), _ => Cow::Owned(self.item(i)) } } - /// Each item through `int`, `real` or `other` by its storage. A complex item must be real. Characters are DOMAIN. An item that a running level carries reads as a count does (`Value::observed_number`): a dual gives its primal, because no derivative flows through an integer, and a batch stops its run. - fn each( - &self, int: impl Fn(i64) -> Result, real: impl Fn(f64) -> Result, other: impl Fn(&Number) -> Result - ) -> Result, ErrorKind> { - match *self { - Self::Compact(View::Booleans(d)) => d.iter().map(|&b| int(b.into())).collect(), - Self::Compact(View::Integers(ints)) | Self::Fixed(ints) => with_ints!(ints, |d| d.iter().map(|&n| int(n.to_i64())).collect()), - Self::Compact(View::Floats(d)) => with_floats!(d, |d| d.iter().map(|&n| real(n.into())).collect()), - Self::Compact(View::Complexes(d)) => with_complexes!(d, |d| d.iter().map(|z| real_part(z).and_then(&real)).collect()), - Self::Compact(View::Characters(_)) => Err(Kind::Domain.into()), Self::Values(d) => d.iter().map(|v| other(v.observed_number()?)).collect() - } - } - /// Each item as an integer. 64-bit integer storage is borrowed. A fraction, an infinity or a non-number is DOMAIN, and a value outside `i64` is LIMIT. - pub(crate) fn integers(&self) -> Result, ErrorKind> { - use rustymath::tolerant::{Tolerance, wholes}; - if let Some(ints) = self.ints() { return Ok(ints.widened()); } - // Floats that all stand for integers inside `i64` convert in one pass. Others go item by item, which gives the error. - const LIMIT: f64 = 9_223_372_036_854_775_808.0; - let converted = match *self { - Self::Compact(View::Floats(Floats::F64(d))) => wholes(Tolerance::DEFAULT, d, LIMIT), - Self::Compact(View::Floats(Floats::F32(d))) => wholes(Tolerance::DEFAULT, d, LIMIT), _ => None - }; - if let Some((n, _)) = converted { return Ok(Cow::Owned(n)); } - self.each(Ok, real::integer, Number::integer).map(Cow::Owned) + pub(crate) fn item_ref(self, i: usize) -> Cow<'a, Value> { + match self { Self::Values(v) => Cow::Borrowed(&v[i]), _ => Cow::Owned(self.item(i)) } } - /// Each item as a signed position: an integer, or `None` for NaN, which reads a fill. Compact storage decodes through rustymath's `structure::signed_positions`, and every float as `structure::float_position` reads it. A fraction, an infinity or a non-number is DOMAIN. - pub(crate) fn positions(&self) -> Result>, ErrorKind> { - if let Some(view @ (View::Booleans(_) | View::Integers(_) | View::Floats(_))) = self.view() { - return rustymath::structure::signed_positions(view).map_err(|_| Kind::Domain.into()); - } - let real = |y: f64| -> Result, ErrorKind> { rustymath::structure::float_position(y).map_err(|_| Kind::Domain.into()) }; - self.each(|n| Ok(Some(n)), real, |n| match n.as_float() { Some(y) => real(y), None => n.integer().map(Some) }) - } - /// The items of Boolean or integer storage as integers. `None` for any other storage. - pub(crate) fn exact_integers(&self) -> Option> { - match self.view() { Some(View::Booleans(_) | View::Integers(_)) => self.integers().ok(), _ => None } + pub(crate) fn values(self) -> impl Iterator> { (0..self.len()).map(move |i| self.item_ref(i)) } + pub(crate) fn nested(self) -> bool { matches!(self, Self::Values(_) | Self::Segmented(_)) } + pub(crate) fn fixed_width(self) -> Option { match self { Self::Fixed(v) => Some(v.tag()), _ => None } } + pub(crate) fn dtype(self) -> Option { self.physical().map(|(v, _)| v.dtype()) } + pub(crate) fn ints(self) -> Option> { match self.view()? { View::Integers(v) => Some(v), _ => None } } + pub(crate) fn prototype(self) -> Option { + Some(match self { + Self::Fixed(v) => Value::Number(Number::fixed(0, v.tag())), + Self::Compact(View::Booleans(_)) => Value::Number(Number::from_bool(false)), + Self::Compact(View::Integers(_)) => Value::integer(0), + Self::Compact(View::Floats(v)) => Value::Number(Number::float(0.0, v.tag())), + Self::Compact(View::Complexes(v)) => Value::Number(Number::complex_kept(num_complex::Complex64::new(0.0, 0.0), v.tag().into())), + Self::Compact(View::Characters(_)) => Value::Character(' '), + Self::Mapped { source, .. } => source.prototype(), + Self::Segmented(s) => s.prototype().clone(), Self::Values(_) => return None, + }) } - /// The integers of integer storage of any width. - pub(crate) fn ints(self) -> Option> { match self { Self::Compact(View::Integers(ints)) | Self::Fixed(ints) => Some(ints), _ => None } } - /// Each item as a count of type `T`. A negative item is also DOMAIN. Compact numbers that `integers` converts in one pass and that are all nonnegative take that path. Others go item by item, which gives the error and reads counts beyond `i64`. - pub(crate) fn nonnegative_integers>(&self) -> Result, ErrorKind> { - if let Some(View::Integers(_) | View::Floats(_)) = self.view() && let Ok(n) = self.integers() { - // One fold with no branch finds the least and greatest counts, which keeps the loop vectorised. - let (lo, hi) = n.iter().fold((0, 0), |(lo, hi): (i64, i64), &x| (lo.min(x), hi.max(x))); - if lo >= 0 { - // Every count fits `T` when the largest does, so the conversion loop needs no check of its own. - crate::number::count_as::(hi as u64)?; - return Ok(n.into_owned().into_iter().map(|n| T::try_from(n as u64).ok().expect("each count is at most the largest")).collect()); - } - } - self.each( - |n| u64::try_from(n).map_err(|_| Kind::Domain.into()).and_then(crate::number::count_as), real::nonnegative_integer, Number::nonnegative_integer) + pub(crate) fn stacked(self, frame: &Layout) -> Option<(Value, usize)> { + match self { Self::Segmented(s) => Some((s.stacked(frame)?, s.rank())), _ => None } } -} - -fn number(value: &Value) -> Result<&Number, ErrorKind> { match value { Value::Number(n) => Ok(n), _ => Err(Kind::Domain.into()) } } -/// Items copied from source arrays into a new array. The buffer takes the compact dtype that its `widening` gives the nonempty sources, and widens for wider items. A weak number beside strong floats or complex numbers takes their width, wherever it comes among the items (`plan::Kind::join`), so `weak` records whether only weak numbers gave the buffer's dtype. A mixed source, or an item that no compact dtype holds, makes the buffer mixed, and it stays mixed. -/// -/// A source that carries a dual or a labelled value starts a join (`transform::Join`): the buffer then copies what each source holds and that level's data into buffers of their own, and `finish` gives an array that carries the data again. Number duals that `add` takes are repacked this way into an array dual. -pub(crate) struct Gather { - pub(super) data: Storage, dtype: Option, open: bool, weak: bool, widening: Widening, join: Option> -} -impl Gather { - /// A buffer for an operation's result, joining compact arrays. - pub(crate) fn new(sources: &[&Value], capacity: usize) -> Self { Self::with_widening(sources, capacity, Widening::Arrays) } - /// A buffer that continues `data`, for writing into an existing array under `widening`. Empty compact storage takes the dtype of its first items, as joining ignores an empty compact array. - pub(super) fn resume(data: Storage, widening: Widening) -> Self { - let dtype = data.dtype(); - Self { open: dtype.is_some() && data.len() == 0, dtype, data, weak: false, widening, join: None } - } - pub(crate) fn with_widening(sources: &[&Value], capacity: usize, widening: Widening) -> Self { - if let Some(join) = crate::transform::Join::of(sources, capacity, widening) { return Self::joined(join); } - let Some(joined) = Self::joined_kind(sources, widening) else { return Self { widening, ..Self::items(capacity) } }; - Self { data: Storage::with_capacity(joined.dtype, capacity).fixed(joined.fixed.is_some()), dtype: joined.dtype, open: false, weak: joined.weak, - widening, join: None } - } - /// The kind of the items that a buffer joining `sources` under `widening` holds, or `None` where no source holds any. An empty compact source holds no items that could change it. - pub(crate) fn joined_kind(sources: &[&Value], widening: Widening) -> Option { - let mut active = sources.iter().copied().filter(|v| !v.is_empty() || v.dtype().is_none()); - let first = active.next()?; - Some(crate::plan::Kind::join([first].into_iter().chain(active).map(crate::plan::Kind::value), widening).unwrap_or_default()) - } - pub(crate) fn len(&self) -> usize { match &self.join { Some(join) => join.len(), None => self.data.len() } } - /// An empty buffer for an operation's items, added one at a time. It takes the dtype of its first items. Each number keeps its exactness. - pub(crate) fn items(capacity: usize) -> Self { - Self { data: Storage::Compact(Buffer::Integers(IntBuf::I64(Vec::with_capacity(capacity)))), dtype: Some(DType::Integer(Width::I64)), open: true, - weak: false, widening: Widening::Items, join: None } - } - /// Whether a copy from `source` goes through a join: the buffer has one, or `source` starts one (`transform::Join::enter`). - fn leveled(&mut self, source: &Value) -> bool { crate::transform::Join::enter(self, source); self.join.is_some() } - /// Runs the copy `op` on each part of the join (`transform::Join::each`). - fn each(&mut self, source: &Value, replaces: bool, op: impl Fn(&mut Self, &Value)) { - self.join.as_mut().expect("a joined buffer").each(source, replaces, |g, s, _| op(g, s)) - } - /// A buffer that copies through `join`. - pub(crate) fn joined(join: crate::transform::Join) -> Self { Self { join: Some(Box::new(join)), ..Self::items(0) } } - /// The join that this buffer copies through, if it has one. - pub(crate) fn join(&self) -> Option<&crate::transform::Join> { self.join.as_deref() } - /// A buffer of `len` zeros that widens as this one does, as the tangents of the items copied before a join starts. - pub(crate) fn zeros(&self, len: usize) -> Self { - let mut zeros = Self { widening: self.widening, ..Self::items(len) }; - zeros.fill(&Value::integer(0), len); - zeros - } - /// Appends `item`. - pub(crate) fn add(&mut self, item: Value) { - if self.leveled(&item) { return self.join.as_mut().expect("a joined buffer").add(item); } - // A record has no items to read, so it goes into mixed storage whole (`linear`). - if item.carries_here() || !self.compact_fill(&item, 1) { self.mixed().push(item) } - } - /// Makes the buffer ready for items of `dtype`, which only weak numbers gave where `weak` holds. An open buffer takes the dtype. A compact buffer widens where its `widening` allows, narrows where only weak numbers gave its dtype and a strong item arrives (`plan::Kind::join`), and otherwise becomes mixed. A mixed buffer stays mixed. - fn follow(&mut self, source: &Value) { - let source = crate::plan::Kind::value(source); - if self.open { - self.open = false; - if source.dtype != self.data.dtype() { self.data = Storage::with_capacity(source.dtype, self.data.capacity()); } - (self.dtype, self.weak) = (source.dtype, source.weak); - self.data.set_fixed(source.fixed.is_some()); - return; - } - if self.dtype == source.dtype && self.data.fixed_width() == source.fixed && self.weak == source.weak { return; } - if self.dtype.is_none() { return; } - let current = crate::plan::Kind::stored(self.dtype, self.data.fixed_width(), self.weak); - let Ok(joined) = crate::plan::Kind::join([current, source], self.widening) else { self.mixed(); return; }; - let Some(dtype) = joined.dtype else { self.mixed(); return; }; - if self.dtype != Some(dtype) { - let capacity = self.data.capacity(); - let old = std::mem::replace(&mut self.data, Storage::with_capacity(Some(dtype), capacity)); - copy_into!(&mut self.data, old.items(), |d, s, f| d.extend(s.iter().map(|&x| f(x)))); - } - (self.dtype, self.weak) = (Some(dtype), joined.weak); - self.data.set_fixed(joined.fixed.is_some()); - } - /// The buffer as values, for an item that no compact dtype holds. - pub(crate) fn mixed(&mut self) -> &mut Vec { - self.open = false; - self.dtype = None; - if !matches!(self.data, Storage::Mixed(..)) { - let old = std::mem::replace(&mut self.data, Storage::mixed(Vec::new())); - let mut items = Vec::with_capacity(old.capacity()); - items.extend((0..old.len()).map(|i| match old.item(i) { Value::Number(n) => Value::Number(n.marked(self.weak)), v => v })); - self.data = Storage::mixed(items); - } - let Storage::Mixed(d, _) = &mut self.data else { unreachable!() }; - d - } - /// Appends `n` copies of `item` in the buffer's compact dtype, and gives whether it could. - fn compact_fill(&mut self, item: &Value, n: usize) -> bool { - if item_dtype(item).is_none() { self.mixed(); return false; } - self.follow(item); - copy_into!(&mut self.data, item.as_items(), |d, s, f| d.extend(std::iter::repeat_n(f(s[0]), n))) - } - /// Copies the items of `source` in `range`. - pub(crate) fn extend(&mut self, source: &Value, range: std::ops::Range) { - if range.is_empty() { return; } - if self.leveled(source) { return self.each(source, false, |g, s| g.extend(s, range.clone())); } - self.follow(source); - if self.data.copy_slice(source.as_items(), range.clone()) - || copy_into!(&mut self.data, source.as_items(), |d, s, f| d.extend(s[range.clone()].iter().map(|&x| f(x)))) { return; } - self.mixed().extend(source.items(range)) + fn each(self, mut read: impl FnMut(&Number) -> Result) -> Result, ErrorKind> { + (0..self.len()).map(|i| match self.item(i) { Value::Number(n) => read(&n), _ => Err(Kind::Domain.into()) }).collect() } - /// Copies item `i` of `source`, or its prototype where `i` is `Steps::FILL`, as `at` does. - pub(crate) fn push(&mut self, source: &Value, i: usize) { if i == Steps::FILL { self.add(source.prototype()) } else { self.extend(source, i..i + 1) } } - /// Appends the items of `source`, as joining it does. An empty compact source can't change the buffer's dtype, and an empty mixed one makes the buffer mixed. - pub(crate) fn append(&mut self, source: &Value) { - if self.leveled(source) { return self.each(source, false, |g, s| g.append(s)); } - if !source.is_empty() { return self.extend(source, 0..source.len()); } - if source.dtype().is_none() { self.mixed(); } - } - /// Appends each item of the vector `source` as many times as its count. `counts` holds one count for every item or one for each item. The counts are nonnegative and sum to `total`. - pub(crate) fn replicate(&mut self, source: &Value, counts: &[i64], total: usize) { - if self.leveled(source) { return self.each(source, false, |g, s| g.replicate(s, counts, total)); } - self.follow(source); - if copy_into!(&mut self.data, source.as_items(), |d, s, f| rustymath::gather::repeat(d, s, counts, total, f)) { return; } - let d = self.mixed(); - for (i, x) in source.elements().enumerate() { d.extend(std::iter::repeat_n(x, counts[if counts.len() == 1 { 0 } else { i }] as usize)) } - } - /// Copies the items of the vector `source` whose count in the Boolean `mask` is 1. `total` counts are 1. - pub(crate) fn compress(&mut self, source: &Value, mask: &[M], total: usize) { - if self.leveled(source) { return self.each(source, false, |g, s| g.compress(s, mask, total)); } - self.follow(source); - if copy_into!(&mut self.data, source.as_items(), |d, s, f| rustymath::gather::compress(d, mask, total, |j| f(s[j]))) { return; } - self.mixed().extend(source.elements().zip(mask).filter(|&(_, &n)| n.key() == 1).map(|(x, _)| x)) - } - /// Copies the cells of `source` at `rows`, each `width` items long. Cells of one item gather in one typed loop. Wider cells, and every cell of a pending source, are read by `walk`, whose fixed-width loops copy a short cell as one unit where a copy of each row's slice would cost a call for each row. - pub(crate) fn rows>(&mut self, source: &Value, rows: &[R], width: usize) { - if rows.is_empty() || width == 0 { return; } - if self.leveled(source) { return self.each(source, false, |g, s| g.rows(s, rows, width)); } - if width > 1 || source.pending().is_some() { - return self.walk(source, 0, &[Steps::table(rows.iter().map(|&r| r.as_() * width).collect()), Steps::along(width, 1)], None); - } - self.follow(source); - if copy_into!(&mut self.data, source.as_items(), |d, s, f| d.extend(rows.iter().map(|&r| f(s[r.as_()])))) { return; } - let d = self.mixed(); - for &r in rows { d.extend(source.items(r.as_()..r.as_() + 1)) } - } - /// Copies the items of `source` at `offsets`. A `Steps::FILL` offset gives `fill`, or the prototype of `source` where there's none, as `walk` does. - pub(crate) fn at(&mut self, source: &Value, offsets: &[usize], fill: Option<&Value>) { - if offsets.contains(&Steps::FILL) { self.walk(source, 0, &[Steps::table(offsets.to_vec())], fill) } else { self.rows(source, offsets, 1) } - } - /// Appends `n` copies of `item`. - pub(crate) fn fill(&mut self, item: &Value, n: usize) { - if n == 0 { return; } - if self.leveled(item) { return self.each(item, false, |g, s| g.fill(s, n)); } - if !self.compact_fill(item, n) { self.mixed().extend(std::iter::repeat_n(item.clone(), n)) } - } - /// Replaces each item that `writes` reaches with the matching item of `source`, or with its only item when it has one. A later write wins, and a write at a fill writes nothing. A walk writes without listing its positions, through rustymath's `gather::scatter_walk`, which copies cells of adjacent items as slices and cells a stride apart in blocks. - pub(crate) fn scatter(&mut self, writes: Writes<'_>, source: &Value) { - if writes.is_empty() { return; } - if self.leveled(source) { return self.each(source, true, |g, s| g.scatter(writes, s)); } - let single = source.len() == 1; - let pick = |k: usize| if single { 0 } else { k }; - self.follow(source); - // Items of the buffer's own type move without a conversion pass (`gather::Same`). - if copy_into!(&mut self.data, source.as_items(), |d, s| writes.write(d, s, rustymath::gather::Same)) { return; } - if copy_into!(&mut self.data, source.as_items(), |d, s, f| writes.write(d, s, f)) { return; } - let d = self.mixed(); - // A weak number written into mixed storage becomes strong, as an array's items are. - writes.runs_while(&mut |k, run| { - for (j, &o) in run.iter().enumerate() { if o != Steps::FILL { d[o] = source.at(pick(k + j)).strong() } } - true - }); - } - /// Appends `outer` rows, each holding `lens[0]` items of `sources[0]`, then `lens[1]` items of `sources[1]`, as two arrays joined along an axis lay out their lanes. Where lanes are short, compact sources join through rustymath's `gather::interleave`, which writes each row whole, straight into the buffer (`interleaved`). Long lanes, joined duals and storage that isn't compact copy lane by lane. - pub(crate) fn lanes(&mut self, sources: [&Value; 2], lens: [usize; 2], outer: usize) { - // A lane of 64 items or more costs more to copy than the call that copies it (about 13 ns), so long lanes copy one at a time, converting as they copy. - if lens[0] + lens[1] < 128 && outer > 1 && self.interleaved(sources, lens, outer) { return; } - for i in 0..outer { for (source, len) in sources.into_iter().zip(lens) { self.extend(source, i * len..(i + 1) * len) } } + pub(crate) fn integers(self) -> Result, ErrorKind> { + if let Some(ints) = self.ints() { return Ok(ints.widened()); } + self.each(Number::integer).map(Cow::Owned) } - /// Appends the rows that `lanes` describes through `gather::interleave`, and gives whether it could: the buffer has no join, both sources are compact, and the buffer stays compact once both sources' dtypes join it. A source of another dtype converts first, in one pass. - fn interleaved(&mut self, sources: [&Value; 2], lens: [usize; 2], outer: usize) -> bool { - let [Some(a), Some(b)] = sources.map(|s| s.view()) else { return false }; - if self.join.is_some() { return false; } - for (source, len) in sources.into_iter().zip(lens) { if len > 0 { self.follow(source) } } - let Some(dtype) = self.data.dtype() else { return false }; - let mut converted = [None, None]; - for ((v, len), c) in [a, b].into_iter().zip(lens).zip(&mut converted) { - if len == 0 || v.dtype() == dtype { continue; } - let mut s = Storage::with_capacity(Some(dtype), v.len()); - if !copy_into!(&mut s, Items::Compact(v), |d, s, f| rustymath::gather::Convert::slice(&f, d, s)) { return false; } - *c = Some(s); - } - let fixed = self.data.fixed_width().is_some(); - let buffer = std::mem::replace(&mut self.data, Storage::Compact(Buffer::Booleans(Vec::new()))).into_buffer().unwrap(); - rustymath::with_dtype!(dtype, T => { - let [a, b] = [(a, &converted[0]), (b, &converted[1])].map(|(v, c)| match c.as_ref().map_or(Items::Compact(v), Storage::items) { - Items::Compact(v) => T::of(v).unwrap_or(&[]), - _ => unreachable!("compact storage"), - }); - let mut out = T::vec(buffer).expect("the buffer has its dtype"); - rustymath::gather::interleave(&mut out, [a, b], lens, outer); - self.data = Storage::Compact(T::buffer(out)).fixed(fixed); - }); - true + pub(crate) fn exact_integers(self) -> Option> { + self.dtype()?.is_integral().then(|| self.integers().ok()).flatten() } - /// Copies the items of `source` that `steps` reach from `base`, through rustymath's `gather::walk`: one `Steps` for each axis, with the last varying fastest. An offset that reaches no item gives `fill`, or the prototype of `source` where there's none, and through a join only the values read `fill` (`transform::Part`). Compact storage is copied in one typed loop. A pending copy that the walk reads fewer items of is read through its map, without building it (`fusion::walk_through`). - pub(crate) fn walk(&mut self, source: &Value, base: usize, steps: &[Steps], fill: Option<&Value>) { - if self.leveled(source) { - return self.join.as_mut().expect("a joined buffer").each(source, false, |g, s, part| g.walk(s, base, steps, part.fill(fill))); - } - if let Some((inner, base, steps, fill)) = crate::fusion::walk_through(source, base, steps, fill) { - return self.walk(&inner, base, &steps, fill.as_ref()); - } - self.follow(source); - let native = fill.and_then(|fill| source.native_fill(fill)); - if copy_into!(&mut self.data, source.as_items(), |d, s| rustymath::gather::walk( - d, s, base, steps, rustymath::buffer::Fill::item(native.as_ref()), rustymath::gather::Same - )) { return; } - if copy_into!(&mut self.data, source.as_items(), |d, s, f| rustymath::gather::walk( - d, s, base, steps, rustymath::buffer::Fill::item(native.as_ref()), f - )) { return; } - let chosen = fill.map_or_else(std::cell::OnceCell::new, |_| std::cell::OnceCell::from(source.fill_item(fill))); - walk_values(self.mixed(), source, &chosen, base, steps); - } - /// The gathered array, laid out by `layout`. Mixed items that one compact dtype holds, under the rule for items gathered one at a time (`Widening::Items`), are held compact, as the same items written in brackets are. An empty array takes `prototype`, and the storage `prototype`'s kind gives it. A buffer with a join gives what the join finishes (`transform::Join::finish`). - pub(crate) fn finish(self, layout: Layout, prototype: impl Prototype) -> Result { - if let Some(join) = self.join { return join.finish(layout, prototype); } - let shape = crate::Shape::from(layout.shape()); - let value = match self.data { - data if data.len() == 0 => Value::empty(shape, prototype.value())?, - Storage::Mixed(items, _) => Value::from_storage(shape, Storage::compact(items, Widening::Items))?, data => Value::from_storage(shape, data)? - }; - value.with_layout(layout) + pub(crate) fn nonnegative_integers>(self) -> Result, ErrorKind> { self.each(Number::nonnegative_integer) } + pub(crate) fn positions(self) -> Result>, ErrorKind> { + self.each(|n| match n.as_float() { Some(x) => rustymath::structure::float_position(x).map_err(|_| Kind::Domain.into()), None => n.integer().map(Some) }) } } -/// `rustymath::gather::walk` for mixed storage, which reads each item as a value. `fill` gives the item at a missing offset: a walk with no chosen fill builds `source`'s prototype at the first missing offset only. A prototype is the recursive fill of the first item, as large as that item. -fn walk_values(d: &mut Vec, source: &Value, fill: &std::cell::OnceCell, base: usize, steps: &[Steps]) { - let Some((axis, rest)) = steps.split_first() else { return d.push(source.at(base)) }; - let width = Steps::count(rest); - axis.for_each(|_, o| match o { - Some(o) => walk_values(d, source, fill, base + o, rest), None => d.extend(std::iter::repeat_n(fill.get_or_init(|| source.prototype()).clone(), width)) - }); +pub(super) fn walk_values(out: &mut Vec, source: &Value, fill: &Value, base: usize, steps: &[rustymath::gather::Steps]) { + let Some((axis, rest)) = steps.split_first() else { + out.push(if base == rustymath::gather::Steps::FILL { fill.clone() } else { source.at(base) }); return; + }; + for i in 0..axis.len() { walk_values(out, source, fill, axis.at(base, i), rest); } } -/// The bitwise or of `counts` and their wrapping sum, in one pass. The or is negative when any count is, and at most 1 when all the counts are 0 or 1. -pub(crate) fn or_and_sum(counts: &[T]) -> (i64, i64) { - let (mut any, mut sum) = (0i64, 0i64); - for &n in counts { (any, sum) = (any | n.to_i64(), sum.wrapping_add(n.to_i64())); } - (any, sum) -} diff --git a/src/array/text.rs b/src/array/text.rs index 4dbbd6d8..10cb3f8c 100644 --- a/src/array/text.rs +++ b/src/array/text.rs @@ -1,16 +1,15 @@ //! Text forms of values: `⍕` formatting, and source text that reads back. use super::*; -use crate::transform::Plain; use rustymath::buffer::View; /// Text as a double-quoted string literal. pub(crate) fn quoted(text: &str) -> String { format!("\"{}\"", text.replace('"', "\"\"")) } +/// The positions of `len` lines or characters cut to `limit`, keeping up to `edges` of them at the end. fn excerpt(len: usize, limit: usize, edges: usize) -> impl Iterator> { let tail = edges.min(limit / 2); - let (head, end) = if len > limit { (limit - tail, len - tail) } else { (len, len) }; - (0..head).map(Some).chain((head < end).then_some(None)).chain((end..len).map(Some)) + crate::display::ends(len, limit - tail, tail) } /// Whether `test` holds for a character of source text outside brackets, parentheses, braces and quotes. @@ -44,7 +43,7 @@ impl Value { /// `⍕`'s text for this value. Numbers show their kind's markers where `el.marks` is set, as monadic `⍕` shows them. pub(crate) fn formatted(&self, el: Elide) -> Result { if self.is_unit() { - if let Value::Function(f) = self.at(0) { let text = f.to_string(); return Self::characters([text.chars().count()], text.chars().collect()); } + if let Value::Function(f) = self.at(0) { let text = format!("⟨{}⟩", f.bpl()); return Self::characters([text.chars().count()], text.chars().collect()); } } // A keyed array formats as its display text, so its keys stay visible. let keyed = self.has_keys(); @@ -137,31 +136,20 @@ impl Value { /// Source text that reads back as the value. Strings are quoted, and arrays use bracket notation. pub fn literal(&self) -> String { self.source(Elide::NONE) } - /// Whether the value is a function or an operator, or holds one at any depth, including as an empty array's prototype. - pub(crate) fn holds_function(&self) -> bool { - match self { - Self::Function(_) | Self::Operator(_) => true, - Self::Array(_) => match self.as_items() { - Items::Values(items) => items.iter().any(Self::holds_function) || (items.is_empty() && self.prototype().holds_function()), _ => false - }, - _ => false, - } - } - /// Source text for the value, with large arrays elided as `el` says. Elided text doesn't read back. pub(crate) fn source(&self, el: Elide) -> String { - match self.displayed() { - Plain::Number(n) => el.number(n), - Plain::Character(c) if c.is_control() => format!("•ucs {}", c as u32), - Plain::Character(c) => format!("'{c}'"), - Plain::Function(f) => { + match self { + Self::Number(n) => el.number(n), + Self::Character(c) if c.is_control() => format!("•ucs {}", *c as u32), + Self::Character(c) => format!("'{c}'"), + Self::Function(f) => { let text = f.text(el, &mut 1000); // A native that a factory made, such as a generator's `roll` or a `•canvas` drawing function, has no spelling that reads back. if f.system_function().is_some_and(|s| s.entry.is_none()) { return format!("⟨{text}⟩"); } if text.contains(' ') { format!("({text})") } else { text } } - Plain::Operator(op) => { let text = op.text(el); if text.contains(' ') { format!("({text})") } else { text } } - Plain::Array(_) => { + Self::Operator(op) => { let text = op.text(el); if text.contains(' ') { format!("({text})") } else { text } } + Self::Array(_) => { if self.shows_names(el) { return self.named_literal(el); } let edges = if self.shape().len() == 1 { el.last(self.shape()) } else { el.edges(self.shape()) }; if self.string_literal(el.display).is_some() { return quoted(&self.elided_text(el)); } @@ -178,6 +166,7 @@ impl Value { _ => self.block_literal(Self::item, el, edges) } } + Self::Pending(_) | Self::Carried { .. } => unreachable!("source formatting requires observed values"), } } @@ -206,15 +195,14 @@ impl Value { /// An array of rank 2 or more in array notation, one major cell at a time, with `item` writing each item. `edges` elides positions on every axis of the whole array. fn block_literal(&self, item: fn(&Self, Elide) -> String, el: Elide, edges: Option) -> String { - let Ok(cells) = self.cells(self.shape().len() - 1) else { return crate::display::plain(self, el) }; - let rows: Result, ErrorKind> = - positions(cells.len(), edges).map(|i| i.map_or(Ok("…".into()), |i| Ok(cells.get(i)?.row(item, el, edges)))).collect(); - match rows { - // One major cell needs a trailing `⋄`, or it reads back as that cell alone. - Ok(rows) if rows.len() == 1 => format!("[{}⋄]", rows[0]), - Ok(rows) => format!("[{}]", rows.join("⋄")), - Err(_) => crate::display::plain(self, el), - } + let Self::Array(array) = self else { return crate::display::plain(self, el) }; + let layout = self.layout().axes(1..self.rank()); + let size = layout.count(); + let rows: Vec<_> = positions(self.shape()[0], edges).map(|i| i.map_or_else(|| "…".into(), |i| { + Self::stored_part(array, i * size..(i + 1) * size, layout.clone()).expect("a major cell fits its array").row(item, el, edges) + })).collect(); + // One major cell needs a trailing `⋄`, or it reads back as that cell alone. + if rows.len() == 1 { format!("[{}⋄]", rows[0]) } else { format!("[{}]", rows.join("⋄")) } } /// One numeric marker for a nonempty array whose items share a kind. Float widths obey `el.plain`. @@ -225,8 +213,7 @@ impl Value { } /// The dtype and fixedness shared by the items, independent of compact or mixed storage. A carrier shows its template's kind. pub(crate) fn shown_kind(&self) -> Option<(rustymath::buffer::DType, bool)> { - let held = self.template(); - match held.as_items() { + match self.as_items() { Items::Values(items) => super::storage::items_kind(items, Widening::Items), items => items.dtype().map(|dtype| (dtype, items.fixed_width().is_some())), } @@ -257,7 +244,10 @@ impl Value { fn named_literal(&self, el: Elide) -> String { let shape: Vec<_> = self.axis_names().iter().zip(self.shape()) .map(|(n, len)| el.name(n).map_or_else(|| len.to_string(), |n| format!("{}:{len}", quoted(&n)))).collect(); - format!("[{}]⍴{}", shape.join(" "), self.clone().with_axis_names(vec![]).unwrap().source(el)) + let Self::Array(array) = self else { unreachable!("only arrays have axis names") }; + let layout = self.layout().clone().with_names(vec![]).expect("removing axis names"); + let unnamed = Self::stored_part(array, 0..self.len(), layout).expect("the entire stored array"); + format!("[{}]⍴{}", shape.join(" "), unnamed.source(el)) } /// An array of rank 2 or more with keys: a key list for each axis, applied to the array without keys. A position stands for each missing key. @@ -268,7 +258,10 @@ impl Value { .map(|i| i.map_or_else(|| "…".into(), |i| self.shown_key(axis, i, el).map_or_else(|| i.to_string(), |k| quoted(&k)))).collect(); match keys.len() { 0 => "⍬".into(), 1 => format!("[{}]", keys[0]), _ => keys.join(" ") } }).collect(); - format!("[{}]:{}", lists.join(";"), self.unkeyed().source(el)) + let Self::Array(array) = self else { unreachable!("only arrays have axis keys") }; + let layout = self.layout().clone().with_keys(vec![]).expect("removing axis keys"); + let unkeyed = Self::stored_part(array, 0..self.len(), layout).expect("the entire stored array"); + format!("[{}]:{}", lists.join(";"), unkeyed.source(el)) } /// Whether plain display shows the vector as a strand: a vector of two or more numbers, characters or strings. Exact integers show in brackets instead. @@ -276,8 +269,8 @@ impl Value { matches!(self, Self::Array(_)) && self.shape().len() == 1 && self.len() >= 2 && !self.has_keys() && !self.shows_names(el) && self.string_literal(el.display).is_none() && !matches!(self.shown_kind(), Some((rustymath::buffer::DType::Integer(_), _))) && match self.as_items() { - Items::Values(items) => items.iter() - .all(|e| matches!(e, Self::Number(_)) || matches!(e, Self::Character(c) if !c.is_control()) || e.string_literal(el.display).is_some()), + items if items.nested() => items.values() + .all(|e| matches!(*e, Self::Number(_) | Self::Pending(_)) || matches!(*e, Self::Character(c) if !c.is_control()) || e.string_literal(el.display).is_some()), Items::Compact(View::Characters(cs)) => cs.iter().all(|c| !c.is_control()), _ => true } } diff --git a/src/batch.rs b/src/batch.rs index 1f646d01..74afe4d0 100644 --- a/src/batch.rs +++ b/src/batch.rs @@ -1,685 +1,127 @@ -//! Batched maps. A batched run calls the operand of an Each, a Rank or an outer product once, on carriers that hold the map's whole arguments (`Batch`), instead of once for each cell. Each, Rank and an outer product lay their arguments over the map's frame through the agreement that pairs their cells (`Arguments::laid`). A map starts a run through `tried` while the `batch` pref is on. `tried` begins the run's level (`transform::Kind::Batched`) and ends it after the call. A map inside a run widens that run instead, so its operand runs once on the cells of both maps (`Entry::Widen`). Routing hands each call with a batch at that level to `call`, for a primitive or an operation that primitives name, such as a fold by a primitive, or to `route`, for an operation by a dfn. `call` runs the call on the whole arguments at the cells' ranks, by Rank's routes for whole arrays (`eval::whole_cells` and `eval::folded_cells`). A call with no such rule stops the run (`transform::Stop`). The map then calls the operand once for each cell, as it does without batching. A call that would repeat constants into too many items stops the run with a chunk size instead, and the operand runs once on each chunk of cells (`chunked`). Batches hold only compact items of one kind, whose cells of rank 0 are atoms. -use crate::array::{Footprint, Seen}; -use crate::session; +//! Batched functions run on framed whole arguments through the normal dispatcher. use crate::{ - Described, Error, ErrorKind, Function, Span, Value, agreement::{Agree, Agreement, laid_cells, spread}, - array::{Gather, Layout, Promotion, Storage, Widening, item_dtype}, primitive::{FoldKind, Form, Operation, Primitive}, - transform::{Carrier, Cause, Ended, Kept, Op, Pairing, Stop} + array::{self, Carried, Layout}, error::{Error, Result}, eval::Function, + primitive::{Exit, Operation, Order, ReadOp, ViewAxis}, program::{Desc, Origin, Outputs}, + transform::{at_level, carried, Level, LevelKind, Scope}, Value, }; -use rustymath::{agreement::Mapping, buffer::DType, gather::{Steps, Writes}, structure::IndexPart}; -use std::borrow::Cow; -use std::sync::{Arc, OnceLock}; -/// The whole argument of a batched map at level `level`. Each cell of the whole along its first `frame` axes, the map's frame, is the argument that one call of the operand would take. A reader outside a batched call sees one cell. `first` caches the whole's first cell, for queries that read no items, and `items` caches the items as batches of atoms, for code with no rule for batches. -pub struct Batch { pub(crate) level: u64, pub(crate) frame: usize, whole: Value, first: OnceLock, items: OnceLock } - -impl Batch { - /// The batch of `whole` at level `level`, with `frame` frame axes. The whole carries only older levels' data. - fn new(level: u64, whole: Value, frame: usize) -> Self { - assert!(whole.older_than(level), "a batch's whole carries only older levels' data"); - Self { level, frame, whole, first: OnceLock::new(), items: OnceLock::new() } - } - /// The rank of one cell. - pub(crate) fn rank(&self) -> usize { self.whole.shape().len() - self.frame } - /// The number of cells: the product of the frame's lengths. - pub(crate) fn cells(&self) -> usize { crate::array::count(&self.whole.shape()[..self.frame]) } - /// The array that holds every cell, with the frame's axes first. - pub(crate) fn whole(&self) -> &Value { &self.whole } - /// The frame's layout: the whole's first `frame` axes, with their keys and names. - fn frame_layout(&self) -> Layout { self.whole.layout().axes(0..self.frame) } - /// One `Steps` for each frame axis, reading item `start` of each cell of the whole, cell by cell. - fn frame_steps(&self, start: usize) -> Vec { - let (frame, cell) = self.whole.shape().split_at(self.frame); - Steps::row_major(frame, crate::array::count(cell), start).collect() - } - /// The value that cell `c` of the map holds. - fn cell(&self, c: usize) -> Result { self.whole.cells(self.rank()).and_then(|cells| cells.get(c)) } - /// The first cell, which answers the queries that read no items (`Carrier::template`): layout, prototype, fill, dtype and exactness. A batched run never starts on an empty frame, so the cell exists. - pub(crate) fn template(&self) -> &Value { self.first.get_or_init(|| self.cell(0).expect("a batched run's frame has a cell")) } - /// Whether the cells hold a function as a direct item. The whole argument holds every cell's items, so asking it computes no cell, where `template` would. - pub(crate) fn holds_functions(&self) -> bool { self.whole.holds_functions() } - /// Item `i` of every cell, as a batch of atoms. - pub(crate) fn at(&self, i: usize) -> Value { - let items = - crate::primitive::copied(&self.whole, self.frame_layout(), &Mapping::from(self.frame_steps(i))).expect("a frame's positions lie inside the whole"); - batch(self.level, items, self.frame) - } - /// The items, for code with no rule for batches (`ArrayData::storage`). Item `i` is item `i` of every cell, as a batch of atoms. A kernel that reads a number from such an item gets an error (`Value::computed`). An observing read of it stops the run (`Value::observed`). Either way the map falls back to one call for each cell. Building the items records an `itemwise` row in `•explain`. - #[cold] - #[inline(never)] - pub(crate) fn storage(&self) -> &Storage { - crate::transform::Carrier::built(&self.items, rustymath::events::Route::BatchedItems, self.template().len(), |i| self.at(i)) - } - /// This batch with each cell laid out by `layout`, as a view of the whole with no items copied. `layout` has as many items as a cell. - pub(crate) fn view(&self, layout: Layout) -> Value { - batch(self.level, self.whole.relaid(self.frame_layout().concat(&layout)).expect("a view has as many items as its base"), self.frame) - } - /// Each cell's items read through `map` and laid out by `layout`, as `primitive::copied` reads a value's items. The frame's axes are read in front of the map's tables. - pub(crate) fn copied(&self, layout: Layout, map: &Mapping, fill: Option<&Value>) -> Result { - let steps: Vec = self.frame_steps(0).into_iter().chain(map.tables(layout.shape())).collect(); - Ok(batch(self.level, crate::primitive::copied_fill(&self.whole, self.frame_layout().concat(&layout), &map.with_steps(0, steps), fill)?, self.frame)) - } - /// Whether code with no rule for batches has read the items one at a time (`Batch::storage`). - pub(crate) fn items_built(&self) -> bool { self.items.get().is_some() } -} - -impl Footprint for Batch { - /// The bytes of the whole, the first cell and the items built from them. - fn footprint_in(&self, seen: &mut Seen) -> usize { self.whole.footprint_in(seen) + self.first.footprint_in(seen) + self.items.footprint_in(seen) } -} - -impl std::fmt::Debug for Batch { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Batch").field("level", &self.level).field("shape", &self.whole.shape()).field("frame", &self.frame).finish() - } -} - -/// The batch of `whole` at batched run `level`, whose first `frame` axes are the frame: a carried atom where its cells are atoms, and a carried array of one cell's layout otherwise. -pub(crate) fn batch(level: u64, whole: Value, frame: usize) -> Value { - let cell = whole.layout().axes(frame..whole.shape().len()); - let carrier = Carrier::Batch(Arc::new(Batch::new(level, whole, frame))); - if cell.shape().is_empty() { Value::Carried(carrier) } else { Value::from_carrier(cell, carrier) } -} - -/// An argument of a call that batched run `level` handles: a batch at that level, or a constant, which applies to every cell. A value that an older level carries is a constant to this one. -#[derive(Clone, Copy)] -enum Arg<'a> { Batched(&'a Batch), Constant(&'a Value) } - -fn arg(value: &Value, level: u64) -> Arg<'_> { - match value.carrier_at(level).and_then(Carrier::batch) { Some(b) => Arg::Batched(b), None => Arg::Constant(value) } -} - -impl<'a> Arg<'a> { - /// The array that holds every cell's argument: a batch's whole argument, or a constant, which is every cell's. - fn all(self) -> &'a Value { match self { Arg::Batched(b) => &b.whole, Arg::Constant(c) => c } } - /// The rank of each cell's argument. - fn rank(self) -> usize { match self { Arg::Batched(b) => b.rank(), Arg::Constant(c) => c.shape().len() } } - /// The first cell's argument. - fn first(self) -> &'a Value { match self { Arg::Batched(b) => b.template(), Arg::Constant(c) => c } } -} - -/// Cell `c` of `value` in the running batched run. A batch at the run's level gives its cell `c`. Any other value is the same for every cell and gives itself, as it does outside a batched run. -pub(crate) fn cell(value: &Value, c: usize) -> Result { - match session().batch_level().map(|level| arg(value, level)) { Some(Arg::Batched(b)) => b.cell(c).described("invalid batch"), _ => Ok(value.clone()) } -} - -/// The error that stops a batched map's run with `stop`, at `span`. -fn stop(stop: Stop) -> Option> { Some(Err(stop.into())) } - -/// The stop of batched run `level` at a call of `operation` that no rule for batches covers. A primitive's stop keeps its glyph for `•explain` (`Stop::refusing`). -fn refusal(level: u64, operation: Operation) -> Stop { - match operation { Operation::Primitive(p) => Stop::refusing(level, p), _ => Stop::new(level, Cause::Rule) } -} - -/// What a dfn operand's `Definition` remembers of a stopped batched run (`transform::Cause::kept`): `NOTHING`, `ALWAYS`, or a `signature` of the arguments. `ALWAYS` and a signature also hold the stop's cause (`holding`). -const NOTHING: u64 = 0; -/// A dfn operand's `Definition` remembers a stop for every later call. -const ALWAYS: u64 = 1; -/// The bits of a memory that hold the cause of the stop it remembers. -const CAUSE: u64 = 7 << 2; -/// `memory`, `ALWAYS` or a signature, holding `cause` in the bits of `CAUSE`. -fn holding(memory: u64, cause: Cause) -> u64 { memory | (cause as u64) << 2 } - -/// What `•explain` reports in `fallbacks` where a map skips its batched call, because its dfn operand remembers a stop: "an earlier stop" and the stop's cause. -struct Earlier(Cause); - -impl Earlier { - /// The reason for skipping because of the stop that `memory` remembers. - fn of(memory: u64) -> Self { Earlier(Cause::from_bits((memory & CAUSE) >> 2)) } -} - -impl std::fmt::Display for Earlier { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "an earlier stop: {}", self.0.reason()) } -} - -/// A signature of the arguments of a map's batched try: for each argument, whether it's a batch, the kind of its items and the shape of one of its cells, and with `with_frame`, the shape of the map's frame, `frame`. Unlike `NOTHING` and `ALWAYS`, a signature sets bit 1. Bit 0 says whether the frame's shape is in it. The bits of `CAUSE` stay clear for the cause that a memory holds (`holding`). -fn signature(left: Option<&Side>, right: &Side, frame: &[usize], with_frame: bool) -> u64 { - use std::hash::{Hash, Hasher}; - let mut hasher = std::hash::DefaultHasher::new(); - for side in left.into_iter().chain([right]) { - let (batched, value, cell) = match side { Side::Batched(v) => (true, v, &v.shape()[frame.len()..]), Side::Constant(c) => (false, c, c.shape()) }; - (batched, value.template_dtype().map(DType::index), cell).hash(&mut hasher); - } - with_frame.then_some(frame).hash(&mut hasher); - hasher.finish() & !31 | 2 | u64::from(with_frame) -} - -/// `whole` as a result batch of level `level`, with `frame` frame axes, or the run's stop, `refused`, where a batch can't hold it. Batches hold only compact items of one kind, whose cells of rank 0 are atoms. Where `whole` has no axes after the frame, `atoms` says whether each cell's own result is an atom. -pub(crate) fn result(level: u64, whole: Value, frame: usize, atoms: impl FnOnce() -> bool, refused: Stop) -> Result { - if whole.template_dtype().is_none() || (whole.shape().len() == frame && !atoms()) { return Err(refused.into()); } - Ok(batch(level, whole, frame)) -} - -/// Each cell's own value, in the order of the batched map's frame `frame`, as one batch at level `level`, or `None` where the values don't stack into one compact array, as values of different shapes or kinds don't. -pub(crate) fn from_cells(level: u64, frame: &Layout, cells: Vec) -> Option { - let first = cells.first()?; - let (cell, prototype) = (first.layout().clone(), first.prototype()); - if cells.iter().any(|c| *c.layout() != cell) { return None; } - let collected = frame.collect(cells, prototype).ok()?; - let whole = if cell.shape().is_empty() { collected } else { collected.mix(Widening::Items, None).ok()? }; - whole.template_dtype()?; - Some(batch(level, whole, frame.shape().len())) -} - -/// `f` for each cell of batched run `level`, whose frame is `frame`, in cell order, with the cells' values joined into one batch (`from_cells`). Where the values don't stack, as values of different shapes or kinds don't, the run stops for a call with no rule for these arguments (`Cause::Rule`). `•explain` counts the cells as the `itemwise` route `batch cells`. -pub(crate) fn each_cell(level: u64, frame: &Layout, f: impl FnMut(usize) -> Result) -> Result { - let count = frame.count(); - crate::explain::itemwise(rustymath::events::Route::BatchCells, count); - let cells = (0..count).map(f).collect::, _>>()?; - from_cells(level, frame, cells).ok_or_else(|| Stop::new(level, Cause::Rule).into()) -} - -/// `value`, a result of batched run `level` over the cells that `frame` lays out, as one array with the frame's axes first: a batch's whole, or a constant, which is every cell's result, repeated over the frame. A result that holds batches as items, as a strand of batches does, joins into one batch where it can. Primitives handle each batch in such a value on its own, so only a run's results need the join. `None` for any other result, such as a batch over other cells, or a scalar that isn't an atom, because a batch's cells of rank 0 are atoms. -pub(crate) fn whole_over(value: Value, level: u64, frame: &Layout) -> Option { - let value = joined(&value).unwrap_or(value); - if let Some(b) = value.carrier_at(level).and_then(Carrier::batch).filter(|b| b.whole.layout().shape()[..b.frame] == *frame.shape()) { - return Some(b.whole.clone()); - } - if constant(&value, level) { spread(&value, frame).ok() } else { None } -} - -/// Whether `value`, a result of batched run `level`, is a constant: made before the run, of a dtype that a batch holds, and an atom or an array with axes. Every cell takes it as its result. -pub(crate) fn constant(value: &Value, level: u64) -> bool { - value.older_than(level) && value.template_dtype().is_some() && (value.is_atom() || !value.shape().is_empty()) -} - -/// The Booleans that a guard's predicate `value` gives the cells of batched run `level`, laid out as the run's frame, where `value` is a batch of the run or an array of its batches that joins into one. Each cell's predicate is one Boolean, as `Value::boolean` reads it. `None` where `value` is the same for every cell. -pub(crate) fn truths(value: &Value, level: u64) -> Option> { - let joined = joined(value); - let value = joined.as_ref().unwrap_or(value); - let b = value.carrier_at(level).and_then(Carrier::batch)?; - let frame = b.frame_layout(); - if b.whole.len() != b.cells() { return Some(Err(rustymath::error::Kind::Length.into())); } - if b.whole.dtype() == Some(DType::Boolean) { return Some(b.whole.relaid(frame)); } - Some(b.whole.elements().map(|e| e.boolean()).collect::, _>>().and_then(|bits| frame.booleans(bits))) -} - -/// `value` as a case of run `level` sees it (`transform::Levels::case`): a batch of the run read by `read`, which takes the batch's whole and its number of frame axes and gives the case's whole and its number of frame axes. An array that holds such batches reads each. Any other value is the same for every cell. -pub(crate) fn read_cells(value: &Value, level: u64, read: &impl Fn(&Value, usize) -> Result<(Value, usize), Error>) -> Result { - if let Some(b) = value.carrier_at(level).and_then(Carrier::batch) { let (whole, frame) = read(&b.whole, b.frame)?; return Ok(batch(level, whole, frame)); } - if value.carrier().is_none() && value.holds_values() && value.carried() { - let items = value.elements().map(|e| read_cells(&e, level, read)).collect::, _>>()?; - return value.layout().collect(items, value.prototype()).described("invalid array"); - } - Ok(value.clone()) -} - -/// `value` as code in batched run `level`, widened to `frame`, reads it: each batch of the run with fewer frame axes than `frame`, which a binding made before the widening holds, laid over `frame` (`laid_over`). An array that holds such batches lays each. -pub(crate) fn widened(value: &Value, level: u64, frame: &Layout) -> Result { - let axes = frame.shape().len(); - read_cells(value, level, &|whole, frame_axes| { - Ok(if frame_axes < axes { (laid_over(whole, frame_axes, frame).described("invalid batch")?, axes) } else { (whole.clone(), frame_axes) }) - }) -} - -/// The call of `op` as batched run `level` handles it (`transform::call`). -/// - `⌽` and `⊖` with no left argument or a scalar one, and `#` with the same counts for every cell, run along a cell axis as one call along that axis of the batch's whole, past its frame (`along`). Any other primitive along an axis stops the run, because no rule moves the axis past the frame. -/// - `⊢` and `⊣` give the argument they choose, a batch or a constant. -/// - A form defined by other primitives runs its own code, whose calls route again. -/// - `⌷` and bracket indexing whose selector lists hold batches run their own code, which reads each cell's positions on the batch's whole (`bracketed`). -/// - `¿` runs its own code, which draws from the generator's one stream: a roll once on the batch's whole, and a deal for each cell in cell order (`primitive::drawn_value`). -/// - `⍕` runs its own code, whose effect check stops the run, as it does for a batch inside a nested argument (`transform::formats`). -/// - Monadic `⍴` and `≢` give one constant, every cell's result, because a batch's cells share one shape. -/// - `↕` with one window specification for every cell runs its own code, whose copy reads each cell's windows (`Batch::copied`). A specification that differs from cell to cell stops the run, because each cell's windows could have their own shape. -/// - A fold or scan of each cell by a primitive runs by Rank's rule for folds (`eval::folded_cells`). A seed that is the same number in every cell seeds each lane in that rule. Any other seed, and a seed of a fold of atoms, leave the fold to its own loop over the batch's items. A fold of an atom with no seed is the atom. A fold along an axis that isn't one of the cell's stops the run, so that each cell's call gives its own result or error. A fold by `+`, `×`, `⌈` or `⌊` along several cell axes takes the same rule, as one fold of the whole along all of them, where `eval::folded_along` asks for it (`folded`). -/// - A fold or scan by an operand that isn't a primitive runs its own loop, which calls the operand once for each stage, on item `j` of every cell as one batch of atoms (`Batch::at`). A fold of an atom is the atom, and a fold along several axes, or along an axis that isn't one of the cell's, stops the run. -/// - A system function runs once for each cell, as it runs once for each item of an argument that holds several, and code in it that acts stops the run (`system`). A random draw runs its own code, which draws for each cell as `¿` does. -/// - A function with an attached rule runs its own code on a batch. Where the batch's whole carries a dual, the rule applies at that dual's level, as it would to each cell. -/// - A dyadic `f.g` by functions that aren't primitives runs its own code, whose calls of `f` and `g` route again. Its monadic form stops the run. -/// - A primitive paired by `Pairing::At`, such as accumulation, combines on the wholes, as a write into a batch does (`scattered`). -/// - Any other call, such as a product of primitives, or `⌹`, `+.×` or `-.×` on each matrix of stacks, runs on the whole arguments by Rank's routes (`whole_call`). A call that no route covers stops the run. -pub(crate) fn call(op: Op<'_>, left: Option<&Value>, right: &Value, level: u64, span: &Span) -> Option> { - let (op, pairing) = match op { - Op::Operation(op, pairing) => (op, pairing), - Op::System(function, f) => return system(function, f, left, right, level, span), - Op::Ruled(f, rule) => { - let tag = match arg(right, level) { Arg::Batched(b) => crate::derivative::tag(&b.whole), Arg::Constant(c) => crate::derivative::tag(c) }; - return tag.map(|tag| if left.is_none() { crate::derivative::attached(f, rule, right, tag, span) } else { Err(crate::derivative::two_arguments()) }); - } - Op::Fold(kind, axis) => { - let (None, Arg::Batched(b)) = (left, arg(right, level)) else { return stop(Stop::new(level, Cause::Rule)) }; - if b.rank() == 0 { return Some(Ok(right.clone())); } - return match kind.axis(axis, b.template()) { Ok(_) => None, Err(_) => stop(Stop::new(level, Cause::Rule)) }; - } - Op::Product => return if left.is_some() { None } else { stop(Stop::new(level, Cause::Rule)) }, - }; - let (x, y) = (left.map(|v| arg(v, level)), arg(right, level)); - match (op, pairing) { - (Operation::Primitive(p), Pairing::Axis(axis)) => { - if let Some(axis) = axis { return along(p, axis, x, y, level, span); } - if matches!(p.facts(left.is_some()).form, Form::Defined) || matches!(p, Primitive::Random | Primitive::Format) { return None; } - // `⌷` and bracket indexing with batches in their selector lists read them on the batch's whole, in their own code (`bracketed`). - if matches!(p, Primitive::Index | Primitive::Bracket) && left.is_some_and(|l| l.carrier().is_none() && l.carried()) { return None; } - if let Primitive::Identity(chooses_left) = p { return Some(Ok(if chooses_left { left.unwrap_or(right) } else { right }.clone())); } - if matches!(p, Primitive::Windows) && !matches!(x, Some(Arg::Batched(_))) { return None; } - if left.is_none() && let Arg::Batched(b) = y - && let Some(cell) = crate::eval::cell_constant(p, &b.whole.layout().axes(b.frame..b.whole.shape().len())) { return Some(Ok(cell)); } - whole_call(op, x, y, false, level, span) - } - (Operation::Fold(p, kind), Pairing::Axis(axis)) => { - let seed = match x { None => None, Some(Arg::Constant(s @ Value::Number(_))) => Some(s), Some(_) => return None }; - let Arg::Batched(b) = y else { return stop(Stop::new(level, Cause::Rule)) }; - if b.rank() == 0 { return seed.is_none().then(|| Ok(right.clone())); } - let along = axis.unwrap_or(kind.default_axis(b.rank())); - fold_cells(level, b, p, kind, &[along], seed, span) - } - (Operation::Primitive(p), Pairing::At(writes)) => { - let frame = frame_of(&[left?, right], level)?; - Some(combined(left?, right, p, level, &frame, &writes.listed(), 0, span)) - } - _ => whole_call(op, x, y, matches!(pairing, Pairing::Matrices), level, span).or_else(|| stop(refusal(level, op))), - } -} - -/// `p` along cell axis `axis` of batched run `level`, as one call along that axis of the batch's whole, past its frame. Only forms that act along one axis and treat every other axis alike take this route: `⌽` and `⊖` with no left argument or a scalar one, and `#` with the same counts for every cell. A larger left argument of `⌽` or `⊖` would pair with the frame's axes in the whole call. Any other call along an axis stops the run, because no rule moves the axis past the frame. -fn along(p: Primitive, axis: usize, x: Option>, y: Arg<'_>, level: u64, span: &Span) -> Option> { - let routed = match (p, x) { - (Primitive::Reverse(_), None) | (Primitive::Replicate, Some(Arg::Constant(_))) => true, - (Primitive::Reverse(_), Some(Arg::Constant(c))) => c.shape().is_empty(), _ => false +pub(crate) fn map(function: &Function, parent: &Origin<&[Value]>, args: &[Value], frame: usize, atoms: &[bool], from: &[Option]) -> Result { + assert_eq!(args.len(), atoms.len()); + let atoms = match array::flatten(args) { + Some((trees, _)) => trees.leaf_ranges().zip(atoms).flat_map(|(range, &atom)| std::iter::repeat_n(atom, range.len())).collect::>().into_boxed_slice(), + None => atoms.into(), }; - let (true, Arg::Batched(b)) = (routed, y) else { return stop(Stop::refusing(level, p)) }; - Some(p.call_axis(x.map(Arg::all), &b.whole, Some(axis + b.frame), span).and_then(|whole| result(level, whole, b.frame, || true, Stop::refusing(level, p)))) -} - -/// The fold by `p` of each cell of batch `b` of run `level` along the cell axes `axes`, with `seed` as each lane's seed, as one fold of the batch's whole along them past its frame, by Rank's rule for folds (`eval::folded_cells`). `None` where that rule doesn't apply. -fn fold_cells(level: u64, b: &Batch, p: Primitive, kind: FoldKind, axes: &[usize], seed: Option<&Value>, span: &Span) -> Option> { - Some( - crate::eval::folded_cells(p, kind, axes, seed, &b.whole, b.rank(), span)? - .and_then(|whole| result(level, whole, b.frame, || true, Stop::new(level, Cause::Rule)))) -} - -/// The fold by `p` of each cell along the cell axes `axes`, where `value` is a batch of the running batched level (`fold_cells`). A fold reaches `call` along one axis, so `eval::folded_along` asks here for a fold along several. `None` for any other value. -pub(crate) fn folded(value: &Value, p: Primitive, kind: FoldKind, axes: &[usize], span: &Span) -> Option> { - let level = session().batch_level()?; - let Arg::Batched(b) = arg(value, level) else { return None }; - fold_cells(level, b, p, kind, axes, None, span) -} - -/// The call of system function `f`, or of its inverse, `function`, as batched run `level` handles it. A random draw runs its own code, which draws for each cell as `¿` does (`distribution::drawn_cells`). Any other system function runs once for each cell, on that cell's arguments, as it runs once for each item of an argument that holds several (`system::Item`), and the cells' results join into one batch (`each_cell`). Code in it that acts stops the run where it acts (`Context::acts`). So does an argument that holds the run's batches inside a nested array, because a cell's call would read them whole. -fn system( - function: &Function, f: &crate::system::SystemFunction, left: Option<&Value>, right: &Value, level: u64, span: &Span) -> Option> { - if f.call.draws() { return None; } - let nested = |v: &Value| v.carrier_at(level).is_none() && v.newest().is_some_and(|m| m.id() == level); - if left.is_some_and(nested) || nested(right) { return stop(Stop::new(level, Cause::Rule)); } - let (_, frame) = session().batched()?; - Some(each_cell(level, &frame, |c| { - let x = left.map(|x| cell(x, c)).transpose()?; - function.call_array(x.as_ref(), &cell(right, c)?, span) - })) -} - -/// `operation` on `x` and `y` by Rank's routes for whole arguments (`eval::whole_cells`), at the cells' ranks, or on each matrix of the cells where `matrices`, as a result batch at level `level`. `None` where neither argument is a batch at this level, as for `⌻` with a batch inside its point, whose own code then runs. The run stops where the call would hold too many items (`oversized`), where no route covers the call, where a pervasive call's result has a kind that one route wouldn't give every item (`pervasive::expected`), and where a batch can't hold the result (`result`), such as one that gives each cell a scalar. -fn whole_call(operation: Operation, x: Option>, y: Arg<'_>, matrices: bool, level: u64, span: &Span) -> Option> { - let batch = [x, Some(y)].into_iter().flatten().find_map(|a| match a { Arg::Batched(b) => Some(b), Arg::Constant(_) => None })?; - if let Some(chunk) = oversized(x, y, batch.cells()) { return stop(Stop::oversized(level, chunk)); } - let pervasive = match operation { Operation::Primitive(p) if p.facts(x.is_some()).pervasive() => Some(p), _ => None }; - let kind = pervasive.and_then(|p| crate::pervasive::expected(p, x.map(Arg::all), y.all())); - let rank = |a: Arg<'_>| if matrices { 2 } else { a.rank() }; - let whole = match crate::eval::whole_cells(operation, x.map(|a| (a.all(), rank(a))), (y.all(), rank(y)), span) { - Some(Ok(whole)) => whole, Some(Err(e)) => return Some(Err(e)), None => return stop(refusal(level, operation)) - }; - // A result of another kind took a route that some cells alone wouldn't take. - if kind.is_some_and(|k| whole.template_dtype().is_none_or(|d| d.kind() != k.kind())) { return stop(refusal(level, operation)); } - // A pervasive call gives a cell an atom where every argument at the cell is one, and a constant scalar would make it a scalar. Another primitive's first cell shows it. Stacks of matrices give atoms only as determinants. - let atoms = || match (operation, pervasive) { - (_, Some(_)) => [x, Some(y)].into_iter().flatten().all(|a| match a { Arg::Constant(c) => c.is_atom(), Arg::Batched(_) => true }), - (Operation::Primitive(p), None) => p.call(x.map(Arg::first), y.first(), span).is_ok_and(|v| v.is_atom()), _ => true + let body = Function::applied_kind(crate::eval::OperatorKind::Framed { axes: frame, atoms }, vec![Value::Function(function.clone())])?; + let origin = body.bound(parent, args, from)?; + Operation::dispatch(origin.reborrow())?.assemble() +} + +pub(crate) fn framed(function: &Function, axes: usize, atoms: &[bool], origin: Origin<&[Value]>) -> Result { + if axes == 0 { return function.run(origin); } + let positions: Vec<_> = Function::argument_ranges(&origin)?.into_iter().flatten().flatten().collect(); + assert_eq!(positions.len(), atoms.len(), "one cell atom flag per explicit argument leaf"); + let descriptions = positions.iter().map(|&i| Desc::of(&origin.list[i]).map(|d| d.into_owned())).collect::>>(); + let Some(descriptions) = descriptions.filter(|descs| !descs.is_empty() && descs.iter().all(|d| d.shape().len() >= axes)) else { + return crate::program::run::framed_fallback(function, axes, atoms, origin); }; - Some(result(level, whole, batch.frame, atoms, refusal(level, operation))) -} - -/// The cells of each chunk that a call on `x` and `y` can run on, where on all the frame's `cells` cells it would hold too many items, or `None`. It holds too many where its constants with axes, taken once for each cell, would pass both 4 times the items that its arguments hold, and `CHUNK` items. Broadcasting and spreading repeat such constants (`eval::whole_cells`), and so does a result the size of a constant in each cell, where a run over cells holds one cell's share at a time. A batch counts the items it holds logically, so windows and an outer product's laid arguments count in full. A chunk is the most cells, a power of two, whose share of the constants is at most `CHUNK` items, and at least one cell. -fn oversized(x: Option>, y: Arg<'_>, cells: usize) -> Option { - let (mut held, mut repeated) = (0usize, 0usize); - for a in [x, Some(y)].into_iter().flatten() { - held = held.saturating_add(a.all().len()); - if let Arg::Constant(c) = a { if !c.shape().is_empty() { repeated = repeated.saturating_add(c.len().saturating_mul(cells)); } } - } - too_many(repeated, held, CHUNK).then(|| 1 << (CHUNK / (repeated / cells)).max(1).ilog2()) -} - -/// The most items that a batched call repeats in constants, before it runs in chunks of cells instead, and the most that each chunk repeats (`oversized`). Measured on 20 October 2026 for maps with 256 to a million items of constants per cell: chunks of 2²¹ items beat the run over cells at every size, by 10% to 7 times, while chunks of 2²⁴ items lost from 256K items per cell on, and chunks of 2¹⁸ items were no faster. -pub(crate) const CHUNK: usize = 1 << 21; - -/// Whether a batched call that repeats `repeated` items of constants holds too many against the `held` items of its arguments: more than both 4 times `held` and `floor` items. -pub(crate) fn too_many(repeated: usize, held: usize, floor: usize) -> bool { repeated > held.saturating_mul(4).max(floor) } - -/// The batch that `value`'s items form where `value` has no carrier of its own and some item is a batch, as when a batched operand returns a strand of batches (`whole_over`, `truths`). Each cell of the batch holds the array of the items' values at that cell. Each item must be a batch of atoms at one level, or a plain atom. The items' kinds must join to one compact dtype by the rule that a cell-by-cell run uses for its items (`Widening::Items`). An empty layout can't form a batch, because its cells would be scalars, and a batch can't hold a scalar as a cell. `None` where any of these rules fails, and for a value that has a carrier or holds no values as items. -pub(crate) fn joined(value: &Value) -> Option { - if value.carries_here() || !value.holds_values() { return None; } - let (layout, items) = (value.layout(), value.elements().collect::>()); - let first = items.iter().find_map(|v| v.carrier().and_then(Carrier::batch))?; - let shape = layout.shape(); - if shape.is_empty() { return None; } - let frame = &first.whole.layout().shape()[..first.frame]; - let mut joint: Option = None; - for item in &items { - let kind = match item.carrier() { - Some(Carrier::Batch(b)) if b.level == first.level && b.rank() == 0 && b.whole.layout().shape()[..b.frame] == *frame => b.whole.template_dtype(), - None if item.is_atom() => item_dtype(item), _ => None - }?; - joint = Some(match joint { Some(d) => d.join(kind, Widening::Items)?, None => kind }); - } - // Each item's values at every cell, item after item, then read with the frame's axes first. - let count = crate::array::count(frame); - let mut data = Gather::with_widening(&[], items.len() * count, Widening::Items); - for item in &items { match item.carrier().and_then(Carrier::batch) { Some(b) => data.append(&b.whole), None => data.fill(item, count) } } - let stacked = data.finish(Layout::from([&[items.len()][..], frame].concat()), || first.template().clone()).ok()?; - let tables: Vec = Steps::row_major(frame, 1, 0).chain(Steps::row_major(shape, count, 0)).collect(); - let whole = crate::primitive::copied(&stacked, first.frame_layout().concat(layout), &Mapping::from(tables)).ok()?; - Some(batch(first.level, whole, first.frame)) -} - -/// `whole`, which holds the frame's axes, laid out as `frame`, before each cell's items, with `n` items in each cell: as it is, or with each cell's one item repeated `n` times. -fn repeated(whole: &Value, frame: &Layout, n: usize) -> Value { - if n == 1 || crate::array::count(&whole.shape()[frame.shape().len()..]) != 1 { return whole.clone(); } - let one = whole.relaid(frame.concat(&Layout::from([1]))).expect("a cell of one item lays out as one item"); - crate::agreement::expand(&one, &frame.concat(&Layout::from([n]))) -} - -/// Offsets into the whole of `cells` cells of `size` items each: each cell's `n` offsets moved to its place, with each `Steps::FILL` kept. Cell `c` uses `list[c * stride..][..n]`, so a stride of 0 gives every cell the same offsets. A size of 0 reads an array that every cell shares. -fn placed(list: &[usize], n: usize, stride: usize, cells: usize, size: usize) -> Vec { - (0..cells).flat_map(|c| list[c * stride..][..n].iter().map(move |&o| if o == Steps::FILL { o } else { c * size + o })).collect() -} - -/// `value`'s whole where it is a batch of run `level`, or `None` where every cell shares it. The run stops at a value that carries a newer level, such as an outer `∂`'s, which offsets in each cell don't serve. -fn level_whole(value: &Value, level: u64) -> Result, Stop> { - match arg(value, level) { - Arg::Batched(b) => Ok(Some(&b.whole)), Arg::Constant(c) if c.older_than(level) => Ok(None), Arg::Constant(_) => Err(Stop::new(level, Cause::Rule)) - } -} - -/// `target` with `source` combined into it by `combine`, a write or a sum at offsets, where one of them is a batch of run `level`, whose frame `frame` lays out the cells, as one combination on the wholes. Cell `c` combines at the offsets from `list[c * stride..]`, which `placed` moves to its place, so a stride of 0 gives every cell the same offsets. A constant `target` is laid over the frame first. `source` gives each cell an item for each offset, or one item for them all. The run stops at any other source, and where the combined whole can't be a batch, as where a cell's items turn mixed. -fn scattered>( - target: &Value, source: &Value, level: u64, frame: &Layout, list: &[usize], stride: usize, - combine: impl FnOnce(&mut Value, &[usize], &Value) -> Result<(), E>) -> Result { - let stop = || Stop::new(level, Cause::Write); - let n = if stride == 0 { list.len() } else { stride }; - let cells = frame.count().max(1); - let mut whole = match level_whole(target, level)? { Some(whole) => whole.clone(), None => spread(target, frame).map_err(|_| stop())? }; - let items = match level_whole(source, level)? { - Some(items) if [1, n].contains(&(items.len() / cells)) => repeated(items, frame, n), None if source.len() == 1 => source.clone(), - None if source.len() == n => spread(source, frame).map_err(|_| stop())?, _ => return Err(stop().into()) - }; - let at = placed(list, n, stride, cells, whole.len() / cells); - combine(&mut whole, &at, &items)?; - if whole.template_dtype().is_none() { return Err(stop().into()); } - Ok(batch(level, whole, frame.shape().len())) -} - -/// `target` with `source` combined by `p` at offsets, as `Pairing::At` combines them (`Primitive::at`), on the wholes that `scattered` lays out. -fn combined(target: &Value, source: &Value, p: Primitive, level: u64, frame: &Layout, list: &[usize], stride: usize, span: &Span) -> Result { - scattered(target, source, level, frame, list, stride, |whole, at, items| p.at(whole, Writes::Offsets(at), items, span)) -} - -/// Writes `source` into `target` at `writes`, positions in a cell that every cell of batched run `level` shares, where one of them is a batch of the run (`transform::written`), as one scatter into the wholes (`scattered`). -pub(crate) fn written(target: &mut Value, source: &Value, level: u64, writes: Writes<'_>) -> Result<(), Stop> { - let frame = frame_of(&[target, source], level).expect("a batch of the run takes part in the write"); - *target = scattered(target, source, level, &frame, &writes.listed(), 0, Value::scatter)?; - Ok(()) -} - -/// The frame of the first of `values` that is a batch of run `level`. -fn frame_of(values: &[&Value], level: u64) -> Option { - values.iter().find_map(|v| match arg(v, level) { Arg::Batched(b) => Some(b.frame_layout()), Arg::Constant(_) => None }) -} - -/// Each cell's own offsets among a cell's items, where a record's gather reads and its scatter adds (`linear::Offsets`), and where a selection by each cell's own positions reads and writes (`primitive::Targets`), in batched run `level`. Cell `c` of the cells that `frame` lays out uses `list[c * n..][..n]`. -pub(crate) struct Cells { level: u64, frame: Layout, list: Vec } - -impl Cells { - /// The offsets `list` that the batch of ids `ids` gives its cells. - pub(crate) fn new(ids: &Batch, list: Vec) -> Self { Self { level: ids.level, frame: ids.frame_layout(), list } } - - /// The bytes that the offsets hold. - pub(crate) fn footprint(&self) -> usize { self.list.capacity() * size_of::() } - - /// The number of offsets in each cell. - pub(crate) fn len(&self) -> usize { self.counts().1 } - - /// The number of cells, and of offsets in each. - fn counts(&self) -> (usize, usize) { let cells = self.frame.count().max(1); (cells, self.list.len() / cells) } - - /// The run's stop for a write that these offsets can't make, such as one into an item nested in a value that every cell shares. - pub(crate) fn stop(&self) -> Stop { Stop::new(self.level, Cause::Write) } - - /// Each cell's items of `source` at its offsets, laid out as `layout`. A `Steps::FILL` offset reads 0. The run stops where the items can't make a batch. - pub(crate) fn read(&self, source: &Value, layout: &Layout) -> Result { - let (cells, n) = self.counts(); - let (all, size) = match level_whole(source, self.level)? { Some(whole) => (whole, whole.len() / cells), None => (source, 0) }; - let read = crate::primitive::copied_at(all, placed(&self.list, n, n, cells, size), self.frame.concat(layout), None).described("invalid read")?; - result(self.level, read, self.frame.shape().len(), || true, Stop::new(self.level, Cause::Rule)) - } - - /// Each cell's zeros of `layout`, with its items of `items` added at its own offsets, as a cotangent sums them (`linear::sum_at`). - pub(crate) fn added(&self, items: &Value, layout: &Layout, zero: &Value, span: &Span) -> Result { - scattered(&crate::linear::zeros(layout, zero), items, self.level, &self.frame, &self.list, self.counts().1, |whole, at, items| { - crate::linear::sum_at(whole, Writes::Offsets(at), items, span) - }) - } - - /// `target` with each cell's items of `source` written at its own offsets (`scattered`). - pub(crate) fn written(&self, target: &Value, source: &Value) -> Result { - scattered(target, source, self.level, &self.frame, &self.list, self.counts().1, Value::scatter) - } - - /// `target` with each cell's items of `source` combined by `p` at its own offsets, as `Pairing::At` combines them (`combined`). - pub(crate) fn combined(&self, target: &Value, source: &Value, p: Primitive, span: &Span) -> Result { - combined(target, source, p, self.level, &self.frame, &self.list, self.counts().1, span) - } -} - -/// The selectors in `parts` as `primitive::framed_plan` takes them, where some part is a batch of the running batched run: a batch part's whole, which holds the frame's axes first, and any other part as it is. It gives the run's level and the first batch part too. `None` where no part is a batch of a running level. -fn framed_parts(parts: &[IndexPart]) -> Option<(u64, Option<&Batch>, Vec<(Value, bool)>)> { - if !parts.iter().any(|p| matches!(p, IndexPart::Axis(v) if v.carrier().is_some())) { return None; } - let level = session().batch_level()?; - let mut first = None; - let laid = parts.iter().map(|part| { - let IndexPart::Axis(v) = part else { return None }; - Some(match arg(v, level) { Arg::Batched(b) => { first.get_or_insert(b); (b.whole.clone(), true) } Arg::Constant(c) => (c.clone(), false) }) - }).collect::>>()?; - Some((level, first, laid)) -} - -/// `⌷`, or bracket indexing where `paired`, of each cell of the running batched run where some selector in `parts` is a batch of the run (`indexing::select`): one selection of the whole (`primitive::framed_plan`) by the selectors that `framed_parts` lays out. `right` can be a batch too. `None` where `framed_parts` or `framed_plan` gives none, so that the call reads each cell, which stops the run. -pub(crate) fn bracketed(parts: &[IndexPart], right: &Value, paired: bool, fill: Option<&Value>, span: &Span) -> Option> { - let (level, first, laid) = framed_parts(parts)?; - let (right, framed, b) = match arg(right, level) { Arg::Batched(b) => (&b.whole, true, first.unwrap_or(b)), Arg::Constant(c) => (c, false, first?) }; - let plan = crate::primitive::framed_plan(&laid, right.layout(), framed, paired, &b.frame_layout(), span)?; - let whole = crate::primitive::planned_read(right, plan, false, fill.cloned()).ok()?; - Some(result(level, whole, b.frame, || true, Stop::new(level, Cause::Rule))) -} - -/// Each cell's offsets and selection layout from the shared framed plan. Fill-bearing update plans fall back to ordinary execution. -pub(crate) fn cell_selection(parts: &[IndexPart], right: &Value, paired: bool, span: &Span) -> Option<(Layout, Cells)> { - let (level, Some(b), laid) = framed_parts(parts)? else { return None }; - let frame = b.frame_layout(); - let plan = crate::primitive::framed_plan(&laid, right.layout(), false, paired, &frame, span)?; - if plan.steps.iter().any(Steps::fills) { return None; } - let list = Steps::combined(0, &plan.steps); - Some((plan.layout.axes(frame.shape().len()..plan.layout.shape().len()).into(), Cells { level, frame, list })) -} - -/// A batch whose whole is a dual at level `tag`, split into the batch of the dual's primal and the batch of its tangent. An attached rule that applies at an older `∂` level meets such a batch (`route`). The tangent is the full one, in the primal's layout, because a batch's whole holds the frame's axes, which a pervasive rule's smaller tangent can lack. `None` for any other value. -pub(crate) fn split_whole(value: &Value, tag: u64) -> Option<(Value, Value)> { - let b = value.carrier().and_then(Carrier::batch)?; - let (primal, tangent) = crate::derivative::split(&b.whole, tag); - Some((batch(b.level, primal, b.frame), batch(b.level, tangent?, b.frame))) -} - -/// The dual at level `tag` of `primal` and `tangent`, where one of them is a batch newer than `tag`, as the batch of the dual of their wholes, so that the newest level stays outermost. A part that isn't that batch is the same in every cell, and `derivative::attached`, the only producer, has already laid it over the result's layout (`along`), so it lays over the frame. `None` where neither part is such a batch. -pub(crate) fn dual_of_wholes(tag: u64, primal: &Value, tangent: &Value) -> Option { - let b = [primal, tangent].into_iter().find_map(|v| v.carrier().and_then(Carrier::batch).filter(|b| b.level > tag))?; - let frame = b.frame_layout(); - let [p, t] = [primal, tangent].map(|v| whole_over(v.clone(), b.level, &frame).expect("a dual's part that every cell shares lays over the frame")); - Some(batch(b.level, crate::derivative::dual(tag, p, t), b.frame)) -} - -/// A map's arguments as its batched try reads them, each with the rank of its cells. An argument with a frame is laid over the map's frame (`Arguments::laid`). An argument with no frame is the same in every cell. -pub(crate) struct Arguments { pub(crate) left: Option<(Value, usize)>, pub(crate) right: (Value, usize) } - -impl Arguments { - /// `left` and `right`, each with the rank of its cells, laid over the map's frame through `agreement`, the pairing of their frames that the map's calls on each cell read. An argument with no frame stays as it is. `None` where a laid argument would pass the size limit. - pub(crate) fn laid(agreement: &Agreement, left: Option<(Value, usize)>, right: (Value, usize)) -> Option { - let lay = - |(v, k): (Value, usize), mapping: &Mapping| Some((if v.shape().len() == k { v } else { laid_cells(&v, k, mapping, &agreement.layout).ok()? }, k)); - let left = match left { Some(x) => Some(lay(x, &agreement.left)?), None => None }; - Some(Self { left, right: lay(right, &agreement.right)? }) - } -} - -/// An argument of a batched try: a batch of its cells, or a constant that applies to every cell. -enum Side { Batched(Value), Constant(Value) } - -/// How a batched try's run takes its level: it begins a batched run, or it widens batched run `level`, whose frame is `outer`, for a map inside that run (`transform::Levels::case`). A widened run's frame is `outer` followed by the map's frame. -enum Entry { Begin, Widen { level: u64, outer: Layout } } - -/// An argument whose cells have rank `rank`, as a side of a batched try that enters its run by `entry`, whose frame is `frame`, or `None` where the try can't batch it. An argument with no frame is a constant, which every cell reads whole. An argument with a frame, which its map laid over the map's frame, is a batch where it is compact. A batch has no names on its frame axes (`unnamed_frame`), as the run has none on its frame (`tried`). A widening lays each batch of the running run over `frame`, whether or not its cells have the map's frame (`laid_over`), and spreads a constant with a frame over the run's frame. It can't batch a value that carries another level's data. -fn side((value, rank): (Value, usize), entry: &Entry, frame: &Layout) -> Option { - let value = match entry { - Entry::Begin => value, - Entry::Widen { level, outer } => { - let value = joined(&value).unwrap_or(value); - match value.carrier_at(*level).and_then(Carrier::batch) { - Some(b) => laid_over(&b.whole, b.whole.shape().len().checked_sub(rank).filter(|&axes| axes >= b.frame)?, frame).ok()?, - None if value.carried() => return None, None if value.shape().len() > rank => spread(&value, outer).ok()?, None => value - } + let mut references = Vec::with_capacity(axes); + let mut frame = Layout::from([]); + for axis in 0..axes { + let source = descriptions.iter().position(|d| d.shape()[axis] != 1).unwrap_or(0); + let length = descriptions[source].shape()[axis]; + if descriptions.iter().any(|d| d.shape()[axis] != 1 && d.shape()[axis] != length) { + return Err(Error::new(rustymath::error::Kind::Length, "map frames do not agree")); } + frame = frame.concat(&descriptions[source].layout.axes(std::iter::once(axis))); + references.push(source); + } + let scope = Scope::new(LevelKind::Batched { frame }); + let mut inputs = origin.list.to_vec(); + for ((&position, mut cell), &atom) in positions.iter().zip(descriptions).zip(atoms) { + cell.layout = cell.layout.axes(axes..cell.shape().len()); + cell.atom = atom; + inputs[position] = carried(scope.level.id, Carried::Batch { whole: inputs[position].clone(), cell }); + } + let result = function.run(origin.with_list(inputs.as_slice())); + let id = scope.level.id; + drop(scope); + let result = match result { + Err(error) if error.batch_level() == Some(id) => return crate::program::run::framed_fallback(function, axes, atoms, origin), + result => result?, }; - if value.shape().len() == rank { return Some(Side::Constant(value)); } - value.template_dtype().is_some().then(|| Side::Batched(value.unnamed_frame(rank))) -} - -/// `whole`, whose first `axes` axes are its frame, laid over `frame`, which starts with those axes, as leading-axis agreement lays it: repeated along `frame`'s further axes, or `whole` itself where its frame is `frame`. -pub(crate) fn laid_over(whole: &Value, axes: usize, frame: &Layout) -> Result { - laid_cells(whole, whole.shape().len() - axes, &Agreement::new(&whole.layout().axes(0..axes), frame)?.left, frame) -} - -/// One batched run of `operand` on the cells that `frame` lays out, entered by `entry`, with a batch of the cells of each batched side, and each constant as it is. It gives the whole of the cells' results (`whole_over`). -fn run(operand: &Function, x: Option<&Side>, y: &Side, frame: &Layout, entry: &Entry, span: &Span) -> Result { - let call = |level: u64| -> Result { - let wrapped = |side: &Side| match side { Side::Batched(v) => batch(level, v.clone(), frame.shape().len()), Side::Constant(c) => c.clone() }; - let value = operand.call_array(x.map(&wrapped).as_ref(), &wrapped(y), span)?; - // The run's cells share each name that `•index` draws, where calls on single cells would each draw their own (`transform::Levels::keep_fresh`). - if session().holds_fresh(&value) { return Err(Stop::new(level, Cause::Effect).into()); } - Ok(value) - }; - let (level, called) = match entry { - Entry::Begin => { - let level = session().begin(crate::transform::Kind::batched(frame.clone())); - let called = call(level); - session().end(); - (level, called) - } - Entry::Widen { level, .. } => (*level, session().widened(frame, span, || call(*level))), - }; - match called { - Ok(value) => whole_over(value, level, frame).ok_or(Ended::Fell("the result")), - Err(e) => Err(match e.kind { - ErrorKind::Stop(stop) if stop.level() == level => Ended::Stopped(stop, None), _ if e.kind.is_control() => Ended::Passed(e), - _ => Ended::Fell("an error") - }), - } -} - -/// The whole of the cells' results from batched runs of `operand` on successive chunks of `chunk` cells of the frame that `frame` lays out, each run on its chunk's cells of the batched sides. The chunks' results join as the run over cells assembles its cells' results (`Widening::Items`). Each generator first gets back its state from before the try, so the chunks draw what one run on all the cells would draw. -fn chunked(operand: &Function, x: Option<&Side>, y: &Side, frame: &Layout, chunk: usize, span: &Span) -> Result { - crate::distribution::settle(&session().drawn, true); - let (axes, cells) = (frame.shape().len(), frame.count()); - let mut wholes = Vec::new(); - for start in (0..cells).step_by(chunk) { - let len = chunk.min(cells - start); - let slice = |side: &Side| match side { - Side::Batched(v) => { - let cell = v.layout().axes(axes..v.shape().len()); - let lens: Vec = [len].into_iter().chain(cell.shape().iter().copied()).collect(); - let steps: Vec = Steps::row_major(&lens, 1, start * cell.count()).collect(); - Side::Batched( - crate::primitive::copied(v, Layout::from(vec![len]).concat(&cell), &Mapping::from(steps)) - .expect("a chunk's positions lie inside its argument")) - } - Side::Constant(c) => Side::Constant(c.clone()), + array::map_leaves(&result, |leaf| { + let (whole, batched) = match at_level(leaf, id) { + Some(Carried::Batch { whole, .. }) => (whole, true), _ => (leaf, false), }; - wholes.push(run(operand, x.map(&slice).as_ref(), &slice(y), &Layout::from(vec![len]), &Entry::Begin, span)?); - } - let cell = wholes[0].layout().axes(1..wholes[0].shape().len()); - if wholes.iter().any(|w| w.layout().axes(1..w.shape().len()) != cell) { return Err(Ended::Fell("the result")); } - let mut gather = Gather::with_widening(&wholes.iter().collect::>(), cells * cell.count(), Widening::Items); - for whole in &wholes { gather.append(whole); } - gather.finish(frame.concat(&cell), || wholes[0].zeroed()).map_err(|_| Ended::Fell("the result")) + let cell_start = if batched { axes } else { 0 }; + let mut dimensions: Vec<_> = references.iter().enumerate().map(|(axis, &source)| ViewAxis { input: 1 + source as u32, axis: Some(axis) }).collect(); + dimensions.extend((cell_start..whole.rank()).map(|axis| ViewAxis { input: 0, axis: Some(axis) })); + let destinations = (0..whole.rank()).map(|axis| if batched { axis } else { axes + axis }).collect(); + let operation = Operation::Read(ReadOp::Axes { axes: dimensions.into(), destinations }); + let mut list = Vec::with_capacity(1 + positions.len()); + list.push(whole.clone()); list.extend(positions.iter().map(|&i| origin.list[i].clone())); + let from: Vec<_> = std::iter::once(None).chain(positions.iter().copied().map(Some)).collect(); + let call = crate::eval::projected_call(&origin, operation, &list, &from)?; + Ok(Operation::dispatch(call.reborrow())?.into_vec().pop().expect("an axis view has one output")) + }) } -/// Where the `batch` pref is on, the result of the map whose frame is `layout`, from one call of `operand` on batches of its cells. With `mixed`, the cells' results mix into one array, as Rank mixes them. Otherwise they are the result's items, as in Each. `None` means the map calls its operand once for each cell instead. -/// -/// The try's conditions: `tried` gives `None` for an empty frame or a frame of rank 0, and where the running levels allow no try (`tries`). Otherwise it calls `arguments` for the map's whole arguments. It gives `None` where `arguments` does, and for an argument with a frame that isn't compact (`side`). It gives `None`, after a `fallbacks` event of `•explain`, for a dfn operand whose earlier batched run stopped for a cause that it remembers for every call, without calling `arguments`, or for a cause that it remembers for these arguments (`signature`). Each refusal before the call, other than an empty frame, is a decline with its reason (`rustymath::events`). -/// -/// The run: a map outside any batched run begins one (`Entry::Begin`). A map inside one widens it (`Entry::Widen`): the run's frame comes first in the widened frame, each of the run's batches lays over the widened frame, and each constant with a frame spreads over the run's frame (`side`). -/// -/// The call: `operand` runs once, with a batch of the cells of each argument that has a frame, and with each other argument as it is (`run`). Where a run that began stops for too many items with chunks of cells that wouldn't (`Stop::chunk`), the operand runs once on each chunk instead (`chunked`). A chunk that stops for too many items again starts the chunks over with the smaller chunks that its stop gives. Where a call stops at its level for another cause, or a widened run stops for any cause, or a call fails, `tried` gives `None` after a `fallbacks` event that names the stop's cause (`transform::Stop`), or `"an error"`. A widened try that gives `None` leaves the running run going, and the map calls its operand once for each of its cells inside it. A dfn operand remembers the stop for as long as its cause says (`transform::Cause::kept`). An interrupt, a timeout and an older level's stop pass through. Unless a try that began its run gives a result, each generator that it drew from gets back its state from before the try (`distribution::settle`). A widened try puts back no generator, so the map's draws can then differ from a run with the `batch` pref off, as a cell's draws can in any batched map (`meta/random.md`). -/// -/// The result's assembly: the cells' results come from the call's result where that is a batch of the cells, or holds batches as items that join into one batch, as a strand of batches does (`joined`). A result that is an atom or a compact array, with no carrier, is every cell's result. For any other result, such as a function, `tried` gives `None` after a `fallbacks` event. A widened run gives each cell of the running run its map's result, the mixed results or Each's atoms, as a batch of that run. Where Each's results are arrays, a widened try gives `None` after a `fallbacks` event. -pub(crate) fn tried( - operand: &Function, arguments: impl FnOnce() -> Option, layout: &Layout, mixed: bool, span: &Span) -> Option> { - let frame = layout.shape(); - if frame.is_empty() || frame.contains(&0) { return None; } - let cells = layout.count(); - let declined = |reason: &'static str| -> Option> { - rustymath::events::declined(rustymath::events::Route::Batching, reason, cells); - None - }; - if !tries(cells) { return None; } - let memory = operand.remembers().map_or(NOTHING, |d| d.remembered()); - if memory & !CAUSE == ALWAYS { return fell_back(Earlier::of(memory), cells); } - let Some(Arguments { left, right }) = arguments() else { return declined("an argument too large to lay over the frame") }; - let entry = session().batched().map_or(Entry::Begin, |(level, outer)| Entry::Widen { level, outer }); - // The run's frame has no axis names. Every argument is laid over the frame already, so names stay on cell axes, which pair by name as separate calls on cells pair them. The result gets the frame's names back. - let unnamed = if layout.has_names() { Cow::Owned(layout.clone().inherit_names(Vec::new())) } else { Cow::Borrowed(layout) }; - let run_frame = match &entry { Entry::Begin => unnamed, Entry::Widen { outer, .. } => Cow::Owned(outer.concat(&unnamed)) }; - let as_side = |argument| side(argument, &entry, &run_frame); - let (x, y) = match (left.map(as_side), as_side(right)) { - (Some(None), _) | (_, None) => return declined("an argument that isn't compact"), (x, Some(y)) => (x.flatten(), y) - }; - let signature = |with_frame: bool| signature(x.as_ref(), &y, run_frame.shape(), with_frame); - if memory != NOTHING && memory & !CAUSE == signature(memory & 1 == 1) { return fell_back(Earlier::of(memory), cells); } - let mut ran = run(operand, x.as_ref(), &y, &run_frame, &entry, span); - if let Entry::Begin = entry { - while let Err(Ended::Stopped(stop, _)) = &ran && let Some(chunk) = stop.chunk() { ran = chunked(operand, x.as_ref(), &y, layout, chunk, span); } - crate::distribution::settle(&session().drawn, ran.is_err()); - } - let whole = match ran { - Ok(whole) => whole, - Err(Ended::Stopped(stop, _)) => { - if let Some(definition) = operand.remembers() { - match stop.cause().kept() { - Kept::Always => definition.remember(holding(ALWAYS, stop.cause())), - Kept::Arguments { frame } => definition.remember(holding(signature(frame), stop.cause())), Kept::Never => {} - } +pub(crate) fn rule(level: &Level, origin: Origin<&[Value]>) -> Result> { + let LevelKind::Batched { frame } = &level.kind else { unreachable!("a batched rule has a frame") }; + let cells = Operation::describe(&origin.call_view(&origin.list))?.ok_or_else(|| Error::batch_stop(level.id))?; + let wholes = origin.list.iter().map(|value| array::map_leaves(value, |leaf| Ok(match at_level(leaf, level.id) { + Some(Carried::Batch { whole, .. }) => whole.clone(), _ => leaf.clone(), + }))).collect::>>()?; + let call = Operation::cells(&origin, &wholes, frame.shape().len())?; + let results = Operation::dispatch(call.reborrow())?; + debug_assert_eq!(results.as_ref().len(), cells.as_ref().len()); + let mut cells = cells.into_vec().into_iter(); + Ok(results.map(|whole| carried(level.id, Carried::Batch { whole, cell: cells.next().expect("batching preserves output structure") }))) +} + +pub(crate) fn primitive_cells(origin: &Origin<&[Value]>, wholes: &[Value], frame: usize) -> Result>> { + use crate::primitive::Pairing; + let level = origin.list.iter().filter_map(Value::level).max().and_then(Level::find).ok_or_else(Error::planning)?; + let LevelKind::Batched { frame: layout } = &level.kind else { return Err(Error::planning()); }; + let mut values = wholes.to_vec(); + let references = (0..frame).map(|axis| origin.list.iter().enumerate().find_map(|(i, value)| { + matches!(at_level(value, level.id), Some(Carried::Batch { .. })).then_some(i) + .filter(|&i| wholes[i].shape().get(axis) == layout.shape().get(axis)) + }).ok_or_else(|| Error::batch_stop(level.id))).collect::>>()?; + for position in origin.call_view(&origin.list).data_positions() { + if at_level(&origin.list[position], level.id).is_some() || Desc::of(&values[position]).is_none() { continue; } + let value = &values[position]; + let mut axes: Vec<_> = references.iter().enumerate().map(|(axis, _)| ViewAxis { input: 1 + axis as u32, axis: Some(axis) }).collect(); + axes.extend((0..value.rank()).map(|axis| ViewAxis { input: 0, axis: Some(axis) })); + let destinations = (frame..frame + value.rank()).collect(); + let inputs: Vec<_> = std::iter::once(value.clone()).chain(references.iter().map(|&i| wholes[i].clone())).collect(); + let from: Vec<_> = std::iter::once(Some(position)).chain(references.iter().copied().map(Some)).collect(); + let call = crate::eval::projected_call(origin, Operation::Read(ReadOp::Axes { axes: axes.into(), destinations }), &inputs, &from)?; + values[position] = Operation::dispatch(call.reborrow())?.assemble()?; + } + let pair = match &origin.operation { + Operation::Primitive { pairing, .. } => (*pairing, true), + Operation::Recipe(_) | Operation::System => (Pairing::Axes, true), + Operation::Fold { first, pairing, .. } => { + if *first && *pairing == Pairing::Axes { + let Operation::Fold { primitive, scan, .. } = &origin.operation else { unreachable!() }; + let args: Vec<_> = std::iter::once(Value::integer(frame as i64)).chain(values[..origin.explicit as usize].iter().cloned()).collect(); + let from: Vec<_> = std::iter::once(None).chain((0..origin.explicit as usize).map(Some)).collect(); + return crate::eval::projected_call(origin, Operation::Fold { primitive: *primitive, first: *first, scan: *scan, pairing: Pairing::Explicit }, &args, &from); } - return fell_back(stop, cells); + (*pairing, false) } - Err(Ended::Fell(reason)) => return fell_back(reason, cells), - Err(Ended::Passed(e)) => return Some(Err(e)), - }; - let cell = whole.layout().axes(run_frame.shape().len()..whole.shape().len()); - let one_array = mixed || cell.shape().is_empty(); - let assembled = match entry { - Entry::Begin if one_array => whole.relaid(layout.concat(&cell)), - Entry::Begin => whole.cells(cell.shape().len()).and_then(|cells| cells.framed()).and_then(|items| items.relaid(layout.clone())), - Entry::Widen { .. } if !one_array => return fell_back("the result", cells), - Entry::Widen { level, outer } => whole.relaid(outer.concat(layout).concat(&cell)).map(|whole| batch(level, whole, outer.shape().len())), + Operation::Convert(_) => return Ok(origin.with_list(values)), + _ => return Err(Error::batch_stop(level.id)), }; - if assembled.is_ok() { rustymath::events::taken(rustymath::events::Route::Batching, cells); } - Some(assembled.described("invalid result")) -} - -/// Whether a map of `cells` cells may try a batched call: the `batch` pref is on, and the running levels admit a batched run (`transform::Levels::admits_batch`), or let the map widen the running one (`transform::Levels::widens`). Where it may not, the refusal is a decline (`rustymath::events`). `tried` checks it first. A caller checks it before building arguments that only a try reads. -pub(crate) fn tries(cells: usize) -> bool { - let reason = if !crate::settings::batch() { "the batch pref is off" } - else if !session().admits_batch() && !session().widens() { "a label level, or a ∂ call inside a batched run" } - else { return true; }; - rustymath::events::declined(rustymath::events::Route::Batching, reason, cells); - false + if pair.0 == Pairing::Explicit { + if origin.list[0].keys(0).is_some() { return Err(Error::batch_stop(level.id)); } + let right = origin.call_view(&origin.list).explicit_arguments().last().ok_or_else(Error::planning)?; + let axes = crate::primitive::resolve_axes(&origin.list[0], origin.list[right].layout())?; + let axes = crate::primitive::axes(&axes, origin.list[right].rank())?; + let shifted: Vec<_> = if pair.1 { (0..frame).chain(axes.into_iter().map(|axis| frame + axis)).collect() } + else { axes.into_iter().map(|axis| frame + axis).collect() }; + values[0] = Value::integers([shifted.len()], shifted.into_iter().map(|axis| axis as i64).collect())?; + } + Ok(origin.with_list(values)) } - -/// `None`, after a `fallbacks` event of `•explain` for a batched try of `cells` cells that falls back for `reason`. -fn fell_back(reason: impl std::fmt::Display, cells: usize) -> Option { crate::explain::fallback(reason, cells); None } diff --git a/src/canvas.rs b/src/canvas.rs index 987f0df8..e075a551 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -1,5 +1,5 @@ //! `•canvas`: drawing functions written in JavaScript. `•canvas Y` makes a drawing function from the JavaScript source `Y`. `X draw Y` gives a record of the source, the options `X` and the data `Y`, which displays as a canvas. Displaying it calls the JavaScript function with the canvas's 2D context, the data and the options, which `js.rs` converts to JavaScript values. -use crate::{Described, Error, Span, Value, data, display, js, keyed, system::{Call, Valence::Ambivalent, native}}; +use crate::{Described, Error, Value, data, display, js, keyed, system::{Call, HostCall, Valence::Ambivalent, native}}; use base64::{Engine, engine::general_purpose::STANDARD}; use std::sync::Arc; @@ -12,7 +12,7 @@ const BYTES_TYPE: &str = "application/x-bpl-canvas-data"; /// The drawing function for the JavaScript `source`, as `•canvas` returns it. pub(crate) fn function(source: Arc) -> Value { Value::Function(native("•canvas", Call::Canvas(source), Ambivalent)) } -pub(crate) fn factory(_: Option<&Value>, right: &Value, _: &Span) -> Result { Ok(function(data::text(right)?.into())) } +pub(crate) fn factory(HostCall { right, .. }: HostCall<'_>) -> Result { Ok(function(data::text(right)?.into())) } /// `X draw Y`: a record of the drawing function's source, the options `X` and the data `Y`, displayed as a canvas. Without options, the JavaScript gets an empty object. pub(crate) fn draw(source: &str, options: Option<&Value>, data: &Value) -> Result { @@ -22,7 +22,7 @@ pub(crate) fn draw(source: &str, options: Option<&Value>, data: &Value) -> Resul } /// The browser build sends the canvas as JSON, with its typed arrays as bytes, for the page to draw with `canvas.js`. The native build sends HTML that holds the same JSON and bytes, and runs `canvas.js` itself. Both send the text `canvas: N frames` when the options give `fps` and the data is a vector of `N` frames, and `canvas` otherwise. -fn render(_: Option<&Value>, canvas: &Value, _: &Span) -> Result { +fn render(HostCall { right: canvas, .. }: HostCall<'_>) -> Result { let (json, bytes) = js::serialize(&js::export(canvas).described("canvas data must not hold functions")?); let json = json.to_string(); // The default text form would show the whole data. diff --git a/src/csv.rs b/src/csv.rs index e9f08877..ddc3e396 100644 --- a/src/csv.rs +++ b/src/csv.rs @@ -1,5 +1,5 @@ use crate::execution::check; -use crate::{Described, Error, Number, Span, Value, array::generated_len, data::{Types, text as string}, keyed}; +use crate::{Described, Error, Number, Value, system::HostCall, array::generated_len, data::{Types, text as string}, keyed}; use num_bigint::BigInt; use rustymath::error::Kind; use std::sync::Arc; @@ -13,17 +13,15 @@ struct Options { trim: bool, decimal: char, thousands: Option, - /// The width of the floats that fields read as, the session's default width. - width: rustymath::buffer::FloatWidth, + numeric: crate::number::Numeric, } impl Options { - fn new(left: Option<&Value>, import: bool) -> Result { + fn new(left: Option<&Value>, import: bool, numeric: crate::number::Numeric) -> Result { let mut allowed = vec!["separator", "quotechar", "escapechar", "doublequote", "decimal", "thousands", "trim", "header", "fill"]; allowed.extend(if import { &["missing", "types"][..] } else { &["forcequotes", "lineending"][..] }); let common = crate::data::Options::new(if import { "•csv" } else { "•csv⁻¹" }, left, None, &allowed)?; - let width = crate::number::Numeric::current().width; - let mut opts = Self { common, separator: b',', quote: Some(b'"'), escape: None, double_quote: true, trim: false, decimal: '.', thousands: None, width }; + let mut opts = Self { common, separator: b',', quote: Some(b'"'), escape: None, double_quote: true, trim: false, decimal: '.', thousands: None, numeric }; let byte = |c: Option| -> Result, Error> { c.map(|c| { if c.is_ascii() && !matches!(c, '\r' | '\n' | '\0') { Ok(c as u8) } @@ -75,21 +73,21 @@ impl Options { if let Ok(n) = field.parse::() { return Some(Number::from_integer(n)); } return Some(field.parse::().ok()?.into()); } - let n = Number::float(field.parse::().ok()?, self.width); + let n = Number::float(field.parse::().ok()?, self.numeric.width); if n.is_infinite() && !matches!(digits.to_ascii_lowercase().as_str(), "inf" | "infinity") { return None; } Some(n) } } /// `•csv`: parse CSV text into a keyed vector of columns. -pub(crate) fn parse(left: Option<&Value>, right: &Value, _: &Span) -> Result { import(&string(right)?, &Options::new(left, true)?) } +pub(crate) fn parse(HostCall { left, right, numeric, .. }: HostCall<'_>) -> Result { import(&string(right)?, &Options::new(left, true, numeric)?) } /// `•csv⁻¹`: CSV text for a vector of columns. -pub(crate) fn serialize(left: Option<&Value>, right: &Value, _: &Span) -> Result { export(right, &Options::new(left, false)?) } +pub(crate) fn serialize(HostCall { left, right, numeric, .. }: HostCall<'_>) -> Result { export(right, &Options::new(left, false, numeric)?) } fn import(source: &str, opts: &Options) -> Result { let header = opts.common.boolean("header", true)?; - let fill = opts.common.fill()?; + let fill = opts.common.fill(opts.numeric)?; let mut missing = vec![String::new()]; if let Some(v) = opts.common.values.get("missing") { if let Some(s) = keyed::name(v) { missing.push(s.to_string()); } else { for s in v.elements() { missing.push(string(&s)?); } } @@ -138,10 +136,10 @@ fn import(source: &str, opts: &Options) -> Result { } crate::data::typed_column( "•csv", &name, crate::data::imported(vec![cells.len()], numbers, |i| absent[i], Value::integer(0)).described("invalid CSV column")?, - typed.as_ref(), absent.contains(&true))? + typed.as_ref(), absent.contains(&true), opts.numeric)? } else { - let mut texts = keyed::Texts::default(); - keyed::texts(&[cells.len()], cells.iter().zip(&absent).map(|(s, &a)| texts.text(if a { "" } else { s })).collect()).described("invalid CSV column")? + let column: Vec<&str> = cells.iter().zip(&absent).map(|(s, &a)| if a { "" } else { s.as_str() }).collect(); + keyed::text_column(&[cells.len()], &column).described("invalid CSV column")? }; result.push(column); } @@ -157,7 +155,7 @@ fn export(table: &Value, opts: &Options) -> Result { let rows = columns.first().map_or(0, Value::len); if columns.iter().any(|c| c.len() != rows) { return Err(Error::new(Kind::Length, "CSV columns must have equal lengths")); } let header = opts.common.boolean("header", table.keys(0).is_some())?; - let fill = opts.common.fill()?; + let fill = opts.common.fill(opts.numeric)?; let force = match opts.common.values.get("forcequotes") { None => false, Some(v) => match v.as_number().and_then(|n| n.integer().ok()) { diff --git a/src/data.rs b/src/data.rs index cc64455e..7a4e3eaf 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,11 +1,12 @@ use crate::execution::check; use crate::session; -use crate::{Described, Error, ErrorKind, Number, Span, Value, array::Storage, keyed, number::Numeric, primitive::{numeric, real}}; +use crate::{Described, Error, ErrorKind, Number, Span, Value, keyed, number::Numeric, system::HostCall, primitive::{numeric, real}}; use rustymath::hashing::{HashMap, HashMapExt}; use rustymath::{buffer::{Buffer, ComplexBuf, DType, Element, FloatBuf, FloatWidth, Ints, View, Width}, error::Kind}; use std::io::Read; pub(crate) fn text(value: &Value) -> Result { keyed::name(value).map(|s| s.to_string()).ok_or_else(|| Error::domain("expected text")) } +pub(crate) fn real_items(value: &Value) -> Result, Error> { value.elements().map(|item| real(&item)).collect() } /// Imported items, where `filled` marks missing-value fills. Where every other number is an ordinary integer, every one is a fixed integer of one width, or every one is a Boolean, a fill takes that type wherever it keeps its value there, as a fill under `⍠` does (`Value::fill_of`): a fill of `0` beside integers gives `0ₓ`. Approximate inputs, and a fill that doesn't keep its value in that type, such as NaN or `0.5` beside integers, select real or complex storage at the widest strong approximate width, or the widest weak width when none is strong. Weak approximate values may round; exact values must convert losslessly. Otherwise items keep their own kinds. pub(crate) fn imported(shape: Vec, mut items: Vec, filled: impl Fn(usize) -> bool, empty_prototype: Value) -> Result { @@ -44,7 +45,7 @@ pub(crate) fn imported(shape: Vec, mut items: Vec, filled: impl Fn }).collect::>>(); if let Some(values) = values { return Value::complex(shape, ComplexBuf::collect(width.into(), values.into_iter())); } } else if let Some(floats) = items.iter().map(|v| number(v).and_then(exactly)).collect::>>() { - return Value::from_storage(shape, Storage::Compact(Buffer::Floats(FloatBuf::collect(width, floats.into_iter())))); + return Value::from_buffer(shape, Buffer::Floats(FloatBuf::collect(width, floats.into_iter()))); } } Value::from_parts(shape, items, empty_prototype) @@ -86,9 +87,9 @@ impl Options { } } - pub fn fill(&self) -> Result { + pub fn fill(&self, numeric: Numeric) -> Result { match self.values.get("fill") { - None => Ok(Numeric::current().float(f64::NAN)), Some(Value::Number(n)) if n.as_complex().is_none() => Ok(n.clone()), + None => Ok(numeric.float(f64::NAN)), Some(Value::Number(n)) if n.as_complex().is_none() => Ok(n.clone()), _ => Err(Error::domain(format!("{} fill must be a real number", self.name))) } } @@ -114,8 +115,7 @@ impl Options { pub(crate) enum Types { Named(HashMap), Ordered(Vec) } /// `column`, a column of numbers that `reader` read, named `name`, at the type it takes. A column that the `types` option names takes the type of `typed`, its entry there: an integer of the `int` width under static kinds where `typed` is an ordinary integer. Under static kinds, a column of integers that the option doesn't name takes the `int` width, as every new integer does, so that its type never depends on its values. A value that doesn't keep itself in that type is a DOMAIN error that names the column and the `types` option, and the fill where `missing` says that the column had missing cells. Any other column keeps the type that its numbers gave it. -pub(crate) fn typed_column(reader: &str, name: &str, column: Value, typed: Option<&Value>, missing: bool) -> Result { - let numeric = Numeric::current(); +pub(crate) fn typed_column(reader: &str, name: &str, column: Value, typed: Option<&Value>, missing: bool, numeric: Numeric) -> Result { let int = if numeric.is_static() { numeric.int } else { Width::I64 }; let integers = || match column.dtype() { Some(DType::Integer(_)) => true, @@ -147,17 +147,15 @@ pub(crate) fn typed_column(reader: &str, name: &str, column: Value, typed: Optio b.extend_from(Buffer::kept(n.view()?, dtype).ok()?.view(), 0..1).then_some(b) }), }.ok_or_else(lost)?; - let storage = if fixed || numeric.is_static() || !matches!(dtype, DType::Integer(_)) { Storage::Compact(buffer).fixed(fixed) } - else { Storage::narrowed(i64::of(buffer.view()).expect("integers at 64 bits").to_vec()) }; - Value::from_storage([column.len()], storage).described("invalid column") + let width = match (fixed, dtype) { (true, DType::Integer(width)) => Some(width), _ => None }; + Value::from_buffer_at([column.len()], buffer, width).described("invalid column") } -pub(crate) fn vfi(left: Option<&Value>, right: &Value, _: &Span) -> Result { +pub(crate) fn vfi(HostCall { left, right, numeric, .. }: HostCall<'_>) -> Result { let input = text(right)?; let separators = left.map(|a| text(a)).transpose()?; let mut valid = Vec::new(); let mut numbers = Vec::new(); - let numeric = Numeric::current(); let zero = numeric.float(0.0); let mut field = |s: &str| -> Result<(), Error> { check()?; @@ -186,7 +184,7 @@ fn file_options(name: &'static str, left: Option<&Value>, shorthand: Option<&str /// `data` as a vector of byte values, one byte each. pub(crate) fn byte_vector(data: Vec) -> Result { - Value::from_storage([data.len()], Storage::within(data, crate::array::ordinary_width(Width::U8))) + Value::from_buffer([data.len()], Buffer::Integers(rustymath::buffer::IntBuf::U8(data))) } /// The bytes in an array of integers from 0 to 255, in ravel order. `None` for any other array. @@ -222,11 +220,11 @@ fn flate(left: Option<&Value>, right: &Value, compress: bool) -> Result, right: &Value, _: &Span) -> Result { flate(left, right, false) } -pub(crate) fn deflate(left: Option<&Value>, right: &Value, _: &Span) -> Result { flate(left, right, true) } +pub(crate) fn inflate(HostCall { left, right, .. }: HostCall<'_>) -> Result { flate(left, right, false) } +pub(crate) fn deflate(HostCall { left, right, .. }: HostCall<'_>) -> Result { flate(left, right, true) } /// `•hash` gives the SHA-2 digest of bytes or text in hex, by `"sha224"`, `"sha256"`, `"sha384"` or `"sha512"`, SHA-256 by default. -pub(crate) fn hash(left: Option<&Value>, right: &Value, _: &Span) -> Result { +pub(crate) fn hash(HostCall { left, right, .. }: HostCall<'_>) -> Result { use sha2::{Digest, Sha224, Sha256, Sha384, Sha512}; let data = payload(right)?; let digest = match algorithm(left, "sha256")?.as_str() { @@ -237,7 +235,7 @@ pub(crate) fn hash(left: Option<&Value>, right: &Value, _: &Span) -> Result, right: &Value, _: &Span) -> Result { +pub(crate) fn uuid(HostCall { left, right, .. }: HostCall<'_>) -> Result { use uuid::Uuid; // The standard allows a random node in place of a MAC address, with the multicast bit set. let node = || { @@ -246,8 +244,6 @@ pub(crate) fn uuid(left: Option<&Value>, right: &Value, _: &Span) -> Result Uuid::nil(), (1, None) => Uuid::now_v1(&node()), @@ -269,13 +265,12 @@ pub(crate) fn uuid(left: Option<&Value>, right: &Value, _: &Span) -> Result, right: &Value, span: &Span) -> Result { +pub(crate) fn read(HostCall { left, right, site: span, .. }: HostCall<'_>) -> Result { let opts = file_options("•nget", left, None, &["encoding", "binary"])?; let binary = opts.boolean("binary", false)?; if binary && opts.values.contains_key("encoding") { return Err(Error::domain("binary files do not take an encoding")); } let path = text(right)?; check()?; - crate::transform::acts()?; let data = if path == "-" { session().execution.rest(span)? } else { span.read(&path)? }; check()?; if binary { return byte_vector(data).described("invalid byte vector"); } @@ -284,7 +279,7 @@ pub(crate) fn read(left: Option<&Value>, right: &Value, span: &Span) -> Result, right: &Value, span: &Span) -> Result<(Value, bool), Error> { +pub(crate) fn write(HostCall { left, right, site: span, .. }: HostCall<'_>) -> Result<(Value, bool), Error> { let opts = file_options("•nput", left, Some("path"), &["path", "encoding", "overwrite", "unique", "prefix"])?; let path = opts.text("path", None)?; let data = payload(right)?; @@ -328,7 +323,7 @@ pub(crate) struct Request { pub url: String, pub method: Option, pub hea pub(crate) struct Response { pub status: i64, pub headers: Vec<(String, String)>, pub body: Vec } /// `X •fetch url` requests `url` and returns a record of the response's `status`, `headers` and `body`. Any response is a result, whatever its status. Getting no response is an IO error. -pub(crate) fn fetch(left: Option<&Value>, right: &Value, _: &Span) -> Result { +pub(crate) fn fetch(HostCall { left, right, .. }: HostCall<'_>) -> Result { let opts = Options::new("•fetch", left, None, &["method", "headers", "body", "binary"])?; let url = text(right)?; let method = opts.values.get("method").map(|m| text(m)).transpose()?; @@ -337,7 +332,6 @@ pub(crate) fn fetch(left: Option<&Value>, right: &Value, _: &Span) -> Result>()?, None => Vec::new() }; let body = opts.values.get("body").map(|b| payload(b)).transpose()?; - crate::transform::acts()?; let response = crate::host::fetch(Request { url: url.clone(), method, headers, body })?; let body = response.body; let body = if opts.boolean("binary", false)? { byte_vector(body).described("invalid byte vector")? } diff --git a/src/date.rs b/src/date.rs index 1f748dc7..1b93ae77 100644 --- a/src/date.rs +++ b/src/date.rs @@ -1,5 +1,6 @@ //! `•date` reads a moment, in seconds since the Unix epoch, from text or from a record of fields. `•date⁻¹` writes a moment as a record, or as text with a pattern. Patterns, field names and ISO 8601 text follow chrono. -use crate::{Described, Error, Number, Span, Value, data::{Options, text}, keyed, primitive::real}; +use crate::system::HostCall; +use crate::{Described, Error, Number, Value, data::{Options, text}, keyed, primitive::real}; use chrono::{DateTime, Datelike, FixedOffset, Local, NaiveDate, NaiveDateTime, NaiveTime, Offset, TimeZone, Timelike, Utc}; use rustymath::buffer::FloatWidth; use std::fmt::Write; @@ -65,7 +66,7 @@ fn date_time(fields: &[i64]) -> Option { } /// `•date Y`: the moment that the text `Y` names, or one for each text in `Y`, or one for each position of a record of fields. A pattern, or the `pattern` and `zone` options, go on the left. -pub(crate) fn read(left: Option<&Value>, right: &Value, _: &Span) -> Result { +pub(crate) fn read(HostCall { left, right, .. }: HostCall<'_>) -> Result { let opts = Options::new("•date", left, Some("pattern"), &["pattern", "zone"])?; let zone = Zone::new(&opts)?; let pattern = opts.values.get("pattern").map(|p| text(p)).transpose()?; @@ -100,7 +101,7 @@ pub(crate) fn read(left: Option<&Value>, right: &Value, _: &Span) -> Result, right: &Value, _: &Span) -> Result { +pub(crate) fn write(HostCall { left, right, .. }: HostCall<'_>) -> Result { let opts = Options::new("•date⁻¹", left, Some("pattern"), WRITE_OPTIONS)?; let zone = Zone::new(&opts)?; #[cfg(not(web))] diff --git a/src/derivative.rs b/src/derivative.rs index 43b91d3b..636f0430 100644 --- a/src/derivative.rs +++ b/src/derivative.rs @@ -1,1096 +1,364 @@ -//! `∂`. With one number in `x`, `f∂ x` runs `f` once in forward mode, with `x` seeded along that number, and the result's tangent is the derivative. With more, it linearises `f` once (`linearized`), which records its linear calls (`linear`), then transposes the record once for each item of the result, or evaluates it once for each number in `x`, whichever is fewer. The Jacobian is laid out as `(f x)×⊗x`. A unit result's Jacobian is its gradient, with the argument's structure. -//! -//! A value that `∂` is differentiating is a dual: a tag, a primal and a tangent of the primal's shape. A dual of a number is a carried atom (`Value::Carried`). Each `∂` call takes a fresh tag, larger than the tags of the calls it runs inside, so a value active at two levels is a dual whose primal is a dual with a smaller tag. A rule applies at the largest tag among its arguments, where an argument without that tag is a constant. -//! -//! Primitive dispatch sends each call with a dual argument to `call`, which reads how to run it from its form's row in `Primitive::info()` (`primitive::Facts`). Pervasive forms take their tangents from rustymath's partial derivatives (`rustymath::derivative`): the function object that the kernel table chooses for a call gives its recipe, which `partials` runs as primitive calls on the primals and the primal result. A running trace records each call as a step. Rules call primitives, so partials of duals with smaller tags are themselves differentiated. Tangent rules use only `scale`, `add` and `expand`, and other rules only move a tangent's items or apply linear operations to it, so that reverse mode can supply symbolic versions of them. -use crate::array::{Footprint, Seen}; -use crate::session; +//! Forward linearization under Derivative levels; reverse mode transposes the same graph. + use crate::{ - Error, ErrorKind, Function, Span, Value, agreement::{Agree, Agreement, expand}, array::{Gather, Layout, Promotion, Prototype, Widening}, error::Described, - number::{Arithmetic, Math}, primitive::{Comparison, FoldKind, Form, Operation, Origin, Primitive}, - transform::{Carrier, Cause, Mark, Op, Pairing, Part, Stop, walk} + array::{self, Carried}, error::Result, number::{Arithmetic, Mark}, + primitive::{JvpOutputs, Linearity, Operation, Pairing, Primitive, RecipeOp, Setting}, program::{Origin, Outputs}, + transform::{carried, Level}, Error, ErrorKind, Function, Value, }; -use rustymath::{buffer::{DType, FloatWidth}, recipe::{Binary, Cmp, Unary}}; - -/// A value, atom or array, that `∂` is differentiating: the tag of the `∂` call, the primal, and the tangent, which agrees with the primal's layout. Either part can be pending. `items` holds an array's items as dual atoms, built on the first read by code with no rule for duals (`Dual::storage`). -pub struct Dual { - pub(crate) tag: u64, - pub(crate) primal: Value, - /// The tangent, which agrees with the primal under leading-axis agreement: of the primal's layout, or smaller where a rule's argument was. - pub(crate) tangent: Value, - pub(crate) items: std::sync::OnceLock, - full: std::sync::OnceLock, -} - -impl Dual { - /// A dual at level `tag`. Its parts carry only older levels' data. - pub(crate) fn new(tag: u64, primal: Value, tangent: Value) -> Self { - assert!(primal.older_than(tag) && tangent.older_than(tag), "a dual's parts carry only older levels' data"); - Self { tag, primal, tangent, items: std::sync::OnceLock::new(), full: std::sync::OnceLock::new() } - } - /// The tangent in the primal's layout, expanded once where the stored tangent is smaller. - pub(crate) fn full_tangent(&self) -> &Value { - if self.tangent.layout() == self.primal.layout() { return &self.tangent; } - self.full.get_or_init(|| expand(&self.tangent, self.primal.layout())) - } - /// This dual with `f` applied to its primal and to its tangent in the primal's layout, rebuilt at the same level. - pub(crate) fn map(&self, mut f: impl FnMut(&Value) -> Result) -> Result { - Ok(dual(self.tag, f(&self.primal)?, f(self.full_tangent())?)) - } - /// Item `i`: the dual of the parts' items. - pub(crate) fn at(&self, i: usize) -> Value { let Ok(item) = self.map(|part| Ok::<_, std::convert::Infallible>(part.at(i))); item } - /// Item `i` as bulk iteration reads it (`Carrier::element`): the dual of the parts' items, each read as bulk iteration reads it. - pub(crate) fn element(&self, i: usize) -> Value { let Ok(item) = self.map(|part| Ok::<_, std::convert::Infallible>(part.element(i))); item } - /// The items as dual atoms, built on the first read by code with no rule for duals, which records an `itemwise` row in `•explain`. Cold and out of line, so that `ArrayData::storage`'s other arms stay small. - #[cold] - #[inline(never)] - pub(crate) fn storage(&self) -> &crate::array::Storage { - crate::transform::Carrier::built(&self.items, rustymath::events::Route::DerivativeItems, self.primal.len(), |i| self.element(i)) - } -} +use rustymath::buffer::DType; -impl Footprint for Dual { - /// The bytes of the primal, the tangent and the items built from them. - fn footprint_in(&self, seen: &mut Seen) -> usize { - self.primal.footprint_in(seen) + self.tangent.footprint_in(seen) + self.items.footprint_in(seen) + self.full.footprint_in(seen) - } +pub(crate) fn dual(level: usize, primal: Value, tangent: Value) -> Value { + carried(level, Carried::Dual { primal, tangent }) } -impl std::fmt::Debug for Dual { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Dual").field("tag", &self.tag).field("shape", &self.primal.shape()).finish() - } -} - -/// The tag of `value`'s outermost dual, or `None` for a value that isn't a dual. An array whose items are dual atoms isn't a dual itself. -pub(crate) fn tag(value: &Value) -> Option { dual_of(value).map(|dual| dual.tag) } - -/// `value`'s outermost dual, atom or array, or `None` for a value that isn't a dual. -pub(crate) fn dual_of(value: &Value) -> Option<&Dual> { value.carrier().and_then(Carrier::dual).map(std::sync::Arc::as_ref) } - -/// `value` at level `tag`: its primal, and its tangent in the primal's layout. A value without that tag is a constant at this level, with no tangent. -pub(crate) fn split(value: &Value, tag: u64) -> (Value, Option) { parts(value, tag, true) } - -/// `split`, keeping a dual's tangent as small as its rule left it, for pervasive rules, whose calls broadcast it. -fn split_raw(value: &Value, tag: u64) -> (Value, Option) { parts(value, tag, false) } - -/// `value`'s primal and tangent at level `tag`, with the tangent in the primal's layout where `full`. An array that holds that level's duals inside it, at any depth, is active, and packs into one dual first (`packed`). Code that reads a dual array's items one at a time builds such arrays. -fn parts(value: &Value, tag: u64, full: bool) -> (Value, Option) { - let repacked = (value.holds_values() && value.newest() == Some(Mark::Dual(tag))).then(|| packed(value, tag)); - match dual_of(repacked.as_ref().unwrap_or(value)) { - Some(dual) if dual.tag == tag => (dual.primal.clone(), Some(if full { dual.full_tangent() } else { &dual.tangent }.clone())), - // A batch whose whole is a dual at this level, as an attached rule in a batched run meets it, splits into batches. - _ => crate::batch::split_whole(value, tag).map_or_else(|| (value.clone(), None), |(primal, tangent)| (primal, Some(tangent))), - } -} - -/// The number of `∂` levels whose tags `value` carries, through its duals' primals and tangents and inside nested arrays. It counts no tags inside records. A record's coefficients belong to the dual that holds the record as its tangent. -pub(crate) fn levels(value: &Value) -> usize { - fn collect(value: &Value, tags: &mut Vec) { - let parts = dual_of(value).map(|dual| (dual.tag, dual.primal.clone(), dual.tangent.clone())); - if let Some((tag, primal, tangent)) = parts { - if !tags.contains(&tag) { tags.push(tag); } - collect(&primal, tags); - collect(&tangent, tags); - } else if value.holds_values() { value.elements().for_each(|e| collect(&e, tags)); } - } - let mut tags = Vec::new(); - collect(value, &mut tags); - tags.len() -} - -/// The error for the system function `name` called on a dual, which `∂` can't differentiate. -#[cold] -#[inline(never)] -pub(crate) fn system(name: &str) -> Error { Error::new(ErrorKind::Unsupported, format!("∂ can't differentiate {name}")) } - -/// The error for a call with two arguments of a function that has a rule that `f∂←g` attached, where `∂` needs a derivative through the call. A rule describes a function of one argument. -#[cold] -#[inline(never)] -pub(crate) fn two_arguments() -> Error { - Error::domain("∂ can't use an attached derivative in a call with two arguments. Bind the left argument first, as in bound←10↣f ⋄ bound∂←g") -} - -/// Whether `value`'s numbers are complex, which `∂` doesn't differentiate. A carried value answers from the value its carrier holds (`Carrier::template`): a dual's primal, or a batch's first cell, which has the kind of every cell. -pub(crate) fn complex(value: &Value) -> bool { - if let Some(carrier) = value.carrier() { return complex(carrier.template()); } - match value { Value::Number(n) => n.as_complex().is_some(), Value::Array(_) => matches!(value.dtype(), Some(DType::Complex(_))), _ => false } -} - -/// `split`, looking inside nested arrays: the primal and the tangent keep `value`'s structure, with tangents of 0 for the parts without the tag. -pub(crate) fn split_deep(value: &Value, tag: u64) -> (Value, Option) { - let mut found = false; - let primal = walk(value, &mut |leaf| { - let (primal, tangent) = split(leaf, tag); - found |= tangent.is_some(); - Ok(primal) - }); - if !found { return (value.clone(), None); } - let tangent = walk(value, &mut |leaf| Ok(split(leaf, tag).1.unwrap_or_else(|| zeros(leaf)))); - (primal.expect("splitting can't fail"), Some(tangent.expect("splitting can't fail"))) -} - -/// The dual of `primal` and `tangent` at level `tag`: a carried atom for a number, and a dual array otherwise. -pub(crate) fn dual(tag: u64, primal: Value, tangent: Value) -> Value { - // A part that is a batch newer than `tag`, as an attached rule in a batched run gives it, makes a batch of duals. - if let Some(batched) = crate::batch::dual_of_wholes(tag, &primal, &tangent) { return batched; } - if matches!(primal, Value::Number(_) | Value::Carried(_)) - && (tangent.is_atom() || crate::linear::record_of(&tangent).is_some_and(|r| r.layout.shape().is_empty())) { - return Value::Carried(Carrier::Dual(std::sync::Arc::new(Dual::new(tag, primal, tangent)))); - } - Value::from_dual(tag, primal, tangent) -} - -/// `value` with level `tag` taken off: each dual at that tag replaced by its primal. Duals with other tags stay. -pub(crate) fn strip(value: &Value, tag: u64) -> Value { walk(value, &mut |leaf| Ok(split(leaf, tag).0)).expect("stripping can't fail") } - -/// `value` with each dual replaced by its primal, at every tag, as `•detach` gives it. -pub(crate) fn detach(value: &Value) -> Value { - walk(value, &mut |leaf| { - Ok(match tag(leaf) { Some(t) => detach(&split(leaf, t).0), None => leaf.clone() }) - }).expect("detaching can't fail") -} - -/// `value` with its numbers converted to `to`, as `width•float` and width marks convert them to floats, and fixed marks to fixed integers (`Value::converted`). A pending array converts as one more pending call (`fusion::convert`). A weak number becomes strong, as a literal with a width marker is. Each carrier converts through `Carrier::cast`. -pub(crate) fn converted(value: &Value, to: DType, span: &Span) -> Result { - let numeric = crate::number::Numeric::current(); - if !value.carried() { return plain(value, to, numeric); } - walk(value, &mut |leaf| match leaf.carrier() { Some(carrier) => carrier.cast(to, span), None => plain(leaf, to, numeric) }) -} - -/// `converted` for a value that carries no level. The conversion is one more pending call where `fusion::convert` defers it, and runs at once otherwise. It's always inlined, because the compiler otherwise calls it out of line from both of its callers, which costs a mark about 20 instructions. -#[inline(always)] -fn plain(v: &Value, to: DType, numeric: crate::number::Numeric) -> Result { - match crate::fusion::convert(v, to, numeric) { - Some(pending) => Ok(pending), - None => v.converted(to, numeric).described(if matches!(to, DType::Integer(_)) { "can't convert result to fixed integers" } - else { "•float takes numbers" }), +fn visit_duals(value: &Value, visit: &mut impl FnMut(usize)) { + match value { + Value::Carried { level, carried } => match &**carried { + Carried::Dual { primal, tangent } => { visit(*level); visit_duals(primal, visit); visit_duals(tangent, visit); } + Carried::Batch { whole, .. } => visit_duals(whole, visit), + }, + Value::Array(array) => match &**array { + array::ArrayData::Mixed { items, prototype, .. } => for item in items.iter().chain(prototype.iter()) { visit_duals(item, visit); }, + array::ArrayData::Segmented { segments, .. } => visit_duals(segments.prototype(), visit), + _ => {} + }, + _ => {} } } -/// Zeros in `like`'s layout, in the kind of its plain items. -pub(crate) fn zeros(like: &Value) -> Value { let zero = like.prototype(); if like.is_atom() { zero } else { like.layout().filled(&zero) } } - -/// Writes `source` into `target` with `write` at level `tag`, as `transform::written` does for a dual: the primal into the primal and the tangent into the tangent. A side without the level has tangents of 0. An error from `write` leaves `target` unchanged. -pub(crate) fn written(target: &mut Value, source: &Value, tag: u64, write: impl Fn(&mut Value, &Value) -> Result<(), E>) -> Result<(), E> { - let (mut primal, tangent) = split(target, tag); - let (items, tangents) = split(source, tag); - let mut tangent = tangent.unwrap_or_else(|| zeros(&primal)); - write(&mut tangent, &tangents.unwrap_or_else(|| zeros(&items)))?; - write(&mut primal, &items)?; - *target = dual(tag, primal, tangent); - Ok(()) +pub(crate) fn tag(value: &Value) -> Option { + let mut latest = None; visit_duals(value, &mut |id| latest = latest.max(Some(id))); latest } -/// The duals that a `Gather` copies for the `∂` call tagged `tag`: their primals and their tangents, each in a buffer of its own. Where the tangents are records (`linear`), the tangent buffer holds the ids that `records` gives them. `finish` gives the dual array. -pub(crate) struct Join { tag: u64, primal: Gather, tangent: Gather, records: Option } - -impl Join { - /// A join for copying `sources`, at the largest tag among them, or `None` where none is a dual. An inactive source's tangents are zeros of its own dtype. - pub(crate) fn of(sources: &[&Value], capacity: usize, widening: Widening) -> Option { - let tag = sources.iter().filter_map(|v| tag(v)).max()?; - let parts: Vec<(Value, Option)> = sources.iter().map(|v| split(v, tag)).collect(); - let primals: Vec<&Value> = parts.iter().map(|(p, _)| p).collect(); - let tangents: Vec<&Value> = parts.iter().map(|(p, t)| t.as_ref().unwrap_or(p)).collect(); - // A record tangent copies as its ids, into a buffer of integers. - let first = parts.iter().find_map(|(_, t)| t.as_ref().and_then(crate::linear::record_of)); - let tangent = if first.is_some() { Gather::items(capacity) } else { Gather::with_widening(&tangents, capacity, widening) }; - Some(Self { tag, primal: Gather::with_widening(&primals, capacity, widening), tangent, records: first.map(crate::linear::Sources::meeting) }) - } - /// A join at tag `tag` whose primals are what `held` holds, with tangents of 0 (`transform::Join::enter`). - pub(crate) fn over(tag: u64, held: Gather) -> Self { Self { tag, tangent: held.zeros(held.len()), primal: held, records: None } } - /// The tag of the `∂` call whose duals this join copies. - pub(crate) fn tag(&self) -> u64 { self.tag } - /// The number of items copied so far. - pub(crate) fn len(&self) -> usize { self.primal.len() } - /// Runs the copy `op` on the primals with `source`'s primal, and on the tangents with `source`'s tangent, with the part that each copy writes. An inactive source appends tangents of 0, or, for a copy that `replaces` items, writes 0 where it writes. - pub(crate) fn each(&mut self, source: &Value, replaces: bool, op: impl Fn(&mut Gather, &Value, Part)) { - let (primal, tangent) = split(source, self.tag); - let before = self.primal.len(); - op(&mut self.primal, &primal, Part::Values); - match tangent { - // A buffer of record ids takes an inactive tangent as fill ids. - Some(tangent) if crate::linear::record_of(&tangent).is_some() || self.records.is_some() => { - let ids = self.records.get_or_insert_with(Default::default).of(Some(&tangent), &tangent); - op(&mut self.tangent, &ids, Part::Derived) - } - Some(tangent) => op(&mut self.tangent, &tangent, Part::Derived), - None if replaces => op(&mut self.tangent, &Value::integer(0), Part::Derived), - None => { let n = self.primal.len() - before; self.tangent.fill(&Value::integer(0), n) } +pub(crate) fn split(value: &Value, level: usize) -> (Value, Option) { + let Value::Carried { level: outer, carried: parts } = value else { return (value.clone(), None); }; + match &**parts { + Carried::Dual { primal, tangent } if *outer == level => (primal.clone(), Some(tangent.clone())), + Carried::Batch { whole, cell } => { + let (primal, tangent) = split(whole, level); + if tangent.is_none() { return (value.clone(), None); } + let wrap = |whole| carried(*outer, Carried::Batch { whole, cell: cell.clone() }); + (wrap(primal), tangent.map(wrap)) } - } - /// Appends `item`. An array whose tangent is a record joins as one item, the record itself, which keeps its layout (`linear::Sources::retained`). - pub(crate) fn add(&mut self, item: Value) { - let (primal, tangent) = split(&item, self.tag); - if let Some(record) = tangent.as_ref().and_then(crate::linear::record_of).filter(|r| !r.layout.shape().is_empty()) { - let kept = self.records.get_or_insert_with(Default::default).retained(record); - self.tangent.mixed().push(kept); - return self.primal.add(primal); + Carried::Dual { primal, tangent } => { + let (p, dp) = split(primal, level); let (t, dt) = split(tangent, level); + if dp.is_none() && dt.is_none() { return (value.clone(), None); } + let direction = dual(*outer, dp.unwrap_or_else(|| crate::linear::zeros_like(&p)), dt.unwrap_or_else(|| crate::linear::zeros_like(&t))); + (dual(*outer, p, t), Some(direction)) } - self.each(&item, false, |g, s, _| g.add(s.clone())) - } - /// The dual array gathered, laid out by `layout`. An empty array takes `prototype`. - pub(crate) fn finish(self, layout: Layout, prototype: impl Prototype) -> Result { - let primal = self.primal.finish(layout.clone(), prototype)?; - let tangent = self.tangent.finish(layout, Value::integer(0))?; - let tangent = match self.records { Some(records) => records.assembled(tangent, None), None => tangent }; - Ok(dual(self.tag, primal, tangent)) - } -} - -/// The numbers that `∂` differentiates with respect to in a leaf of `walk`: 1 for a number or a carried atom that holds one, and one per item for a dual array or a simple numeric array. -fn numbers(leaf: &Value) -> Result { - match leaf { - Value::Number(_) => Ok(1), - // A batch of characters gets DOMAIN, as each of its cells would. - Value::Carried(_) if matches!(leaf.template(), Value::Number(_)) => Ok(1), - // A dual array holds numbers whatever its primal's storage. An outer `∂` joins dual atoms into one, whose primal is mixed where they hold rationals or mix exact and approximate numbers. - Value::Array(_) if dual_of(leaf).is_some() || leaf.template_dtype().is_some_and(|k| k.exact().is_some()) => Ok(leaf.len()), - // `∂` has no rule for a batch whose cells hold nested arrays. Its run stops, and its map calls the operand once for each cell. - _ => match leaf.carrier() { - Some(Carrier::Batch(b)) if !leaf.is_atom() => Err(Stop::new(b.level, Cause::Rule).into()), - _ => Err(Error::domain("∂ differentiates with respect to numbers")), - }, } } -/// The number of numbers in `value`, each of which `∂` differentiates with respect to. -fn leaves(value: &Value) -> Result { - let mut count = 0; - crate::transform::visit(value, &mut |leaf| { count += numbers(leaf)?; Ok(()) })?; - Ok(count) -} - -/// `value` with each number made a dual at level `tag`, with tangent 1 at the `j`th number and 0 elsewhere, in that number's kind. A simple numeric array becomes one array dual. -fn seed(value: &Value, tag: u64, j: usize) -> Result { - let mut k = 0; - walk(value, &mut |leaf| { - let n = numbers(leaf)?; - if complex(leaf) { return Err(Error::new(ErrorKind::Unsupported, "∂ can't differentiate with respect to complex numbers")); } - Ok(dual(tag, leaf.clone(), direction(leaf, n, &mut k, j))) - }) -} - -/// Zeros in `like`'s layout and kind, with a 1 at item `i`. The kind is that of `like`'s prototype, or of its template where a level carries the prototype, as it carries an item that a trace read one at a time. -fn unit(like: &Value, i: usize) -> Value { - let Some(zero) = (if like.is_atom() { like.template() } else { like.prototype().template() }).as_number() else { - unreachable!("a numeric value has a number prototype") - }; - let one = Value::Number(zero.one()); - if like.is_atom() { return one; } - let mut e = zeros(like); - e.scatter(&[i], &one).expect("plain zeros take a plain 1"); - e -} - -/// A leaf's tangent in the pass along number `j` of `x`: 1 at that number and 0 elsewhere. The leaf holds `n` numbers, after the `k` numbers before it. `k` moves past the leaf. -fn direction(leaf: &Value, n: usize, k: &mut usize, j: usize) -> Value { - let tangent = if (*k..*k + n).contains(&j) { unit(leaf, j - *k) } else { zeros(leaf) }; - *k += n; - tangent -} - -/// The tangent of `value` at level `tag`, in `value`'s structure: 0 for a number without that tag. -fn tangent(value: &Value, tag: u64) -> Result { - walk(value, &mut |leaf| match (leaf, dual_of(leaf)) { - (_, Some(dual)) if dual.tag == tag => Ok(dual.full_tangent().clone()), (Value::Number(_) | Value::Carried(_), _) => Ok(crate::linear::zero_of(leaf)), - (Value::Array(_), _) => Ok(zeros(leaf)), _ => Err(Error::domain("∂ needs a numeric result")) - }) -} +use crate::array::map_leaves; -/// `f∂ right`, or `left f∂ right`: the Jacobian of `f` at `right`, with the layout of `(f right)×⊗right`. `left` passes to `f` unchanged. `f` runs on `right` with its integers that wrap, under static kinds or at a fixed width, as floats of the width that their division gives (`plan::Kind::approximate`). -pub(crate) fn differentiate(f: &Function, left: Option<&Value>, right: &Value, span: &Span) -> Result { - let numeric = crate::number::Numeric::current(); - let right = &walk(right, &mut |leaf| match crate::plan::Kind::approximate([crate::plan::Kind::value(leaf)], numeric).map_err(Error::domain)? { - Some(width) => converted(leaf, DType::Float(width), span), None => Ok(leaf.clone()) +pub(crate) fn split_deep(value: &Value, level: usize) -> Result<(Value, Option)> { + let mut active = false; + let primal = map_leaves(value, |leaf| { + let (primal, tangent) = split(leaf, level); + active |= tangent.is_some(); + Ok(primal) })?; - if let Some(result) = f.programmed(left, right, span, &|g, x, y| derived(g, x, y, span)) { return result; } - derived(f, left, right, span) -} - -/// `differentiate`'s Jacobian once `right`'s integers that wrap are floats: by forward mode where `right` holds at most one number, and by reverse mode otherwise. A dfn's first `∂` with given argument types and layouts runs this on traced values, which builds the program that later calls replay (`program::call`). -fn derived(f: &Function, left: Option<&Value>, right: &Value, span: &Span) -> Result { - let n = leaves(right)?; - if n > 1 { return reverse(f, left, right, n, span); } - let tag = session().begin(crate::transform::Kind::Derivative); - let passes = pass(f, left, right, tag, n, span); - session().end(); - let (primal, columns) = passes?; - jacobian(right, &primal, columns) + if !active { return Ok((primal, None)); } + let tangent = map_leaves(value, |leaf| { + let (primal, tangent) = split(leaf, level); + Ok(tangent.unwrap_or_else(|| crate::linear::zeros_like(&primal))) + })?; + Ok((primal, Some(tangent))) } -/// The result of `f` without tangents, and the tangent from one forward pass when `x` holds one number. `n` is 0 or 1: `reverse` handles more. -fn pass(f: &Function, left: Option<&Value>, right: &Value, tag: u64, n: usize, span: &Span) -> Result<(Value, Vec), Error> { - if n == 0 { return Ok((f.call_array(left, right, span)?, Vec::new())); } - let result = f.call_array(left, &seed(right, tag, 0)?, span)?; - Ok((strip(&result, tag), vec![tangent(&result, tag)?])) +pub(crate) fn strip(value: &Value, level: usize) -> Value { + map_leaves(value, |leaf| Ok(split(leaf, level).0)).expect("stripping reads no data") } -/// `x` made a dual at level `tag` with tangent `v`, item by item through nested arrays. -fn along(x: &Value, v: &Value, tag: u64) -> Value { - match x { - Value::Array(_) if x.holds_values() => { - let items = x.elements().zip(v.elements()).map(|(x, v)| along(&x, &v, tag)).collect::>(); - x.layout().collect(items, || x.prototype()).expect("a direction has its point's layout") +pub(crate) fn detach(value: &Value) -> Value { + map_leaves(value, |leaf| Ok(match leaf { + Value::Carried { carried: parts, .. } if matches!(&**parts, Carried::Dual { .. }) => { + let Carried::Dual { primal, .. } = &**parts else { unreachable!() }; + detach(primal) } - _ => dual(tag, x.clone(), v.clone()), - } -} - -/// An error unless `primal`, a result of the function that `∂` differentiates, is a number or a simple array of numbers. -fn numeric(primal: &Value) -> Result<(), Error> { - crate::transform::visit(primal, &mut |leaf| numbers(leaf).map(drop).map_err(|_| Error::domain("∂ needs a numeric result")))?; - if !primal.is_atom() && !primal.is_simple() { return Err(Error::domain("∂ needs a simple numeric result")); } - Ok(()) -} - -/// The Jacobian with the layout of `primal×⊗right`, from the tangents of passes that each seeded one number of `right`. -fn jacobian(right: &Value, primal: &Value, columns: Vec) -> Result { - if right.is_atom() && columns.len() == 1 { numeric(primal)?; return Ok(columns.into_iter().next().expect("a number has one column")); } - laid_out(right, primal, &mut |i, j| { let c = &columns[j]; if c.is_atom() { c.clone() } else { c.element(i) } }) -} - -/// The Jacobian with the layout of `primal×⊗right`. Its item for result item `i` and argument item `k` has the structure of `right`'s item `k`, and holds `entry(i, j)` for each number `j` of `right` in that item. -fn laid_out(right: &Value, primal: &Value, entry: &mut dyn FnMut(usize, usize) -> Value) -> Result { - numeric(primal)?; - let rows = if primal.is_atom() { 1 } else { primal.len() }; - let (layout, items) = if right.is_atom() { - if primal.is_atom() { return Ok(entry(0, 0)); } - (primal.layout().clone(), (0..rows).map(|i| entry(i, 0)).collect::>()) - } else { - let layout = Agreement::outer(primal.layout(), right.layout()).described("Jacobian is too large")?.layout; - let mut items = Vec::with_capacity(rows * right.len()); - for i in 0..rows { - let mut j = 0; - let mut next = || { j += 1; entry(i, j - 1) }; - for k in 0..right.len() { items.push(rebuild(&right.element(k), &mut next)?); } + Value::Carried { level, carried: parts } => { + let Carried::Batch { whole, cell } = &**parts else { unreachable!() }; + carried(*level, Carried::Batch { whole: detach(whole), cell: cell.clone() }) } - (layout, items) - }; - let jacobian = layout.collect(items, || Value::integer(0)).described("invalid Jacobian")?; - // Inside a batched run each entry is a batch of numbers, which join into one batch. - Ok(crate::batch::joined(&jacobian).unwrap_or(jacobian)) + _ => leaf.clone(), + })).expect("detaching reads no data") } -/// `f∂ right` in reverse mode: one run of `f` against seeds that record its linear calls, then a transpose of the record for each result item, or a forward evaluation for each number of `right`, whichever are fewer. -fn reverse(f: &Function, left: Option<&Value>, right: &Value, n: usize, span: &Span) -> Result { - let lin = linearized(f, left, right, span)?; - let primal = &lin.value; - // A unit result's Jacobian is its gradient, which has the argument's structure, with no axis for the result. - if primal.is_atom() { return lin.gradient(span); } - let m = primal.len(); - // A Jacobian too large to hold fails here, before the loops below build and keep its rows or columns. - crate::array::generated_len(&[m, n])?; - if m <= n { - let mut rows = Vec::with_capacity(m); - for i in 0..m { rows.push(lin.transposed(unit(primal, i), span)?.iter().flat_map(numbers_of).collect::>()); } - return laid_out(right, primal, &mut |i, j| rows[i][j].clone()); - } - let mut columns = Vec::with_capacity(n); - for j in 0..n { - let mut k = 0; - let seeds = lin.parts.iter().map(|part| Ok(direction(part, numbers(part)?, &mut k, j))).collect::, Error>>()?; - columns.push(lin.evaluated(&seeds, span)?); - } - jacobian(right, primal, columns) -} - -/// `f` linearised at an argument: its value there, and the record of its derivative, which gives vector–Jacobian products and JVPs without running `f` again. `parts` are the argument's numeric leaves, in the order `walk` visits them. -pub(crate) struct Linearized { argument: Value, parts: Vec, pub(crate) value: Value, nodes: Vec> } - -/// `f` linearised at `right`, with `left` as its left argument. -pub(crate) fn linearized(f: &Function, left: Option<&Value>, right: &Value, span: &Span) -> Result { - let record = session().begin(crate::transform::Kind::Record); - let tag = session().begin(crate::transform::Kind::Derivative); - let run = linearize(f, left, right, tag, record, span); - session().end(); - session().end(); - let (parts, value, record) = run?; - Ok(Linearized { argument: right.clone(), parts, value, nodes: record.map_or_else(Vec::new, |r| crate::linear::order(&r)) }) -} - -impl Linearized { - /// `u`, which has the value's layout, times the Jacobian, as one gradient for each part. - fn transposed(&self, u: Value, span: &Span) -> Result, Error> { - if self.nodes.is_empty() { return Ok(self.parts.iter().map(zeros).collect()); } - let grads = crate::linear::transpose(&self.nodes, u, self.parts.len(), span)?; - Ok(grads.into_iter().zip(&self.parts).map(|(g, part)| g.unwrap_or_else(|| zeros(part))).collect()) - } - - /// The derivative along `seeds`, one for each part. - fn evaluated(&self, seeds: &[Value], span: &Span) -> Result { - if self.nodes.is_empty() { return Ok(zeros(&self.value)); } - crate::linear::evaluate(&self.nodes, seeds, span) - } - - /// `u` times the Jacobian, with the argument's layout. - pub(crate) fn vjp(&self, u: Value, span: &Span) -> Result { - let mut grads = self.transposed(u, span)?.into_iter(); - walk(&self.argument, &mut |_| Ok(grads.next().expect("one gradient for each part"))) - } - - /// The gradient, for a value that is one number. - pub(crate) fn gradient(&self, span: &Span) -> Result { self.vjp(unit(&self.value, 0), span) } - - /// The derivative along `v`, which has the argument's layout. - pub(crate) fn jvp(&self, v: &Value, span: &Span) -> Result { - let mut seeds = Vec::with_capacity(self.parts.len()); - crate::transform::visit(v, &mut |leaf| { seeds.push(leaf.clone()); Ok(()) })?; - self.evaluated(&seeds, span) - } -} - -/// The numbers of `v`, in order. -fn numbers_of(v: &Value) -> Vec { if v.is_atom() { vec![v.clone()] } else { v.elements().collect() } } - -/// Runs `f` once, with each part of `right` that `∂` differentiates seeded with a record at record level `record`, as the tangent of a dual at level `tag`. Gives those parts, the result without tangents, and the record of the result's tangent, laid out as the result. The record is `None` where the result doesn't depend on `right`. -fn linearize( - f: &Function, left: Option<&Value>, right: &Value, tag: u64, record: u64, span: &Span -) -> Result<(Vec, Value, Option>), Error> { - let mut parts = Vec::new(); - let seeded = walk(right, &mut |leaf| { - numbers(leaf)?; - if complex(leaf) { return Err(Error::new(ErrorKind::Unsupported, "∂ can't differentiate with respect to complex numbers")); } - let record = crate::linear::seed(parts.len(), leaf, record); - parts.push(leaf.clone()); - Ok(dual(tag, leaf.clone(), record)) - })?; - let result = packed(&f.call_array(left, &seeded, span)?, tag); - let (primal, tangent) = split(&result, tag); - let primal = strip(&primal, tag); - numeric(&primal)?; - let record = match tangent { - Some(t) => match crate::linear::record_of(&t) { - Some(_) => crate::linear::record_of(&expand(&t, primal.layout())).cloned(), - // A tangent with no record anywhere in it came only from constants, as when `c∩⍵` draws its items from `c`. The result then doesn't depend on the argument. - None if !t.carried() => None, - None => return Err(Error::new(ErrorKind::Unsupported, "∂ can't differentiate this result in reverse mode")), - }, - None => None, - }; - Ok((parts, primal, record)) -} - -/// `value`, with each array that holds duals at level `tag`, at any depth, repacked into one array dual, innermost first. -fn packed(value: &Value, tag: u64) -> Value { - if !value.holds_values() { return value.clone(); } - let items: Vec = value.elements().map(|e| packed(&e, tag)).collect(); - if items.iter().filter_map(|e| dual_of(e).map(|d| d.tag)).max() != Some(tag) { return value.clone(); } - let mut data = Gather::items(items.len()); - for e in items { data.add(e); } - // `value`'s own items fill its own layout. A fallback here would drop the tangents. - data.finish(value.layout().clone(), || value.prototype()).expect("a value's items fill its own layout") -} - -/// A value with `value`'s structure whose numbers come from `next`, in the order that `seed` seeds them. -fn rebuild(value: &Value, next: &mut dyn FnMut() -> Value) -> Result { - walk(value, &mut |leaf| match numbers(leaf)? { - _ if leaf.is_atom() => Ok(next()), - n => leaf.layout().collect((0..n).map(|_| next()).collect::>(), || Value::integer(0)).described("invalid Jacobian"), - }) -} - -/// `op` applied to arguments of which some is a dual of the running `∂` call `tag`, which `transform::call` finds, or `None` where its own code handles the duals, through the calls it makes. A primitive's form gives its rule (`primitive_rule`), except for `⌹` and `⊥`, whose rules compute through matrix products (`solve` and `decode`). A fold with no seed takes its rule where it has one (`fold_dual`), `+.×` with two arguments the product rule (`product_dual`), and `-.×` with one the determinant's (`determinant`). A seeded fold runs its own code, whose calls of its primitive take their rules. With `Pairing::Matrices`, `⌹`, `+.×` and `-.×` take each matrix of a stack, or each pair of matrices of two stacks, in one call. A rule that `f∂←g` attached applies to a call with one argument (`attached`). A system function that refuses duals, as all but `system::Call::Active` ones do, and `f.g` by functions that aren't primitives with one argument, give UNSUPPORTED ERROR. A system function's inverse runs its own code. A fold by an operand that isn't a primitive, or such an `f.g` with two arguments, runs its own code, whose calls of primitives take their rules. -#[cold] -#[inline(never)] -pub(crate) fn call(op: Op<'_>, left: Option<&Value>, right: &Value, tag: u64, span: &Span) -> Option> { - let (op, pairing) = match op { - Op::Operation(op, pairing) => (op, pairing), - // A rule describes a function of one argument, so a call with two can't use it. - Op::Ruled(f, rule) => return Some(if left.is_none() { attached(f, rule, right, tag, span) } else { Err(two_arguments()) }), - // A system function's inverse runs its own code, as outside `∂`. `•json⁻¹` writes a dual as its plain value. - Op::System(g, f) => return (g.system_function().is_some() && !matches!(f.call, crate::system::Call::Active(_))).then(|| Err(system(f.name))), - Op::Product if left.is_none() => return Some(Err(expansion())), - Op::Fold(..) | Op::Product => return None, - }; - let stacked = matches!(pairing, Pairing::Matrices); - match (op, pairing) { - (Operation::Fold(p, kind), Pairing::Axis(axis)) if left.is_none() => fold_dual(p, kind, axis, right, span), - (Operation::Primitive(Primitive::Arithmetic(Arithmetic::Plus)), Pairing::At(writes)) => left.map(|base| accumulated(base, right, writes, tag, span)), - // Another call along an axis runs its own code. - (_, Pairing::Axis(Some(_)) | Pairing::Agreed(_)) | (Operation::Fold(..), _) => None, - // A cellwise form applies to cells through Rank first, and each cell's call comes back here. - (Operation::Primitive(p @ (Primitive::MatrixDivide | Primitive::Decode)), _) => (stacked || p.cells(left, right).is_none()).then(|| match p { - Primitive::MatrixDivide => solve(left, right, tag, stacked, span), _ => decode(left, right, span) - }), - (Operation::Primitive(p), Pairing::Axis(None)) => primitive_rule(p, left, right, tag, span), - (Operation::Product(..), _) => match left { - Some(x) if op == Operation::PLUS_TIMES => Some(product_dual(x, right, tag, stacked, span)), - None if op == Operation::MINUS_TIMES => Some(determinant(right, tag, stacked, span)), None => Some(Err(expansion())), Some(_) => None - }, - _ => None, - } +pub(crate) fn levels(value: &Value) -> usize { + let mut tags = Vec::new(); visit_duals(value, &mut |id| if !tags.contains(&id) { tags.push(id); }); tags.len() } -/// The UNSUPPORTED ERROR of monadic `f.g` other than `-.×`, the determinant, which is the only one that `∂` differentiates. -fn expansion() -> Error { Error::new(ErrorKind::Unsupported, "∂ differentiates monadic f.g only as -.×") } - -/// `items` added into `base` at `writes` (`Primitive::at`), where either is a dual of `∂` call `tag`. Accumulation is linear in both arguments. The primals accumulate as a write does, each written item keeping its kind. The tangents sum as cotangents do (`linear::sum_at`), in the kind that both join to, as the linear record's transposes and forward runs give them. A side without a tangent adds nothing to the tangent. Where `base` has none, the tangent's sum starts from zeros of the tangent's kind. -fn accumulated(base: &Value, items: &Value, writes: rustymath::gather::Writes<'_>, tag: u64, span: &Span) -> Result { - let plus = Primitive::Arithmetic(Arithmetic::Plus); - let ((mut primal, base_tangent), (items, items_tangent)) = (split(base, tag), split(items, tag)); - plus.at(&mut primal, writes, &items, span)?; - let tangent = match (base_tangent, items_tangent) { - (base_tangent, Some(t)) => { - let mut sum = base_tangent.unwrap_or_else(|| crate::linear::zeros(primal.layout(), &crate::linear::zero_of(&t))); - crate::linear::sum_at(&mut sum, writes, &t, span)?; - sum +fn promoted(value: &Value) -> Result { + map_leaves(value, |leaf| { + if matches!(leaf.kind().map(|kind| kind.dtype()), Some(DType::Complex(_))) + || matches!(leaf, Value::Number(number) if number.as_complex().is_some()) { + return Err(Error::new(ErrorKind::Unsupported, "∂ doesn't differentiate complex numbers")); } - (Some(b), None) => b, - (None, None) => unreachable!("a call that ∂ takes holds a dual"), - }; - Ok(dual(tag, primal, tangent)) -} - -/// `p` on arguments of which some is a dual of the running `∂` call `tag`, by the rule that its form's row gives (`primitive::Facts`), or `None` where `p`'s own code handles the duals. -fn primitive_rule(p: Primitive, left: Option<&Value>, right: &Value, tag: u64, span: &Span) -> Option> { - // A circle code for each item gives a partial for each item: the call goes item by item, through the dual's items. - if p.info().dyad.code && left.is_some_and(|x| !x.is_atom()) { return None; } - Some(match p.facts(left.is_some()).form { - Form::Pervasive => pervasive(p, left, right, tag, span), - Form::Constant => p.call(left.map(|x| strip(x, tag)).as_ref(), &strip(right, tag), span), - // Mix applies to the primal and the tangent apart, which keeps a nested primal whole. - Form::Moves { .. } if matches!(p, Primitive::Mix) && left.is_none() => apart(p, None, right, tag, span), - Form::Orders => return ordered(p, left, right, tag, span), - // A move that joins several sources reads a batch's items one at a time. Where a primal is a batch, the move runs on the primals and on the tangents apart, so that each call takes its own level's rules. - Form::Moves { planned: false, .. } if [left, Some(right)].into_iter().flatten().any(batched_primal) => apart(p, left, right, tag, span), - // Other moves run their own code, which moves each part of a dual through the carrier's moves (`transform::Carrier`). A form defined by other primitives takes their rules. - Form::Moves { .. } | Form::Defined => return None, - Form::Own if matches!(p, Primitive::Polynomial) => polynomial(left.expect("⌻ takes coefficients on the left"), right, tag, span), - // Monadic `#` runs its own code, on the dual's items. - Form::Own if matches!(p, Primitive::Replicate) => return None, - // `x↦y` counts from `x`, so every item moves with `x` (`range`). Monadic `↦` is constant. - Form::Own if matches!(p, Primitive::Range) => range(left.expect("only dyadic ↦ has its own rule"), right, tag, span), - // Monadic `∨` and `∧` give each real number's two parts along a new last axis (`real_parts`). - Form::Own if matches!(p, Primitive::Math(Math::Gcd | Math::Lcm)) => real_parts(p, right, tag, span), - // `[a b] betainc x` calls `BetaincAt` with its pair on x, a pervasive function of x that has slopes. It has none in a or b. - Form::Own if matches!(p, Primitive::Special(crate::primitive::Special::Betainc)) => match left.and_then(|x| split(x, tag).1) { - Some(_) => Err(Error::new(ErrorKind::Unsupported, format!("∂ can't differentiate {} in its left argument", p.info().glyph))), - None => return None, - }, - Form::Own | Form::Unsupported => Err(Error::new(ErrorKind::Unsupported, format!("∂ can't differentiate {}", p.info().glyph))), - }) -} - -/// `x↦y` under `∂`. Each item is `x` plus a whole number, so the result's tangent is `x`'s, which agrees with every item. `y` gives only the count, whose plain value it reads, as counts do. -fn range(left: &Value, right: &Value, tag: u64, span: &Span) -> Result { - let (x, tx) = split_raw(left, tag); - let r = Primitive::Range.call(Some(&x), &strip(right, tag), span)?; - Ok(match tx { Some(t) => dual(tag, r, t), None => r }) -} - -/// Whether `value` is a batch, or a dual whose primal, through every dual inside it, is one. -fn batched_primal(value: &Value) -> bool { - match value.carrier() { Some(Carrier::Batch(_)) => true, Some(Carrier::Dual(dual)) => batched_primal(&dual.primal), _ => false } -} - -/// `p`, a move, applied to its arguments' primals and then to their tangents. An argument without a dual at level `tag` has zeros as its tangent, which a record's ids read as fill. A left argument that chooses items, as a shape or counts do (`Primitive::discards`), passes its primal to both calls. Parts that hold records as items move through their ids (`linear::primitive_call`). -fn apart(p: Primitive, left: Option<&Value>, right: &Value, tag: u64, span: &Span) -> Result { - let part = |v: &Value| { - let (primal, tangent) = split(v, tag); - let tangent = tangent.unwrap_or_else(|| zeros(&primal)); - (primal, tangent) - }; - let call = |x: Option<&Value>, y: &Value| { - if [x, Some(y)].into_iter().flatten().any(crate::linear::holds_records) { crate::linear::primitive_call(p, x, y, None, span) } - else { p.call(x, y, span) } - }; - let (y, ty) = part(right); - let (x, tx) = left.map(|l| { - if p.discards(true) { let x = split(l, tag).0; (x.clone(), x) } else { part(l) } - }).unzip(); - let primal = call(x.as_ref(), &y)?; - let tangent = call(tx.as_ref(), &ty)?; - Ok(dual(tag, primal, tangent)) -} - -/// Monadic `∨` or `∧` of real numbers under `∂`. Each gives a number's two parts along a new last axis: `∨` its real and imaginary parts, and `∧` its magnitude and angle. For a real number the second part is constant, and the first is the number, or for `∧` its magnitude, whose derivative is the number's sign. So the tangent is `y`'s tangent, times its sign for `∧`, beside a 0. -fn real_parts(p: Primitive, right: &Value, tag: u64, span: &Span) -> Result { - let (y, ty) = split(right, tag); - if complex(&y) { - return Err(Error::new(ErrorKind::Unsupported, format!("∂ can't differentiate {} on complex numbers", p.info().glyph))); - } - let r = p.call(None, &y, span)?; - let Some(t) = ty else { return Ok(r) }; - let times = Primitive::Arithmetic(Arithmetic::Times); - let t = if matches!(p, Primitive::Math(Math::Lcm)) { times.call(Some(×.call(None, &y, span)?), &t, span)? } else { t }; - // `t` gains a last axis of length 1, which agreement extends over `1 0` laid along the new last axis. - let k = y.shape().len(); - let first = Value::integers([vec![1; k], vec![2]].concat(), vec![1, 0]).described("invalid shape")?; - let t = if k == 0 { t } else { t.relaid(t.layout().concat(&crate::array::Layout::from(vec![1]))).described("invalid shape")? }; - Ok(dual(tag, r, times.call(Some(&t), &first, span)?)) -} - -/// A form that orders items, at level `tag`: the primal's and the tangent's items at the positions that the form takes (`Primitive::taken`). Indexing by those positions gives what the form itself gives, without running it again. `None` where the form takes no such positions, which leaves it its own code. -fn ordered(p: Primitive, left: Option<&Value>, right: &Value, tag: u64, span: &Span) -> Option> { - let (x, tx) = match left { Some(v) => { let (x, t) = split(v, tag); (Some(x), t) } None => (None, None) }; - let (y, ty) = split(right, tag); - let taken = p.taken(x.as_ref(), &y, span)?; - Some(taken.and_then(|(origin, positions)| { - let order = positions.enclose().expect("positions enclose"); - let at = |v: &Value| Primitive::Index.call(Some(&order), v, span); - let tangent = |t: Option, like: &Value| t.unwrap_or_else(|| zeros(like)); - let left = || x.as_ref().expect("a form that takes from its left argument has one"); - let (r, t) = match origin { - Origin::Left => (at(left())?, at(&tangent(tx, left()))?), - Origin::Right => (at(&y)?, at(&tangent(ty, &y))?), - Origin::Both => { - let after = |l: &Value, r: Value| Primitive::CatenateFirst.call(Some(l), &r, span); - (after(left(), at(&y)?)?, after(&tangent(tx, left()), at(&tangent(ty, &y))?)?) + match leaf { + Value::Number(_) => Mark::Real.conversion().call(std::slice::from_ref(leaf), 0), + _ if matches!(leaf.kind().map(|kind| kind.dtype()), Some(DType::Float(_) | DType::Integer(_) | DType::Boolean)) => { + Mark::Real.conversion().call(std::slice::from_ref(leaf), 0) } - }; - Ok(dual(tag, r, t)) - })) -} - -/// A pervasive primitive's result at level `tag`: the primitive on the primals, with the tangent that the partials table gives. -fn pervasive(p: Primitive, left: Option<&Value>, right: &Value, tag: u64, span: &Span) -> Result { - let (x, tx) = match left { Some(v) => { let (x, t) = split_raw(v, tag); (Some(x), t) } None => (None, None) }; - let (y, ty) = split_raw(right, tag); - let r = p.call(x.as_ref(), &y, span)?; - if complex(&y) || x.as_ref().is_some_and(complex) || complex(&r) { - return Err(Error::new(ErrorKind::Unsupported, format!("∂ can't differentiate {} on complex numbers", p.info().glyph))); - } - // A float result's partials are floats of its width, computed from arguments converted to that width. Partials of exact arguments, such as `÷y` of an exact count, would otherwise be exact fractions, computed one item at a time. - let floats = |v: Value| match r.template_dtype() { - Some(DType::Float(w)) if v.template_dtype() != Some(DType::Float(w)) => converted(&v, DType::Float(w), span), _ => Ok(v) - }; - let (x, y) = (x.map(floats).transpose()?, floats(y)?); - let [dx, dy] = partials(p, x.as_ref(), &y, &r, [tx.is_some(), ty.is_some()], span)?; - let mut t: Option = None; - for (d, td) in [(dx, tx), (dy, ty)] { - let (Some(d), Some(td)) = (d, td) else { continue }; - let term = scale(&d, &td, span)?; - t = Some(match t { Some(t) => add(&t, &term, span)?, None => term }); - } - let t = t.expect("a dual argument has a tangent"); - Ok(dual(tag, r, t)) -} - -/// `coefficient` times the tangent `t`. A linear operation: reverse mode transposes it to itself. A factor of 1 or ¯1 is skipped, where skipping it keeps the result's kind. -fn scale(coefficient: &Value, t: &Value, span: &Span) -> Result { - let unit = |v: &Value, other: &Value| match v { - Value::Number(n) if n.as_integer().is_some_and(|i| i.abs() == 1) => n.as_integer(), - Value::Number(n) if n.as_float().is_some_and(|f| f.abs() == 1.0) && other.float_width() >= n.float_width() => n.as_float().map(|f| f as i64), _ => None - }; - let negate = |v: &Value| Primitive::Arithmetic(Arithmetic::Minus).call(None, v, span); - match (unit(coefficient, t), unit(t, coefficient)) { - (Some(1), _) => Ok(t.clone()), (Some(_), _) => negate(t), (_, Some(1)) => Ok(coefficient.clone()), (_, Some(_)) => negate(coefficient), - _ => Primitive::Arithmetic(Arithmetic::Times).call(Some(coefficient), t, span) - } -} - -/// The sum of two tangents. A linear operation, whose transpose passes the cotangent to both. -fn add(a: &Value, b: &Value, span: &Span) -> Result { Primitive::Arithmetic(Arithmetic::Plus).call(Some(a), b, span) } - -/// `C⌻x` at level `tag`. With respect to the point, the tangent is each partial derivative polynomial at the point times that coordinate's tangent, summed over the coordinates. `⌻` evaluates the derivative polynomials, so a point that is a dual at a smaller tag differentiates them too. With respect to coefficients `C`, the tangent is `tC⌻x`, because `⌻` is linear in them. -fn polynomial(c: &Value, x: &Value, tag: u64, span: &Span) -> Result { - let p = Primitive::Polynomial; - let (c, tc) = split_deep(c, tag); - let (x, tx) = split_deep(x, tag); - let r = p.call(Some(&c), &x, span)?; - let mut t = None; - if let Some(tc) = tc { - if !c.all_numbers() { return Err(Error::new(ErrorKind::Unsupported, "∂ can't differentiate ⌻ with respect to roots or exponents")); } - t = Some(match crate::linear::record_of(&tc) { Some(record) => coefficients(record, &x, span)?, None => p.call(Some(&tc), &x, span)? }); - } - if let Some(tx) = tx { - for (k, d) in crate::polynomial::partials(&c)?.iter().enumerate() { - let term = scale(&p.call(Some(d), &x, span)?, &coordinate(&tx, k), span)?; - t = Some(match t { Some(t) => add(&t, &term, span)?, None => term }); + _ => Ok(leaf.clone()), } - } - Ok(dual(tag, r, t.expect("a dual argument has a tangent"))) -} - -/// The tangent of `C⌻x` along `tc`, a record of the tangents of `C`. `C⌻x` is linear in its coefficients `C`, whose last axis holds each polynomial's coefficients. The points broadcast against the frame of `C`, as `⌻` pairs them. Each point's powers scale its coefficients' tangents, and a product with ones sums them. -fn coefficients(tc: &std::sync::Arc, x: &Value, span: &Span) -> Result { - if !x.all_numbers() { return Err(Error::new(ErrorKind::Unsupported, "∂ can't transpose ⌻")); } - let shape = tc.layout.shape(); - let (frame, n) = match shape.split_last() { Some((&n, frame)) => (frame.to_vec(), n), None => (Vec::new(), 1) }; - let points = Primitive::Arithmetic(Arithmetic::Plus).call(Some(&crate::primitive::reshape_to(&frame, &Value::integer(0), span)?), x, span)?; - let mut column = points.shape().to_vec(); - // The coefficients take length-1 axes between their frame and their last axis, so that they agree with points of higher rank. - let padded: Vec = frame.iter().copied().chain(std::iter::repeat_n(1, column.len() - frame.len())).chain([n]).collect(); - let coefficients = crate::linear::view(tc, Layout::from(crate::Shape::from(padded))); - let mut row = vec![1; column.len()]; - column.push(1); - row.push(n); - let exponents = Value::integers(vec![n], (0..n as i64).collect()).described("invalid powers")?; - let powers = Primitive::Math(Math::Power).call( - Some(&crate::primitive::reshape_to(&column, &points, span)?), &crate::primitive::reshape_to(&row, &exponents, span)?, span)?; - let ones = Value::integers(vec![n], vec![1; n]).described("invalid ones")?; - let scaled = scale(&powers, &coefficients, span)?; - Ok(crate::linear::product(&ones, crate::linear::record_of(&scaled).expect("a scaled record is a record"), false, false, points.layout().clone())) + }) } -/// `x+.×y`, or, where `stacked`, the product of each pair of matrices of two stacks (`linear::plus_times`). One factor can be a record, which then records a `Product` with the other factor as its coefficient. `+.×` repeats a scalar along the axis that the other factor contracts, so a record that is a scalar first broadcasts to that axis's length (`agreement::expand`), whose transpose sums it back, and the `Product` reads a child of the length it contracts. -pub(crate) fn product(x: &Value, y: &Value, stacked: bool, span: &Span) -> Result { - let spread = |v: &Value, len: Option| { - (!stacked && v.shape().is_empty() && crate::linear::record_of(v).is_some()).then_some(len).flatten() - .map(|n| crate::agreement::expand(v, &Layout::from([n]))) - }; - let (sx, sy) = (spread(x, y.shape().first().copied()), spread(y, x.shape().last().copied())); - let (x, y) = (sx.as_ref().unwrap_or(x), sy.as_ref().unwrap_or(y)); - let layout = || -> Result { - if !stacked { return Ok(x.layout().contracted(y.layout())); } - let (kx, ky) = (x.shape().len(), y.shape().len()); - let frame = Agreement::new(&x.layout().axes(0..kx - 2), &y.layout().axes(0..ky - 2)).described("stacks of matrices agree")?.layout; - Ok(frame.concat(&x.layout().axes(kx - 2..kx - 1)).concat(&y.layout().axes(ky - 1..ky))) - }; - match (crate::linear::record_of(x), crate::linear::record_of(y)) { - (Some(t), None) => Ok(crate::linear::product(y, t, false, stacked, layout()?)), - (None, Some(t)) => Ok(crate::linear::product(x, t, true, stacked, layout()?)), _ => crate::linear::plus_times(x, y, stacked, span) +pub(crate) fn rule(level: &Level, origin: Origin<&[Value]>) -> Result> { + let (mut primals, mut tangents) = (Vec::with_capacity(origin.list.len()), Vec::with_capacity(origin.list.len())); + for value in origin.list { + let (primal, tangent) = split_deep(value, level.id)?; + primals.push(primal); tangents.push(tangent); } + let result = Operation::jvp(&origin.with_list(primals.as_slice()), &tangents, level.id)?; + debug_assert_eq!(result.primal.as_ref().len(), result.tangent.as_ref().len()); + let mut tangents = result.tangent.into_vec().into_iter(); + Ok(result.primal.map(|primal| match tangents.next().expect("a JVP preserves its output structure") { + Some(tangent) => dual(level.id, primal, tangent), None => primal, + })) } +pub(crate) fn system(name: &str) -> Error { Error::new(ErrorKind::Unsupported, format!("∂ can't differentiate {name}")) } -/// `⌹`, with `R←⌹A`, or `⌹⍤2` on stacks of matrices where `stacked`. The tangent of `⌹A` is `-R+.×tA+.×R`, and the tangent of `X←B⌹A` is `R+.×tB-tA+.×X`. The primal is `⌹`'s own result, so a function computes the same values with or without `∂`. `B⌹A` is linear in `B` for any `A`, with `⌹A` as its coefficient, so an inactive `A` can be any matrix. -fn solve(left: Option<&Value>, right: &Value, tag: u64, stacked: bool, span: &Span) -> Result { - let (a, ta) = split_deep(right, tag); - let divide = |x: Option<&Value>, y: &Value| Operation::Primitive(Primitive::MatrixDivide).call(Pairing::stacked(stacked), x, y, span); - let minus = Primitive::Arithmetic(Arithmetic::Minus); - let Some(b) = left else { - let (r, _) = regular_inverse(&a, "⌹", stacked, span)?; - let t = product(&r, &product(&ta.expect("an active argument has a tangent"), &r, stacked, span)?, stacked, span)?; - let t = minus.call(None, &t, span)?; - return Ok(dual(tag, r, t)); - }; - let (b, tb) = split_deep(b, tag); - let x = divide(Some(&b), &a)?; - let r = match ta { Some(_) => regular_inverse(&a, "⌹", stacked, span)?.0, None => divide(None, &a)? }; - let t = match (tb, ta) { - (Some(tb), Some(ta)) => minus.call(Some(&tb), &product(&ta, &x, stacked, span)?, span)?, - (None, Some(ta)) => minus.call(None, &product(&ta, &x, stacked, span)?, span)?, (tb, None) => tb.expect("an active argument has a tangent") - }; - Ok(dual(tag, x, product(&r, &t, stacked, span)?)) -} - -/// `⌹a` and the determinant of `a`, from one factorization, for an `a` that `∂` differentiates with respect to, or each matrix's inverse and determinant where `stacked`. A matrix that `⌹` gives the pseudoinverse of, as [`matrix_inverse`](crate::primitive::matrix_inverse) finds by rustymath's pivot test, gives UNSUPPORTED, naming `glyph`, because the pseudoinverse's derivative differs. An `a` active at an outer `∂` takes its inverse through `⌹`'s call, so that the outer `∂` differentiates it, and gives no determinant. -fn regular_inverse(a: &Value, glyph: &str, stacked: bool, span: &Span) -> Result<(Value, Option), Error> { - if a.carried() { return Ok((Operation::Primitive(Primitive::MatrixDivide).call(Pairing::stacked(stacked), None, a, span)?, None)); } - let (r, d) = crate::primitive::matrix_inverse(a, stacked)? - .ok_or_else(|| Error::new(ErrorKind::Unsupported, format!("∂ differentiates {glyph} only at an invertible matrix")))?; - Ok((r, Some(d))) -} - -/// `⊥`, as `d+.×⌽×\1,⌽1↓b` for digits `d` and base `b`, each taken to the length that `⊥` decodes along: the weight of each digit is the product of the bases after it. Running that on the duals differentiates with respect to both. Monadic `⊥` has base 2. A base with more than one axis gives UNSUPPORTED. -fn decode(left: Option<&Value>, right: &Value, span: &Span) -> Result { - let base = left.cloned().unwrap_or_else(crate::primitive::binary_base); - let (xlen, ylen) = (base.shape().last().copied().unwrap_or(1), right.shape().last().copied().unwrap_or(1)); - let len = crate::primitive::decode_length(xlen, ylen)?; - if base.shape().len() > 1 { return Err(Error::new(ErrorKind::Unsupported, "∂ differentiates ⊥ only with a scalar or vector base")); } - let reverse = |v: &Value, span: &Span| Primitive::Reverse(false).call(None, v, span); - let digits = if ylen == len { right.clone() } else { - let d = if right.is_atom() { Primitive::Ravel.call(None, right, span)? } else { right.clone() }; - Primitive::Replicate.call_axis(Some(&Value::integer(len as i64)), &d, Some(d.shape().len() - 1), span)? +pub(crate) fn two_arguments() -> Error { + Error::domain("∂ can't use an attached derivative with two arguments; bind the left argument first") +} + +pub(crate) fn custom_rule(function: &Function, origin: &Origin<&[Value]>) -> Result> { + let Some(rule) = function.rules().and_then(|rules| rules.derivative.as_ref()) else { return Ok(None); }; + let roots = Function::argument_roots(origin)?; + let right = roots[1].as_ref().ok_or_else(Error::planning)?; + let argument = root(origin, right); + let Some(level) = tag(&argument) else { return Ok(None); }; + if roots[0].is_some() { return Err(two_arguments()); } + let (argument, direction) = split_deep(&argument, level)?; + let direction = direction.expect("the argument carries this derivative level"); + let call = crate::eval::map_roots(origin, |at| Ok((at.index == right.index) + .then(|| (argument.clone(), Some(at.leaves.clone().map(Some).collect())))))?; + let (primal, captured) = split_deep(&function.run(call.reborrow())?, level)?; + let stripped = origin.with_list(origin.list.iter().map(|value| strip(value, level)).collect::>()); + let tangent = rule.call_bound(&stripped.reborrow(), &[direction, argument], &[None, None])?; + Ok(Some(dual(level, primal, sum(captured, tangent)?.unwrap()))) +} + +pub(crate) fn linearity(operation: &Operation, valence: usize) -> Linearity { + use Linearity::{Jointly, No}; + let primitive = |primitive: Primitive, valence| { + let row = &primitive.info().linear; + if valence == 1 { row.monadic } else { row.dyadic } }; - let shape = Value::integers(vec![1], vec![len as i64]).described("invalid shape")?; - let bases = Primitive::Shape.call(Some(&shape), &base, span)?; - let after = Primitive::Ravel.call(Some(&Value::integer(1)), &reverse(&Primitive::Drop.call(Some(&Value::integer(1)), &bases, span)?, span)?, span)?; - let scan = Operation::Fold(Primitive::Arithmetic(Arithmetic::Times), FoldKind { scan: true, first: false }); - let weights = reverse(&scan.call(Pairing::Axis(None), None, &after, span)?, span)?; - product(&digits, &weights, false, span) -} - -/// Monadic `-.×` at an invertible matrix `A`, or at each matrix of a stack where `stacked`. The tangent of `d←-.×A` is `+/+/(d×⍉R)×tA`, where `R←⌹A` and `⍉` transposes each matrix. One factorization gives `d` and `R`, except where `A` is active at an outer `∂`, where `-.×` computes `d` again, so that the outer `∂` differentiates it. -fn determinant(right: &Value, tag: u64, stacked: bool, span: &Span) -> Result { - let (a, ta) = split_deep(right, tag); - let (r, d) = regular_inverse(&a, "-.×", stacked, span)?; - let d = match d { Some(d) => d, None => Operation::MINUS_TIMES.call(Pairing::stacked(stacked), None, &a, span)? }; - let times = Primitive::Arithmetic(Arithmetic::Times); - let coefficient = times.call(Some(&d), &crate::linear::swapped(&r)?, span)?; - let scaled = times.call(Some(&coefficient), &ta.expect("an active argument has a tangent"), span)?; - let sum = |v: &Value| Operation::Fold(Primitive::Arithmetic(Arithmetic::Plus), FoldKind::REDUCE).call(Pairing::Axis(None), None, v, span); - Ok(dual(tag, d, sum(&sum(&scaled)?)?)) -} - -/// Coordinate `k` of each point's tangent in `t`. A number stands for every coordinate, and a vector gives its `k`th item. -fn coordinate(t: &Value, k: usize) -> Value { - let pick = |e: Value| { if e.is_atom() { e } else if e.len() == 1 { e.at(0) } else { e.at(k) } }; - match t { - Value::Array(_) if t.shape().is_empty() => pick(t.at(0)), - Value::Array(_) if t.holds_values() => { - t.layout().collect(t.elements().map(pick).collect::>(), || t.prototype()).expect("coordinates keep the points' layout") - } - _ => t.clone(), - } -} - -/// A call of `f`, which has a JVP rule `g` that `f∂←g` attached, on an argument with a dual at level `tag`, inside a function that `∂` differentiates. The primal is `f` on the primal argument. `g` runs on the primal with `⍺` a dual of zeros whose tangent is the argument's, so the tangent of its result is the rule applied to that tangent: a value in forward mode, and a record in reverse mode, as JAX transposes a `custom_jvp`. A rule must be linear in `⍺`. -#[cold] -#[inline(never)] -pub(crate) fn attached(f: &Function, g: &Function, right: &Value, tag: u64, span: &Span) -> Result { - let (x, tx) = split_deep(right, tag); - let r = f.call_array(None, &x, span)?; - let origin = walk(&x, &mut |leaf| Ok(zeros(leaf)))?; - let direction = along(&origin, &tx.expect("a dual argument has a tangent"), tag); - let (_, t) = split_deep(&packed(&g.call_array(Some(&direction), &x, span)?, tag), tag); - let t = match t { Some(t) => t, None => walk(&r, &mut |leaf| Ok(zeros(leaf)))? }; - Ok(along(&r, &t, tag)) -} - -/// A fold of a dual by `p`, where `∂` has a rule for it. A linear fold, by `+`, folds the primal and the tangent alike. Folds by `×`, `⌈` and `⌊` have rules of whole-array calls (`fold_rule`), and folds by `-` and `÷` reduce to folds by `+` and `×` (`alternated`). A fold or scan by a primitive whose form is constant, such as a comparison, is constant too, as `primitive_rule` treats the primitive. `None` leaves the fold to its own code, which calls `p` on the dual's items. -fn fold_dual(p: Primitive, kind: FoldKind, axis: Option, right: &Value, span: &Span) -> Option> { - let d = dual_of(right).filter(|_| !right.is_atom())?; - let fold = |v: &Value| Operation::Fold(p, kind).call(Pairing::Axis(axis), None, v, span); - if matches!(p.facts(true).form, Form::Constant) { return Some(fold(&d.primal)); } - if let Some(result) = alternated(p, kind, axis, right, d, span) { return Some(result); } - let row = Operation::Fold(p, kind).facts(false)?; - if row.linear.is_some() { return Some(d.map(fold)); } - let (tag, x, tx) = (d.tag, d.primal.clone(), d.full_tangent().clone()); - (!x.shape().is_empty()).then(|| fold_rule(p, kind, axis, &x, &tx, span).map(|(r, t)| dual(tag, r, t))) -} - -/// The fold by `p`, `-` or `÷`, of `right`, a dual whose primal is `d.primal`, along `axis` or the fold's own axis. A fold from the right alternates: `-/x` is `x₀-x₁+x₂-…`, which is `(+/e#x)-+/o#x`, and `÷/x` is `(×/e#x)÷×/o#x`, where `e` and `o` select the items at even and odd positions along the axis. The tangent is that expression's, through the rules of folds by `+` and `×`. The primal is the fold by `p`, so its results where items are zero stay `÷`'s. `None` for another primitive, a scan, and a lane of fewer than two items. -fn alternated(p: Primitive, kind: FoldKind, axis: Option, right: &Value, d: &Dual, span: &Span) -> Option> { - let q = match p { Primitive::Arithmetic(Arithmetic::Minus) => Arithmetic::Plus, Primitive::Arithmetic(Arithmetic::Divide) => Arithmetic::Times, _ => return None }; - let x = &d.primal; - let rank = x.shape().len(); - let along = axis.unwrap_or(kind.default_axis(rank)); - let n = x.shape().get(along).copied().filter(|&n| n > 1 && !kind.scan)?; - Some((|| { - let r = Operation::Fold(p, kind).call(Pairing::Axis(axis), None, x, span)?; - // The halves are selected and folded along the first axis, where a trace selects whole items, so another axis moves to the front first. - let front = if along == 0 { right.clone() } else { - let to = (0..rank).map(|j| if j < along { j as i64 + 1 } else if j == along { 0 } else { j as i64 }).collect(); - Primitive::Transpose.call(Some(&Value::integers(vec![rank], to).described("invalid axes")?), right, span)? - }; - let half = |odd: usize| -> Result { - let mask = Value::integers(vec![n], (0..n).map(|i| (i % 2 == odd) as i64).collect()).described("invalid mask")?; - let items = Primitive::Replicate.call_axis(Some(&mask), &front, Some(0), span)?; - Operation::Fold(Primitive::Arithmetic(q), kind).call(Pairing::Axis(Some(0)), None, &items, span) - }; - let (_, t) = split(&p.call(Some(&half(0)?), &half(1)?, span)?, d.tag); - let t = t.unwrap_or_else(|| zeros(&r)); - Ok(dual(d.tag, r, t)) - })()) -} - -/// `A+.×B` where an argument is a dual of the running `∂` call `tag`, or `A+.×⍤2 B` on stacks of matrices where `stacked`: the product of the primals, with the tangent `(tA+.×B)+A+.×tB` through the same product kernels. -fn product_dual(left: &Value, right: &Value, tag: u64, stacked: bool, span: &Span) -> Result { - let (a, ta) = split(left, tag); - let (b, tb) = split(right, tag); - let r = crate::linear::plus_times(&a, &b, stacked, span)?; - let mut t = ta.map(|ta| product(&ta, &b, stacked, span)).transpose()?; - if let Some(tb) = tb { - let term = product(&a, &tb, stacked, span)?; - t = Some(match t { Some(t) => add(&t, &term, span)?, None => term }); - } - Ok(dual(tag, r, t.expect("a dual argument has a tangent"))) -} - -/// The primal and tangent of a fold or scan by `p`, which is `×`, `⌈` or `⌊`, of `x` with tangent `tx`, through the linear operations that reverse mode transposes. A product's tangent scales each item's tangent by the product of the others and sums: the product of the nonzero items, times the sum of `tx÷x` where no item is zero, or times the zero item's tangent where one is, and 0 where more are. A maximum's or minimum's tangent gathers the tangent of the item that gives it, the first of any ties, as dyadic `⌈` and `⌊` give a tie to their left argument. -fn fold_rule(p: Primitive, kind: FoldKind, axis: Option, x: &Value, tx: &Value, span: &Span) -> Result<(Value, Value), Error> { - use {crate::array::Lanes, Arithmetic::*, Comparison::Equal}; - let call = |p: Primitive, a: &Value, b: &Value, span: &Span| p.call(Some(a), b, span); - let fold = |p: Primitive, v: &Value| Operation::Fold(p, kind).call(Pairing::Axis(axis), None, v, span); - let (times, plus) = (Primitive::Arithmetic(Times), Primitive::Arithmetic(Plus)); - let r = fold(p, x)?; - let t = if matches!(p, Primitive::Arithmetic(Times)) { - let zero = call(Primitive::Compare(Equal), x, &Value::integer(0), span)?; - let ones = call(plus, x, &zero, span)?; - let rest = fold(p, &ones)?; - let zeros = fold(plus, &zero)?; - let reciprocal = call(Primitive::Arithmetic(Divide), &call(Primitive::Arithmetic(Minus), &Value::integer(1), &zero, span)?, &ones, span)?; - let none = fold(plus, &call(times, &reciprocal, tx, span)?)?; - let one = fold(plus, &call(times, &zero, tx, span)?)?; - let none = call(times, &call(times, &rest, &call(Primitive::Compare(Equal), &zeros, &Value::integer(0), span)?, span)?, &none, span)?; - let one = call(times, &call(times, &rest, &call(Primitive::Compare(Equal), &zeros, &Value::integer(1), span)?, span)?, &one, span)?; - call(plus, &none, &one, span)? - } else { - let along = axis.unwrap_or(kind.default_axis(x.shape().len())); - // One pick on a batch's whole, with the frame's axes first, serves every cell. - let batch = x.carrier().and_then(Carrier::batch); - let all = batch.map_or(x, |b| b.whole()); - let frame = batch.map_or(0, |b| b.frame); - // Windows that slide along the fold's axis give their extremes' positions from their source, with no windows built. - let windows = (batch.is_none() && along == kind.default_axis(x.shape().len())).then(|| crate::fusion::picks(x, p, kind)).flatten(); - let traced = x.carrier().and_then(Carrier::traced); - // Where reverse mode records one item from each lane, or `x` is a batch or traced, the tangent folds the tangent times a mask of the chosen items. Its transpose reads: each item takes its lane's cotangent where it was chosen, and 0 elsewhere. A gather's transpose would scatter. A trace computes the mask in its program (`Traced::picked`). - if !kind.scan && windows.is_none() && (tx.carried() || batch.is_some() || traced.is_some()) { - let mask = match (traced, batch) { - (Some(t), _) => t.picked(p, along).map_err(|reason| t.stop(crate::transform::Cause::Rule, reason))?, - (None, Some(b)) => crate::batch::batch(b.level, chosen(p, all, frame + along)?, b.frame), (None, None) => chosen(p, all, along)? - }; - return Ok((r, fold(plus, &call(times, &mask, tx, span)?)?)); + match operation { + Operation::Primitive { primitive: p, .. } => primitive(*p, valence), + Operation::Read(_) | Operation::Join(_) => Jointly(u8::MAX), + Operation::Convert(Mark::Real | Mark::Type { dtype: DType::Float(_), .. }) => Jointly(1), + Operation::Fold { primitive: p, .. } if matches!(primitive(*p, 2), Jointly(3)) => Jointly(u8::MAX), + Operation::Product { fold, map, .. } if matches!(primitive(*fold, 2), Jointly(3)) => primitive(*map, 2), + Operation::At { combine: None, .. } => Jointly(3), + Operation::At { combine: Some(p), .. } => match primitive(*p, 2) { Jointly(3) => Jointly(3), _ => No }, + _ => No, + } +} + +fn sum(left: Option, right: Value) -> Result> { + Ok(Some(match left { None => right, Some(left) => + Operation::Primitive { primitive: Primitive::Arithmetic(Arithmetic::Add), pairing: Pairing::Axes }.call(&[left, right], 0)?, })) +} + +fn row_jvp(origin: &Origin<&[Value]>, tangents: &[Option]) -> Result { + use Linearity; + let primal = Operation::dispatch(origin.reborrow())?; + let call = origin.call_view(&origin.list); + let positions: Vec<_> = call.data_positions().collect(); + let zero = origin.operation.primitive().is_some_and(|primitive| primitive.info().linear.zero[usize::from(positions.len() == 2)]); + let active = positions.iter().enumerate().filter(|(_, position)| tangents[**position].is_some()).collect::>(); + let trees = primal.trees().cloned(); + let mut tangent = vec![None; primal.as_ref().len()]; + if zero || active.is_empty() || primal.as_ref().iter().all(|value| !matches!(value.dtype(), Some(DType::Float(_)))) { + return Ok(JvpOutputs { primal, tangent: Outputs::from(tangent).with_trees(trees) }); + } + match origin.operation.linearity(positions.len()) { + Linearity::Jointly(mask) => { + let mut arguments = origin.list.to_vec(); + for (operand, &position) in positions.iter().enumerate() { + if mask == u8::MAX || operand < 8 && mask & (1 << operand) != 0 { + arguments[position] = tangents[position].clone().unwrap_or_else(|| crate::linear::zeros_like(&origin.list[position])); + } + } + let result = Operation::dispatch(origin.with_list(arguments.as_slice()))?; + for (output, value) in tangent.iter_mut().zip(result.into_vec()) { *output = Some(value); } } - if let Some(t) = traced { - return Err(t.stop(crate::transform::Cause::Rule, if kind.scan { "a scan by ⌈ or ⌊" } - else { "picks by ⌈ or ⌊ from sliding windows" }).into()); + Linearity::Each => { + for (_, &position) in active { + let mut arguments = origin.list.to_vec(); arguments[position] = tangents[position].clone().unwrap(); + for (output, value) in tangent.iter_mut().zip(Operation::dispatch(origin.with_list(arguments.as_slice()))?.into_vec()) { + *output = sum(output.take(), value)?; + } + } } - let offsets = match windows { Some(offsets) => offsets, None => picked_offsets(p, all, Lanes::new(all.shape(), frame + along)?, kind.scan)? }; - // A batch's scan picks within each cell, at positions of the cell's own. - let positions = match batch { - Some(b) => { - let size = (all.len() / b.cells()) as i64; - crate::batch::batch(b.level, all.layout().integers(offsets.iter().map(|&o| o % size).collect()).described("invalid positions")?, b.frame) + Linearity::No => { + if primal.as_ref().len() != 1 { return Err(system(&origin.operation.glyph())); } + let reads = origin.list.iter().map(|value| crate::program::Read { operand: value.clone(), map: rustymath::agreement::Mapping::Linear(1) }).collect::>(); + let reads = origin.with_list(reads.as_slice()); + let needed = tangents.iter().map(Option::is_some).collect::>(); + let slopes = Operation::partials(&reads, &primal.as_ref()[0], &needed)?; + for (_, &position) in active { + if let Some(slope) = &slopes[position] { + let mut directed = reads.list.to_vec(); directed[position].operand = tangents[position].clone().unwrap(); + let direction = Operation::read_input(&reads.with_list(directed.as_slice()), position, &primal.as_ref()[0])?; + let contribution = Operation::Primitive { primitive: Primitive::Arithmetic(Arithmetic::Multiply), pairing: Pairing::Axes } + .call(&[slope.clone(), direction], 0)?; + tangent[0] = sum(tangent[0].take(), contribution)?; + } } - None => Value::integers(vec![offsets.len()], offsets).described("invalid positions")?, - }; - let offsets = positions.enclose().expect("positions enclose"); - let picked = Primitive::Index.call(Some(&offsets), &Primitive::Ravel.call(None, tx, span)?, span)?; - if r.is_atom() { picked.at(0) } else { picked.relaid(r.layout().clone()).expect("the gathered tangents fill the fold's result") } - }; - Ok((r, t)) -} - -/// A mask of Booleans in `x`'s layout, true at the item of each lane along `along` that `p`, `⌈` or `⌊`, picks: the first of any ties, as dyadic `⌈` and `⌊` give a tie to their left argument. `fold_rule` and a program's picks (`Step::Picks`) both take it. -pub(crate) fn chosen(p: Primitive, x: &Value, along: usize) -> Result { - let mut chosen = vec![false; x.len()]; - for at in picked_offsets(p, x, crate::array::Lanes::new(x.shape(), along)?, false)? { chosen[at as usize] = true; } - x.layout().booleans(chosen).described("invalid mask") -} - -/// The position in the ravel of `x` of each item that `p`, `⌈` or `⌊`, picks: the first of any ties in each of `lanes`' lanes, or where `scan`, in each running fold. -fn picked_offsets(p: Primitive, x: &Value, lanes: crate::array::Lanes, scan: bool) -> Result, Error> { - let crate::array::Lanes { outer, len: n, inner } = lanes; - let numbers = || Error::domain("∂ takes ⌈ and ⌊ of numbers"); - // Mixed storage picks among its numbers as floats. Two different exact integers beyond 2⁵³ can round to one float. They then tie, and the earlier one is picked. - let floats = match x.view() { Some(_) => None, None => Some(x.to_float_width(FloatWidth::F64).map_err(|_| numbers())?) }; - let view = floats.as_ref().unwrap_or(x).view().ok_or_else(numbers)?; - let of = if scan { rustymath::reduce::Picks::Running } else { rustymath::reduce::Picks::Lanes }; - let rows = crate::pervasive::picks(p, view, [outer, n, inner], of).ok_or_else(numbers)?; - Ok(rows.into_iter().enumerate().map(|(j, row)| lanes.offset(if scan { j / (n * inner) } else { j / inner }, row, j % inner) as i64).collect()) -} - -/// The partial derivatives of `p` at `x` and `y`, whose result is `r`, with respect to each argument that `need` asks for, as primitive calls on the values (`Recorder`). The function object that the kernel table of `f64` chooses for the call (`pervasive::Kernels`) gives them by its recipe (`rustymath::derivative::Partials`). The calls differentiate duals of earlier levels again and keep the partials of exact arguments exact. A running trace records each call as a step. A choice without partials gives UNSUPPORTED ERROR. -fn partials(p: Primitive, x: Option<&Value>, y: &Value, r: &Value, need: [bool; 2], span: &Span) -> Result<[Option; 2], Error> { - use crate::pervasive::Kernels; - let recorder = Recorder { values: std::cell::RefCell::new(Vec::with_capacity(16)), error: std::cell::RefCell::new(None), like: r, span }; - let (yi, ri) = (recorder.held(y.clone()), recorder.held(r.clone())); - let chosen = match x { - None => ::monad(p, Recorded { recorder: &recorder, args: [yi], r: ri, need: [need[1]], constants: [None; 2] }) - .flatten().map(|[d]| [None, d]), - Some(x) => { - let xi = recorder.held(x.clone()); - let constants = [x.as_real(), y.as_real()]; - ::dyad(p, Recorded { recorder: &recorder, args: [xi, yi], r: ri, need, constants }).flatten() } - }; - let chosen = chosen.ok_or_else(|| Error::new(ErrorKind::Unsupported, format!("∂ can't differentiate {} here", p.info().glyph)))?; - let slopes = recorder.finish(chosen)?; - // A recipe gives no slope in an argument that has none, such as `gammainc`'s shape a. - if let Some(side) = (0..2).find(|&s| need[s] && slopes[s].is_none()) { - return Err(Error::new(ErrorKind::Unsupported, format!("∂ can't differentiate {} in its {} argument", p.info().glyph, ["left", "right"][side]))); } - Ok(slopes) + Ok(JvpOutputs { primal, tangent: Outputs::from(tangent).with_trees(trees) }) } -/// The chosen object's recipe as primitive calls through `recorder`, on the values that it holds at `args` and `r`, for the slopes that `need` asks for (`partials`). It is the kernel table's loop for `∂`: the object that the table passes to `differentiable` or `extremum` gives the slopes, and any other choice, which has no partials, gives `None`. -struct Recorded<'a, 'b, const N: usize> { recorder: &'a Recorder<'b>, args: [usize; N], r: usize, need: [bool; N], constants: [Option; 2] } - -impl crate::pervasive::Dyad for Recorded<'_, '_, 2> { - type Output = Option<[Option; 2]>; - fn numeric(&self) -> crate::number::Numeric { crate::number::Numeric::current() } - fn many(self, _: impl rustymath::Binary + Copy + Sync) -> Self::Output { None } - fn booleans(self, _: impl rustymath::Binary + Copy + Sync) -> Self::Output { None } - fn integer(self, _: rustymath::buffer::Width, _: impl rustymath::Binary + Copy + Sync) -> Self::Output { None } - fn differentiable(self, f: impl rustymath::Binary + rustymath::derivative::Partials<2> + Copy + Sync) -> Self::Output { - Some(f.partials(self.recorder, self.args, self.r, self.need)) - } - fn extremum>( - self, f: impl rustymath::Binary + rustymath::derivative::Partials<2> + Copy + Sync, _: M - ) -> Self::Output { Some(f.partials(self.recorder, self.args, self.r, self.need)) } - fn constants(&self) -> (Option, Option) { (self.constants[0], self.constants[1]) } -} - -impl crate::pervasive::Monad for Recorded<'_, '_, 1> { - type Output = Option<[Option; 1]>; - fn numeric(&self) -> crate::number::Numeric { crate::number::Numeric::current() } - fn integer(self, _: rustymath::buffer::Width, _: impl rustymath::Unary + Copy + Sync) -> Self::Output { None } - fn others(self, _: impl rustymath::Unary + Copy + Sync) -> Self::Output { None } - fn differentiable(self, f: impl rustymath::Unary + rustymath::derivative::Partials<1> + Copy + Sync) -> Self::Output { - Some(f.partials(self.recorder, self.args, self.r, self.need)) - } +struct GraphOps<'a, 'v> { + origin: &'a Origin<&'v [Value]>, + values: std::cell::RefCell, bool)>>, + width: rustymath::buffer::FloatWidth, } - -/// Calls of BPL's primitives on whole values, which run a recipe of `rustymath::derivative` on them, one call for each operation. A running trace records each call as a step, so partials of duals with smaller tags are themselves differentiated. A value is its index in `values`. After the first error, an operation makes no call and gives index 0, and `finish` gives the error. `like` is the result whose partials the recipe gives. -struct Recorder<'a> { values: std::cell::RefCell>, error: std::cell::RefCell>, like: &'a Value, span: &'a Span } - -impl Recorder<'_> { - /// `value`'s index. - fn held(&self, value: Value) -> usize { +impl GraphOps<'_, '_> { + fn put(&self, value: Result, literal: bool) -> rustymath::facts::Slot { let mut values = self.values.borrow_mut(); - values.push(value); - values.len() - 1 - } - /// The index of `f`'s value, which reads the values. - fn with(&self, f: impl FnOnce(&[Value], &Span) -> Result) -> usize { - if self.error.borrow().is_some() { return 0; } - let made = f(&self.values.borrow(), self.span); - match made { - Ok(v) => self.held(v), - Err(e) => { *self.error.borrow_mut() = Some(e); 0 } - } - } - /// The index of `p` on the values at `x` and `y`. - fn call(&self, p: Primitive, x: Option, y: usize) -> usize { self.with(|values, span| p.call(x.map(|i| &values[i]), &values[y], span)) } - /// The index of the circle function of `code` on the value at `y`. - fn circle(&self, code: i64, y: usize) -> usize { - let code = self.held(Value::integer(code)); - self.call(Primitive::Math(Math::Circle), Some(code), y) - } - /// The values at the indices, or the first error. - fn finish(self, at: [Option; N]) -> Result<[Option; N], Error> { - if let Some(e) = self.error.into_inner() { return Err(e); } - let values = self.values.into_inner(); - Ok(at.map(|i| i.map(|i| values[i].clone()))) - } -} - -impl rustymath::recipe::Ops for Recorder<'_> { - type V = usize; - type B = usize; - /// A whole `x` is an exact integer, which keeps exact arguments' partials exact, and any other a float of the width that a constant beside the result takes (`primitive::float_like`). - fn number(&self, x: f64) -> usize { - let whole = x.fract() == 0.0 && x.abs() < 2f64.powi(53); - self.held(if whole { Value::integer(x as i64) } else { crate::primitive::float_like(x, self.like) }) - } - fn unary(&self, op: Unary, x: usize) -> usize { - let call = |p| self.call(p, None, x); - match op { - Unary::Negate => call(Primitive::Arithmetic(Arithmetic::Minus)), Unary::Magnitude => call(Primitive::Math(Math::Magnitude)), - Unary::Sqrt => call(Primitive::Math(Math::Root)), Unary::Floor => call(Primitive::Math(Math::Floor)), Unary::Ln => call(Primitive::Math(Math::Log)), - Unary::Sin => self.circle(1, x), Unary::Cos => self.circle(2, x), Unary::Sinh => self.circle(5, x), Unary::Cosh => self.circle(6, x), - Unary::Sqrt1mx2 => self.circle(0, x), Unary::Sqrt1px2 => self.circle(4, x), - Unary::Digamma => call(Primitive::Special(crate::primitive::Special::Digamma)), Unary::Exp => call(Primitive::Math(Math::Power)), - Unary::Lgamma => call(Primitive::Special(crate::primitive::Special::Gammaln)), - } - } - fn binary(&self, op: Binary, x: usize, y: usize) -> usize { - let call = |p| self.call(p, Some(x), y); - match op { - Binary::Add => call(Primitive::Arithmetic(Arithmetic::Plus)), Binary::Subtract => call(Primitive::Arithmetic(Arithmetic::Minus)), - Binary::Multiply => call(Primitive::Arithmetic(Arithmetic::Times)), Binary::Divide => call(Primitive::Arithmetic(Arithmetic::Divide)), - Binary::Larger => call(Primitive::Math(Math::Ceiling)), Binary::Smaller => call(Primitive::Math(Math::Floor)), - Binary::BinomDn => call(Primitive::Special(crate::primitive::Special::BinomDn)), - Binary::BinomDk => call(Primitive::Special(crate::primitive::Special::BinomDk)), - Binary::Polygamma => call(Primitive::Special(crate::primitive::Special::Polygamma)), - } - } - fn compare(&self, c: Cmp, x: usize, y: usize) -> usize { - let c = match c { Cmp::Less => Comparison::Less, Cmp::LessEqual => Comparison::LessEqual, Cmp::Equal => Comparison::Equal }; - self.call(Primitive::Compare(c), Some(x), y) - } - fn multiply_add(&self, x: usize, y: usize, z: usize) -> usize { self.add(self.multiply(x, y), z) } - fn and(&self, x: usize, y: usize) -> usize { self.call(Primitive::Math(Math::Lcm), Some(x), y) } - fn or(&self, x: usize, y: usize) -> usize { self.call(Primitive::Math(Math::Gcd), Some(x), y) } - fn not(&self, x: usize) -> usize { self.call(Primitive::Math(Math::Not), None, x) } - /// UNSUPPORTED ERROR: BPL has no select that a trace records, so the recipes of `rustymath::derivative` mask with `one_where` instead. - fn select(&self, _: usize, _: usize, _: usize) -> usize { - self.error.borrow_mut().get_or_insert_with(|| Error::new(ErrorKind::Unsupported, "∂ can't record a select")); - 0 - } - /// A Boolean is already the number 1 or 0. - fn one_where(&self, m: usize) -> usize { m } - fn constant(&self, v: usize) -> Option { self.values.borrow()[v].as_real() } - /// The sign of `x`. Where the result holds floats, `∂|` gives it in floats of `x`'s width: a trace can't record monadic `×` on floats under dynamic kinds, whose result's type depends on values. Elsewhere, monadic `×`. - fn sign(&self, x: usize) -> usize { - let floats = matches!(self.like.template_dtype(), Some(DType::Float(_))); - self.call(if floats { Primitive::Special(crate::primitive::Special::Sign) } else { Primitive::Arithmetic(Arithmetic::Times) }, None, x) - } - /// Monadic `÷`. - fn reciprocal(&self, x: usize) -> usize { self.call(Primitive::Arithmetic(Arithmetic::Divide), None, x) } - /// One call of `*`, whose kernel takes the route that `rustymath::Exponent` gives `k`. - fn power(&self, k: f64, x: usize) -> usize { self.call(Primitive::Math(Math::Power), Some(x), self.number(k)) } + let slot = rustymath::facts::Slot(values.len().try_into().expect("a formula fits u32")); + values.push((value, literal)); slot + } + fn get(&self, slot: rustymath::facts::Slot) -> Result { self.values.borrow()[slot.0 as usize].0.clone() } + fn apply(&self, operation: Operation, slots: &[rustymath::facts::Slot]) -> rustymath::facts::Slot { + let values = self.values.borrow(); + let literals = slots.iter().enumerate().fold(0, |bits, (i, slot)| bits | (u64::from(values[slot.0 as usize].1) << i)); + let args = slots.iter().map(|slot| values[slot.0 as usize].0.clone()).collect::>>(); + drop(values); + let result = args.and_then(|args| { + let mut origin = crate::eval::projected_call(self.origin, operation, &args, &vec![None; args.len()])?; + origin.literals |= literals; + Operation::dispatch(origin.reborrow())?.assemble() + }); + self.put(result, false) + } + fn recipe(&self, operation: RecipeOp, slots: &[rustymath::facts::Slot]) -> rustymath::facts::Slot { + self.apply(Operation::Recipe(operation), slots) + } + fn choose(&self, test: rustymath::facts::Slot, yes: rustymath::facts::Slot, no: rustymath::facts::Slot) -> rustymath::facts::Slot { + self.apply(Operation::Primitive { primitive: Primitive::Choice, pairing: Pairing::Axes }, &[test, yes, no]) + } + fn convert(&self, slot: rustymath::facts::Slot, dtype: DType) -> rustymath::facts::Slot { + self.apply(Operation::Convert(Mark::Type { dtype, fixed: false }), &[slot]) + } +} +impl rustymath::recipe::Ops for GraphOps<'_, '_> { + type V = rustymath::facts::Slot; + type B = rustymath::facts::Slot; + type I = rustymath::facts::Slot; + type W = rustymath::facts::Slot; + fn width(&self) -> rustymath::buffer::FloatWidth { self.width } + fn fused(&self) -> bool { false } + fn number(&self, value: f64) -> Self::V { self.put(Ok(Value::Number(crate::Number::float(value, self.width))), true) } + fn unary(&self, op: rustymath::recipe::Unary, x: Self::V) -> Self::V { self.recipe(RecipeOp::Unary(op), &[x]) } + fn binary(&self, op: rustymath::recipe::Binary, x: Self::V, y: Self::V) -> Self::V { self.recipe(RecipeOp::Binary(op), &[x, y]) } + fn compare(&self, op: rustymath::recipe::Cmp, x: Self::V, y: Self::V) -> Self::B { self.recipe(RecipeOp::Compare(op), &[x, y]) } + fn integer(&self, value: i64) -> Self::I { self.put(Ok(Value::integer(value)), true) } + fn integer_op(&self, op: rustymath::recipe::Integer, x: Self::I, y: Self::I) -> Self::I { self.recipe(RecipeOp::Integer(op), &[x, y]) } + fn integer_compare(&self, op: rustymath::recipe::Cmp, x: Self::I, y: Self::I) -> Self::B { self.recipe(RecipeOp::IntegerCompare(op), &[x, y]) } + fn integer_select(&self, test: Self::B, yes: Self::I, no: Self::I) -> Self::I { self.choose(test, yes, no) } + fn to_integer(&self, x: Self::V) -> Self::I { self.recipe(RecipeOp::ToInteger, &[x]) } + fn from_integer(&self, x: Self::I) -> Self::V { self.recipe(RecipeOp::FromInteger(self.width), &[x]) } + fn to_bits(&self, x: Self::V) -> Self::I { self.recipe(RecipeOp::ToBits, &[x]) } + fn from_bits(&self, x: Self::I) -> Self::V { self.recipe(RecipeOp::FromBits(self.width), &[x]) } + fn widen(&self, x: Self::V) -> Self::W { self.convert(x, DType::Float(rustymath::buffer::FloatWidth::F64)) } + fn wide_number(&self, x: f64) -> Self::W { self.put(Ok(Value::Number(crate::Number::float(x, rustymath::buffer::FloatWidth::F64))), true) } + fn wide_multiply_add(&self, x: Self::W, y: Self::W, z: Self::W) -> Self::W { self.multiply_add(x, y, z) } + fn narrow(&self, x: Self::W) -> Self::V { self.convert(x, DType::Float(self.width)) } + fn multiply_add(&self, x: Self::V, y: Self::V, z: Self::V) -> Self::V { self.binary(rustymath::recipe::Binary::Add, self.binary(rustymath::recipe::Binary::Multiply, x, y), z) } + fn overflow(&self, _: rustymath::recipe::Binary, _: Self::V, _: Self::V) -> Self::B { self.put(Ok(Value::Number(crate::Number::from_bool(false))), true) } + fn and(&self, x: Self::B, y: Self::B) -> Self::B { self.integer_op(rustymath::recipe::Integer::And, x, y) } + fn or(&self, x: Self::B, y: Self::B) -> Self::B { self.integer_op(rustymath::recipe::Integer::Or, x, y) } + fn not(&self, x: Self::B) -> Self::B { self.compare(rustymath::recipe::Cmp::Equal, x, self.number(0.0)) } + fn select(&self, test: Self::B, yes: Self::V, no: Self::V) -> Self::V { self.choose(test, yes, no) } + fn constant(&self, slot: Self::V) -> Option { + let values = self.values.borrow(); + let (value, literal) = &values[slot.0 as usize]; + if *literal { value.as_ref().ok().and_then(Value::as_real) } else { None } + } + fn tolerance(&self, _: f64) -> Self::V { + let position = self.origin.operation.settings().iter().position(|setting| *setting == Setting::Tolerance); + self.put(position.map(|i| self.origin.list[self.origin.explicit as usize + i].clone()).ok_or_else(Error::planning), false) + } +} + +pub(crate) fn partials(origin: &Origin<&[crate::program::Read]>, result: &Value, needed: &[bool]) -> Result>> { + let call = origin.call_view(origin); + let dtype = result.dtype().ok_or_else(|| Error::domain("∂ needs numeric results"))?; + let kernel = call.kernel(call.variant(dtype)?)?.ok_or_else(|| system(&origin.operation.glyph()))?; + let positions: Vec<_> = call.data_positions().collect(); + if positions.len() != kernel.arity() { return Err(Error::planning()); } + let values: Vec<_> = origin.list.iter().map(|read| read.operand.clone()).collect(); + let plain = origin.with_list(values.as_slice()); + let numeric = call.numeric()?; + let graph = GraphOps { origin: &plain, values: Default::default(), width: numeric.width }; + let inputs = positions.iter().map(|&i| graph.put(Operation::read_input(origin, i, result), false)).collect::>(); + let result = graph.put(Ok(result.clone()), false); + let active = positions.iter().map(|&i| needed[i]).collect::>(); + let mut slopes = vec![None; positions.len()]; + kernel.partials(&graph, &inputs, result, &active, &mut slopes); + let mut output = vec![None; origin.list.len()]; + for (position, slope) in positions.into_iter().zip(slopes) { output[position] = slope.map(|slope| graph.get(slope)).transpose()?; } + Ok(output) +} + +fn root<'a>(origin: &Origin<&'a [Value]>, root: &array::TreeRoot) -> std::borrow::Cow<'a, Value> { + origin.trees.as_ref().map_or_else(|| std::borrow::Cow::Borrowed(&origin.list[root.leaves.start]), |trees| trees.at_root(root, origin.list)) +} +// Each root has one packed flag, followed by activity flags for its original leaves. +pub(crate) fn pack(origin: &Origin<&[Value]>, tangents: &[Option], carries: std::ops::Range) -> Result<(Origin>, Box<[bool]>)> { + assert_eq!(origin.list.len(), tangents.len()); + let mut packing = Vec::new(); + let packed = crate::eval::map_roots(origin, |address| { + let range = &address.leaves; + let active = range.clone().map(|i| matches!(origin.list[i].dtype(), Some(DType::Float(_))) && (tangents[i].is_some() || carries.contains(&i))).collect::>(); + let any = active.iter().any(|active| *active); + packing.push(any); packing.extend_from_slice(&active); + if !any { return Ok(None); } + let primal = root(origin, address).into_owned(); + let mut i = range.start; + let tangent = map_leaves(&primal, |leaf| { + let value = tangents[i].clone().unwrap_or_else(|| crate::linear::zeros_like(leaf)); i += 1; Ok(value) + })?; + let from = range.clone().map(Some).chain(std::iter::repeat_n(None, range.len())).collect(); + Ok(Some((Value::tuple(vec![primal, tangent]), Some(from)))) + })?; + Ok((packed, packing.into_boxed_slice())) +} +fn unpack(origin: &Origin<&[Value]>, packing: &[bool], level: usize) -> Result>> { + let mut at = 0; + let result = crate::eval::map_roots(origin, |address| { + let range = &address.leaves; + let packed = *packing.get(at).ok_or_else(Error::planning)?; at += 1; + if packed && range.len() % 2 != 0 { return Err(Error::planning()); } + let count = if packed { range.len() / 2 } else { range.len() }; + let active = packing.get(at..at + count).ok_or_else(Error::planning)?; at += count; + if !packed { return Ok(None); } + let value = root(origin, address); + let Value::Array(array) = value.as_ref() else { return Err(Error::planning()); }; + let array::ArrayData::Mixed { items, .. } = &**array else { return Err(Error::planning()); }; + let [primal, _] = &**items else { return Err(Error::planning()); }; + let mut i = 0; + let restored = map_leaves(primal, |leaf| { + let value = if active[i] { dual(level, leaf.clone(), origin.list[range.start + count + i].clone()) } else { leaf.clone() }; + i += 1; Ok(value) + })?; + if i != count { return Err(Error::planning()); } + Ok(Some((restored, Some((range.start..range.start + count).map(Some).collect())))) + })?; + if at != packing.len() { return Err(Error::planning()); } + Ok(result) +} +pub(crate) fn jvp_step(function: &Function, packing: &[bool], origin: Origin<&[Value]>) -> Result { + let scope = crate::transform::Scope::new(crate::transform::LevelKind::Derivative); + let call = unpack(&origin, packing, scope.level.id)?; + let result = function.run(call.reborrow())?; + let (primal, tangent) = split_deep(&result, scope.level.id)?; + Ok(Value::tuple(vec![primal.clone(), tangent.unwrap_or_else(|| crate::linear::zeros_like(&primal))])) } diff --git a/src/display.rs b/src/display.rs index 9695a89c..97e64876 100644 --- a/src/display.rs +++ b/src/display.rs @@ -1,4 +1,4 @@ -use crate::{Described, Error, Number, Value, keyed, settings, symbols::Notation, transform::Plain}; +use crate::{Described, Error, Number, Value, keyed, settings, symbols::Notation}; use rustymath::{buffer::FloatWidth, error::Kind}; use std::borrow::Cow; use unicode_width::UnicodeWidthStr; @@ -65,7 +65,7 @@ impl Elide { pub fn unmarked(self, n: &Number) -> String { format!("{:#}", n.rounded(self.prec)) } /// The marker of a float of `width`, when markers show and the width isn't `plain`. pub fn width_marker(self, width: FloatWidth) -> Option<&'static str> { - (self.marks && Some(width) != self.plain).then(|| crate::number::Mark::Width(width).marker()) + (self.marks && Some(width) != self.plain).then(|| crate::number::Mark::Numbers(crate::number::Numbers::Float(width)).marker()) } /// The marker for a numeric dtype and its fixedness under these display settings. pub fn kind_marker(self, kind: rustymath::buffer::DType, fixed: bool) -> Option<&'static str> { @@ -88,9 +88,15 @@ impl Elide { /// The positions display shows along an axis of `len` items: all of them, or the first and last `edges` with `None` for the gap between. pub(crate) fn positions(len: usize, edges: Option) -> impl Iterator> + Clone { - let gap = edges.filter(|&e| len > 2 * e); - let (head, tail) = gap.map_or((len, len), |e| (e, len - e)); - (0..head).map(Some).chain(gap.map(|_| None)).chain((tail..len).map(Some)) + let e = edges.unwrap_or(len); + ends(len, e, e) +} + +/// The positions of `len` items: the first `head` and the last `tail` with `None` for the gap between, or all of them where `head + tail` covers them. +pub(crate) fn ends(len: usize, head: usize, tail: usize) -> impl Iterator> + Clone { + let gap = len > head.saturating_add(tail); + let (head, tail) = if gap { (head, len - tail) } else { (len, len) }; + (0..head).map(Some).chain(gap.then_some(None)).chain((tail..len).map(Some)) } /// The index of each position display shows on the axes of `shape`, in order, with `None` for a gap. The positions under a gap collapse into it. Each comes with the axis whose index changed to reach it, or `None` for the first. @@ -194,14 +200,18 @@ impl Settings { } } +/// How `•prefs` acts (`system::Acts`): a record that sets something changes settings, an effect that ends a recording; an empty one only reads them. +pub(crate) fn prefs_acts(_: Option<&Value>, right: &Value) -> crate::system::Acts { + if right.rank() == 1 && !right.is_empty() && right.keys(0).is_some_and(|keys| keys.complete()) { crate::system::Acts::Ends } else { crate::system::Acts::Never } +} + /// `•prefs Y` applies the settings in the record `Y`, and returns all of them. Settings hold for the whole process (`settings`). The result is shy when `Y` sets something, as an assignment's is, so `•prefs ⍬` shows the settings. Besides the display settings, `mathish`, on by default, makes 64-bit floats compare with a tolerance and zero divided by zero give zero, under dynamic kinds. `float` is the width of new floats in bits, 16, 32 or 64. Until it's set, new floats take 64 bits under dynamic kinds and 32 under static kinds. Code parsed after a change reads its literals at the new width. `kinds` is `"dynamic"`, the default, or `"static"`, under which every result's kind follows from the operation, its arguments' kinds and these settings, never from values. `int` is the width of new integers under static kinds, 32 or 64. `fusion` makes each call that computes a pending argument report it. `threads` is the number of threads that large calls split across, from then on. 0 gives the default. 1 runs them on the calling thread alone. `seed`, a nonnegative integer, seeds the session's random numbers, which `¿` and a distribution's `sample` draw. The returned settings leave it out, because the stream moves on as it draws. `batch` makes Each, Rank, stencils and outer products first try one call of their operand on batches of all their cells (`batch::tried`). Where that call can't give every cell's result, they call the operand once for each cell. The results are the same either way. `batch` starts on. pub(crate) fn prefs(session: &crate::Session, right: &Value) -> Result<(Value, bool), crate::Error> { let (mut display, mut numeric, mut threads, mut seed) = (settings::display(), settings::numeric(), None, None); let mut float_set = settings::float_set(); - let (mut hanzi_aliases, mut batch, mut fusion) = (settings::hanzi_aliases(), settings::batch(), settings::reports_fusion()); + let mut hanzi_aliases = settings::hanzi_aliases(); let pairs = keyed::pairs(right).described("•prefs takes a record of settings")?; let shy = !pairs.is_empty(); - if shy { crate::transform::acts()?; } for (key, value) in pairs { if numeric.set(&key, &value, &mut float_set).map_err(|message| Error::domain(message))? { continue; } match &*key { @@ -217,8 +227,6 @@ pub(crate) fn prefs(session: &crate::Session, right: &Value) -> Result<(Value, b let n = value.as_number().and_then(|n| n.nonnegative_integer::().ok()); seed = Some(n.ok_or(Kind::Domain).described("seed must be a nonnegative whole number")?); } - "batch" => batch = value.boolean().described("batch must be Boolean")?, - "fusion" => fusion = value.boolean().described("fusion must be Boolean")?, key => { if !display.set(key, &value).described("invalid display setting")? { return Err(Error::domain("unknown setting")); } } } } @@ -226,16 +234,12 @@ pub(crate) fn prefs(session: &crate::Session, right: &Value) -> Result<(Value, b settings::set_numeric(numeric); settings::set_float_set(float_set); settings::set_hanzi_aliases(hanzi_aliases); - settings::set_batch(batch); - settings::set_reports_fusion(fusion); if let Some(n) = threads { rustymath::parallel::set_threads(n); } if let Some(seed) = seed { - *session.generator.lock().unwrap_or_else(std::sync::PoisonError::into_inner) = crate::distribution::Stream::seeded(seed); + *session.generator.lock().unwrap_or_else(std::sync::PoisonError::into_inner) = crate::distribution::Counter::seeded(seed); } - let flag = |b| Value::Number(Number::from_bool(b)); let entries = display.entries(numeric.width).into_iter().chain(numeric.entries()).chain([ - ("fusion", flag(fusion)), ("threads", Value::integer(rustymath::parallel::thread_setting() as i64)), ("hanzi_aliases", flag(hanzi_aliases)), - ("batch", flag(batch)), + ("threads", Value::integer(rustymath::parallel::thread_setting() as i64)), ("hanzi_aliases", Value::Number(Number::from_bool(hanzi_aliases))), ]); let (keys, values): (Vec>, Vec) = entries.map(|(k, v)| (k.into(), v)).unzip(); Ok((keyed::vector(keys, values).described("invalid settings record")?, shy)) @@ -291,7 +295,7 @@ impl Cell { } /// A cell holding `block`, the text of `item`. fn item(block: Block, item: &Value) -> Self { - let number = matches!(item.displayed(), Plain::Number(_)).then(|| Decimals::of(block.lines[0].chars())); + let number = matches!(item, Value::Number(_)).then(|| Decimals::of(block.lines[0].chars())); Self { block, number, nested: matches!(item, Value::Array(_)) } } pub fn height(&self) -> usize { self.block.lines.len() } @@ -374,13 +378,14 @@ fn array(a: &Value, el: Elide) -> Block { Cell::spot(spot, |i| { let item = if a.is_empty() { a.prototype() } else { a.at(i) }; chars &= matches!(item, Value::Character(_)); - let block = match item.displayed() { + let block = match &item { // An `ₓ` box already says its numbers are exact, and a width's marker gives their width. - Plain::Number(n) if Some(kind) == a.mark(el) => Block::new(el.unmarked(n)), - Plain::Number(n) => Block::new(el.number(n)), - Plain::Character(c) => Block::new(if c.is_control() { c.escape_default().to_string() } else { c.to_string() }), - Plain::Array(item) => array(item, el), - Plain::Function(_) | Plain::Operator(_) => Block::new(plain(&item, el)), + Value::Number(n) if Some(kind) == a.mark(el) => Block::new(el.unmarked(n)), + Value::Number(n) => Block::new(el.number(n)), + Value::Character(c) => Block::new(if c.is_control() { c.escape_default().to_string() } else { c.to_string() }), + Value::Array(_) => array(&item, el), + Value::Function(_) | Value::Operator(_) => Block::new(plain(&item, el)), + Value::Pending(_) | Value::Carried { .. } => unreachable!("display requires observed values"), }; let block = match a.shown_key(0, i, el) { Some(key) => block.labelled(&key), None => block }; Cell::item(block, &item) @@ -405,9 +410,10 @@ fn boxes(a: &Value) -> bool { a.shape().len() != 1 || !a.is_simple() } /// Display text for `a` without frames: a vector or unit as source text, and higher ranks as rows. pub(crate) fn plain(a: &Value, el: Elide) -> String { - match a.displayed() { - Plain::Number(n) => return el.number(n), Plain::Function(f) => return format!("⟨{}⟩", f.text(el, &mut 1000)), - Plain::Operator(op) => return op.text(el), Plain::Character(_) => return a.literal(), Plain::Array(_) => () + match a { + Value::Number(n) => return el.number(n), Value::Function(f) => return format!("⟨{}⟩", f.text(el, &mut 1000)), + Value::Operator(op) => return op.text(el), Value::Character(_) => return a.literal(), Value::Array(_) => (), + Value::Pending(_) | Value::Carried { .. } => unreachable!("display requires observed values"), } if a.has_keys() && a.shape().len() > 1 { return labelled(a, el); } if a.shape().len() <= 1 { @@ -435,9 +441,9 @@ pub(crate) fn table(a: &Value, el: Elide, breaks: bool) -> (usize, Vec<(Option Cell::text(c.into()), item => { spaced = true; - match item.displayed() { - Plain::Number(n) if mark.is_some() => Cell::number(el.unmarked(n)), Plain::Number(n) => Cell::number(el.number(n)), - Plain::Function(_) | Plain::Operator(_) => Cell::text(plain(&item, el)), _ => Cell::text(item.item(el)) + match &item { + Value::Number(n) if mark.is_some() => Cell::number(el.unmarked(n)), Value::Number(n) => Cell::number(el.number(n)), + Value::Function(_) | Value::Operator(_) => Cell::text(plain(&item, el)), _ => Cell::text(item.item(el)) } } }) diff --git a/src/distribution.rs b/src/distribution.rs index 5b80ab2e..10903f5c 100644 --- a/src/distribution.rs +++ b/src/distribution.rs @@ -1,13 +1,14 @@ use crate::execution::check; +use crate::system::HostCall; use crate::session; use crate::{ - Described, Error, Number, Span, Value, array::generated_len, element::{Stored, read_as}, keyed, number::Numeric, primitive::{numeric, pervade, real}, - system::{Call, Valence::{Ambivalent, Dyadic, Monadic}, natives}, transform::{Cause, Stop} + Described, Error, Number, Span, Value, array::generated_len, keyed, primitive::{numeric, pervade, real}, + system::{Call, Valence::{Ambivalent, Dyadic, Monadic}, natives} }; -use rand::{SeedableRng, rngs::ChaCha8Rng}; +use rustymath::random::{Items, Key, Stream}; pub(crate) use rustymath::distribution::Distribution; -use rustymath::{Arg, Unary as _, distribution::{Cdf, Invalid, Pdf, Quantile, standard}}; -use rustymath::{Float as _, buffer::FloatWidth, error::Kind, with_float_width}; +use rustymath::{distribution::{Cdf, Invalid, Pdf, Quantile, standard}}; +use rustymath::{buffer::FloatWidth, error::Kind as ErrorKind}; use std::sync::{Arc, Mutex, PoisonError}; #[derive(Clone, Copy, Debug, PartialEq)] @@ -40,8 +41,8 @@ const DISTRIBUTIONS: &[(&str, &[f64], Constructor)] = &[ const WHOLE: u64 = 1 << 53; fn parameters(right: &Value) -> Result<[f64; N], Error> { - if right.shape().len() > 1 { return Err(Error::new(Kind::Rank, "parameters must be a unit or vector")); } - if right.len() != N { return Err(Error::new(Kind::Length, format!("expected {N} distribution parameters"))); } + if right.shape().len() > 1 { return Err(Error::new(ErrorKind::Rank, "parameters must be a unit or vector")); } + if right.len() != N { return Err(Error::new(ErrorKind::Length, format!("expected {N} distribution parameters"))); } let mut result = [0.0; N]; for (item, n) in right.elements().zip(&mut result) { *n = real(&item)?; @@ -59,183 +60,172 @@ fn construct(constructor: &Constructor, right: &Value) -> Result { let [_, p] = parameters(right)?; let n = numeric(&right.at(0))?.nonnegative_integer::().described("binomial trials must be a nonnegative whole number")?; - if n > WHOLE { return Err(Error::new(Kind::Limit, "binomial trials exceed the sampler's whole-number range")); } + if n > WHOLE { return Err(Error::new(ErrorKind::Limit, "binomial trials exceed the sampler's whole-number range")); } Distribution::binomial(n, p) } Constructor::Poisson => { let [rate] = parameters(right)?; - if rate > WHOLE as f64 { return Err(Error::new(Kind::Limit, "Poisson rate exceeds the sampler's whole-number range")); } + if rate > WHOLE as f64 { return Err(Error::new(ErrorKind::Limit, "Poisson rate exceeds the sampler's whole-number range")); } Distribution::poisson(rate) } }; made.map_err(|e| Error::domain(e.to_string())) } -/// `params •distribution name`: the distribution called `name`, with `params`. Without `params`, a distribution takes its standard parameters, and one without standard parameters is DOMAIN. -pub(crate) fn distribution(left: Option<&Value>, right: &Value, _: &Span) -> Result { +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub(crate) struct Kind(u8); + +impl Kind { + fn named(name: &str) -> Option { DISTRIBUTIONS.iter().position(|(n, ..)| *n == name).map(|i| Self(i as u8)) } + pub(crate) fn make(self, parameters: &Value) -> Result { construct(&DISTRIBUTIONS[self.0 as usize].2, parameters) } + pub(crate) fn discrete(self) -> bool { matches!(DISTRIBUTIONS[self.0 as usize].2, Constructor::Bernoulli | Constructor::Binomial | Constructor::Poisson) } +} + +/// `params •distribution name`, or the named distribution's standard parameters. +pub(crate) fn distribution(HostCall { left, right, .. }: HostCall<'_>) -> Result { let name = keyed::name(right).ok_or_else(|| Error::domain("a distribution name must be a string"))?; - let Some((_, standard, constructor)) = DISTRIBUTIONS.iter().find(|(n, ..)| **n == *name) else { return Err(Error::domain("unknown distribution")) }; + let kind = Kind::named(&name).ok_or_else(|| Error::domain("unknown distribution"))?; + let standard = DISTRIBUTIONS[kind.0 as usize].1; let params = match left { Some(params) => params.clone(), None if standard.is_empty() => return Err(Error::domain(format!("the {name} distribution has no standard parameters"))), None => Value::floats(vec![standard.len()], FloatWidth::F64, standard.iter().copied()).described("invalid standard parameters")? }; - Ok(bundle(construct(constructor, ¶ms)?)) -} - -fn bundle(d: Distribution) -> Value { - let d = Arc::new(d); - let method = |name, op, valence| (name, Call::Distribution(d.clone(), op), valence); - natives([ - method("sample", Operation::Sample, Ambivalent), method("density", Operation::Density, Monadic), method("cdf", Operation::Cdf, Monadic), - method("quantile", Operation::Quantile, Monadic), - ]) + kind.make(¶ms)?; + let method = |name, op, valence| Value::Function(crate::system::made(name, Call::Distribution(kind, op), valence, params.clone())); + keyed::vector(vec!["sample".into(), "density".into(), "cdf".into(), "quantile".into()], vec![ + method("sample", Operation::Sample, Ambivalent), method("density", Operation::Density, Monadic), + method("cdf", Operation::Cdf, Monadic), method("quantile", Operation::Quantile, Monadic), + ]).described("invalid distribution") } -fn exact(n: u64) -> Value { match i64::try_from(n) { Ok(n) => Value::integer(n), Err(_) => Value::Number(num_bigint::BigInt::from(n).into()) } } - -/// `len` draws from `d`, as an array of `shape`: whole numbers in integer storage for a discrete distribution, and floats of the session's width otherwise. Draws come in blocks, with an interrupt check before each. -fn samples(d: &Distribution, shape: &[usize], len: usize, rng: &mut R) -> Result { +/// `len` draws from `d` by `items`, as an array of `shape`: whole numbers in integer storage for a discrete distribution, and floats of `width` otherwise. Draws come in blocks, with an interrupt check before each. +fn samples(d: &Distribution, shape: &[usize], len: usize, width: FloatWidth, items: &mut Items) -> Result { let mut out = vec![0.0; len]; for block in out.chunks_mut(1 << 14) { check()?; - d.samples(rng, block); + d.samples(items, block); } let made = if d.is_discrete() { Value::integers(shape.to_vec(), out.into_iter().map(|k| k as i64).collect()) } - else { Value::floats(shape.to_vec(), Numeric::current().width, out) }; + else { Value::floats(shape.to_vec(), width, out) }; made.described("invalid distribution sample") } -/// The density, cdf or quantile of `d` at each number in `right`, computed at the width of `right`'s floats, or the session's float width for other numbers. An array of compact numbers goes to rustymath as one slice, and its result keeps the array's layout. A discrete quantile is an exact integer, so it goes one number at a time, as atoms and nested arrays do. -fn evaluate(d: &Distribution, op: Operation, right: &Value, span: &Span) -> Result { - let quantile = op == Operation::Quantile; - if !right.is_atom() && !(quantile && d.is_discrete()) { - let width = right.float_width().unwrap_or(Numeric::current().width); - let computed = with_float_width!(width, F => read_as::(right).map(|x| { - if quantile && !x.iter().fold(true, |ok, p| ok & (0.0..=1.0).contains(&p.wide())) { return Err(Error::domain("probability must be in [0,1]")); } - let mut out = Vec::with_capacity(x.len()); - match op { - Operation::Density => Pdf(*d).many(Arg::Many(&x), x.len(), &mut out), Operation::Cdf => Cdf(*d).many(Arg::Many(&x), x.len(), &mut out), - _ => Quantile(*d).many(Arg::Many(&x), x.len(), &mut out) - }; - F::build(right.shape(), out).ok_or(Kind::Limit).described("distribution result exceeds array limits")?.with_layout(right.layout().clone()).described("invalid distribution result") - })); - if let Some(result) = computed { return result; } - } - pervade(None, right, &|_, e| point(d, op, real(e)?, e.float_width().unwrap_or(Numeric::current().width)), &|_, _, _| None, span) +fn kernel(d: Distribution, op: Operation, width: FloatWidth, input: rustymath::buffer::DType) -> Result { + use rustymath::facts::Formula; + let variant = rustymath::facts::Variant { dtype: rustymath::buffer::DType::Float(width), checked: true, mathish: false, wrapping: false }; + Ok(match op { + Operation::Density => Pdf(d).resolve(variant, [input])?, Operation::Cdf => Cdf(d).resolve(variant, [input])?, + Operation::Quantile => Quantile(d).resolve(variant, [input])?, Operation::Sample => return Err(Error::planning()), + }) } -/// The density, cdf or quantile of `d` at one number x, computed at `width` as an array's items are, as a float of `width`. A discrete quantile is computed in `f64` and gives an exact integer where it's finite, because its integers can pass the largest that narrower floats hold exactly. -fn point(d: &Distribution, op: Operation, x: f64, width: FloatWidth) -> Result { - let quantile = op == Operation::Quantile; - if quantile && !(0.0..=1.0).contains(&x) { return Err(Error::domain("probability must be in [0,1]")); } - if quantile && d.is_discrete() { - let y = d.quantile(x); - return Ok(if y.is_finite() { exact(y as u64) } else { Value::Number(Number::float(y, width)) }); - } - let y = with_float_width!(width, F => { let x = F::narrow(x); match op { Operation::Density => Pdf(*d).one(x), Operation::Cdf => Cdf(*d).one(x), _ => Quantile(*d).one(x) }.wide() }); - Ok(Value::Number(Number::float(y, width))) +fn evaluate(d: &Distribution, op: Operation, call: HostCall<'_>) -> Result { + use rustymath::{agreement::{Binding, Mapping, Missing, Read, ReadPlan}, buffer::{Buffer, DType, FloatBuf, Floats, View}}; + crate::array::map_leaves(call.right, |leaf| { + let width = leaf.float_width().unwrap_or(call.numeric.width); + if leaf.is_atom() { return point(d, op, real(leaf)?, width); } + let exact = op == Operation::Quantile && d.is_discrete(); + let computing = if exact { FloatWidth::F64 } else { width }; + let copied = if leaf.physical().is_none() { Some(Buffer::Floats(FloatBuf::F64(crate::data::real_items(leaf)?))) } else { None }; + let (storage, map) = match &copied { Some(buffer) => (buffer.view(), None), None => leaf.physical().expect("a compact source or a converted source") }; + let plan = ReadPlan::copy(0, storage.len(), map.cloned().unwrap_or(Mapping::Linear(1)), leaf.len(), Missing::Default); + let sources = [Binding::full(storage)]; + let mut output = Buffer::with_capacity(DType::Float(computing), leaf.len()); + kernel(*d, op, computing, storage.dtype())?.map(&[Read { sources: &sources, plan: &plan }], 0..leaf.len(), Default::default(), &mut output)?; + if exact { + let View::Floats(Floats::F64(items)) = output.view() else { unreachable!("discrete quantiles compute in f64") }; + return crate::array::Frame::of(leaf).collect(items.iter().map(|&n| whole(n, width)), Value::integer(0)).map_err(Error::from); + } + Value::from_buffer(leaf.shape(), output)?.with_layout(leaf.layout().clone()).described("invalid distribution result") + }) } -pub(crate) fn call(d: &Distribution, op: Operation, left: Option<&Value>, right: &Value, span: &Span) -> Result { - if op != Operation::Sample { return evaluate(d, op, right, span); } - if right.shape().len() > 1 { return Err(Error::new(Kind::Rank, "sample shape must be a unit or vector")); } - let shape = right.as_items().nonnegative_integers().described("invalid sample dimension")?; - let len = generated_len(&shape)?; - let Some(left) = left else { return drawn(None, |rng| samples(d, &shape, len, rng)); }; - let generator = generator_of(left).ok_or_else(|| Error::domain("sample takes a •rand generator on the left"))?; - drawn(Some(&generator), |rng| samples(d, &shape, len, rng)) +fn whole(n: f64, width: FloatWidth) -> Value { + if n.is_finite() { Value::Number(Number::from(i128::from(n as u64))) } else { Value::Number(Number::float(n, width)) } } -/// A stream of random numbers: the session's, which `¿` and a distribution's `sample` draw, or one that `•rand` makes. Every copy of a `•rand` record draws from the same stream. -pub(crate) type Generator = Arc>; - -/// A generator's state. Draws take its numbers in the order they run. A map's cells draw in turn, and a batched run takes every cell's numbers for one draw at once, in cell order. A draw that each cell makes once gives the same numbers whether or not the map batches, except under a guard that splits the cells, where each side draws for its own cells in turn. -#[derive(Debug)] -pub(crate) struct Stream { rng: ChaCha8Rng } - -impl Default for Stream { - /// The session's stream until a program sets a seed, seeded by the operating system. - fn default() -> Self { Self { rng: rand::make_rng() } } -} - -impl Stream { - /// A stream seeded by `seed`. - pub(crate) fn seeded(seed: u64) -> Self { Self { rng: ChaCha8Rng::seed_from_u64(seed) } } - /// The stream's state: its seed, its stream number and how far its draws have reached. - fn state(&self) -> State { (self.rng.get_seed(), self.rng.get_stream(), self.rng.get_word_pos()) } - /// Puts the stream back in `state`. - fn restore(&mut self, (seed, stream, position): State) { - self.rng = ChaCha8Rng::from_seed(seed); - self.rng.set_stream(stream); - self.rng.set_word_pos(position); - } +fn point(d: &Distribution, op: Operation, x: f64, width: FloatWidth) -> Result { + let exact = op == Operation::Quantile && d.is_discrete(); + let computing = if exact { FloatWidth::F64 } else { width }; + let y = kernel(*d, op, computing, rustymath::buffer::DType::Float(computing))?.real(&[x], Default::default())?; + Ok(if exact { whole(y, width) } else { Value::Number(Number::float(y, width)) }) } -/// A stream's state (`Stream::state`). -type State = ([u8; 32], u64, u128); +pub(crate) fn call(kind: Kind, op: Operation, parameters: &Value, call: HostCall<'_>) -> Result { + if op == Operation::Sample { return Err(Error::planning()); } + evaluate(&kind.make(parameters)?, op, call) +} -/// Each generator that the running batched try has drawn from, with its state before the try first drew from it (`Session::drawn`). Where the try falls back, `settle` puts each state back, so the run over cells draws the numbers it would draw had no try run. -pub(crate) type Drawn = Mutex>; +#[derive(Debug)] +pub(crate) struct Generating { counter: Mutex } -/// Ends a batched try's record of its draws, putting each generator's state back where the try fell back (`batch::tried`). -pub(crate) fn settle(drawn: &Drawn, fell_back: bool) { - let drawn = std::mem::take(&mut *drawn.lock().unwrap_or_else(PoisonError::into_inner)); - if fell_back { for (generator, state) in drawn { locked(&generator).restore(state); } } +impl Generating { + /// The generator's counter, locked: its key and count. + pub(crate) fn lock(&self) -> std::sync::LockResult> { self.counter.lock() } } -fn locked(generator: &Generator) -> std::sync::MutexGuard<'_, Stream> { generator.lock().unwrap_or_else(PoisonError::into_inner) } +/// A shared generator: the session's, which `¿` and a distribution's `sample` draw from, or one that `•rand` makes. +pub(crate) type Generator = Arc; -/// `draw` drawing from `generator`, or from the session's generator where it's `None`, given the id and frame of the batched run where one is running. In a batched run, it first records the generator's state, once for each try (`Drawn`). Under a trace, it stops the trace first (`program::trace::each_call`). -fn drawing( - generator: Option<&Generator>, draw: impl FnOnce(Option<(u64, &crate::array::Layout)>, &mut ChaCha8Rng) -> Result) -> Result { - crate::program::trace::each_call("a random draw")?; - let generator = generator.unwrap_or(&session().generator); - let mut stream = locked(generator); - let batched = session().batched(); - if batched.is_some() { - let mut drawn = session().drawn.lock().unwrap_or_else(PoisonError::into_inner); - if !drawn.iter().any(|(g, _)| Arc::ptr_eq(g, generator)) { drawn.push((generator.clone(), stream.state())); } - } - draw(batched.as_ref().map(|(level, frame)| (*level, frame)), &mut stream.rng) -} +pub(crate) use rustymath::random::Counter; -/// `f` drawing from `generator`, as `drawn_cells` does, where `f` doesn't depend on the cell. -pub(crate) fn drawn(generator: Option<&Generator>, f: impl Fn(&mut ChaCha8Rng) -> Result) -> Result { - drawn_cells(generator, |_, rng| f(rng)) -} +/// The session's generator until a program sets a seed, keyed by the operating system. +pub(crate) fn fresh() -> Generator { Arc::new(Generating { counter: Mutex::new(Counter { key: Key(rand::random()), count: 0 }) }) } -/// `f` drawing from `generator`, or from the session's generator where it's `None`, where `f` also takes the cell it draws for. Outside a batched run, `f` draws once, for cell 0. In a batched run, `f` draws for each cell of the run in turn (`batch::each_cell`). `primitive::drawn_value` uses the cell to give each draw that cell of each batched argument (`batch::cell`). -pub(crate) fn drawn_cells(generator: Option<&Generator>, f: impl Fn(usize, &mut ChaCha8Rng) -> Result) -> Result { - drawing(generator, |batched, rng| match batched { Some((level, frame)) => crate::batch::each_cell(level, frame, |c| f(c, rng)), None => f(0, rng) }) -} +fn locked(generator: &Generator) -> std::sync::MutexGuard<'_, Counter> { generator.lock().unwrap_or_else(PoisonError::into_inner) } -/// `f` drawing from `generator` on `value`, where `f` draws for each item of `value` in turn and keeps its layout, as a roll does. In a batched run, `f` draws once, on `value` laid over the run's frame (`batch::whole_over`). That gives each cell the numbers it gets when the cells draw in turn. A value that can't be laid over the frame draws cell by cell (`batch::each_cell`). -pub(crate) fn drawn_items(generator: Option<&Generator>, value: &Value, f: impl Fn(&Value, &mut ChaCha8Rng) -> Result) -> Result { - drawing(generator, |batched, rng| { - let Some((level, frame)) = batched else { return f(value, rng) }; - match crate::batch::whole_over(value.clone(), level, frame) { - Some(whole) => crate::batch::result(level, f(&whole, rng)?, frame.shape().len(), || true, Stop::new(level, Cause::Rule)), - None => crate::batch::each_cell(level, frame, |c| f(&crate::batch::cell(value, c)?, rng)), - } - }) -} +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum Law { Roll, Deal } -#[derive(Clone, Copy, Debug, PartialEq)] -pub(crate) enum Draw { Roll, Deal } -/// `•rand seed` returns a record of `roll` and `deal`, which draw from one stream seeded by `seed`. -pub(crate) fn generator(_: Option<&Value>, right: &Value, _: &Span) -> Result { +pub(crate) fn counter(generator: &Generator) -> Counter { *locked(generator) } + +pub(crate) fn set_count(generator: &Generator, count: u64) { locked(generator).count = count; } + +pub(crate) fn generator(HostCall { right, .. }: HostCall<'_>) -> Result { // Each call makes a new stream, as each cell of a run cell by cell makes its own. - crate::transform::acts()?; - if right.shape().len() > 1 { return Err(Error::new(Kind::Rank, "•rand needs a unit or vector seed")); } - if !right.is_singleton() { return Err(Error::new(Kind::Length, "•rand needs one seed")); } + if right.shape().len() > 1 { return Err(Error::new(ErrorKind::Rank, "•rand needs a unit or vector seed")); } + if !right.is_singleton() { return Err(Error::new(ErrorKind::Length, "•rand needs one seed")); } let seed = numeric(&right.at(0))?.nonnegative_integer::().described("•rand needs a nonnegative whole number as its seed")?; - let rng = Arc::new(Mutex::new(Stream::seeded(seed))); - Ok(natives([("roll", Call::Generator(rng.clone(), Draw::Roll), Monadic), ("deal", Call::Generator(rng, Draw::Deal), Dyadic)])) + let generator = Arc::new(Generating { counter: Mutex::new(Counter::seeded(seed)) }); + Ok(natives([("roll", Call::Generator(Some(generator.clone()), Law::Roll), Monadic), ("deal", Call::Generator(Some(generator), Law::Deal), Dyadic)])) } /// The stream behind a record from `•rand`. fn generator_of(value: &Value) -> Option { let Value::Function(f) = keyed::field(value, "roll")? else { return None; }; - match &f.system_function()?.call { Call::Generator(generator, _) => Some(generator.clone()), _ => None } + match &f.system_function()?.call { Call::Generator(generator, _) => generator.clone(), _ => None } +} + +pub(crate) fn snapshot(operation: &crate::primitive::Operation, arguments: &[Value]) -> Result, Error> { + if !matches!(operation, crate::primitive::Operation::Draw { .. }) { return Ok(None); } + let Some(Value::Function(function)) = arguments.first() else { return Err(Error::planning()); }; + let generator = match function.system_function().map(|f| &f.call) { + Some(Call::Generator(Some(generator), _)) => generator.clone(), + Some(Call::Generator(None, _)) => return Err(Error::planning()), + Some(Call::Distribution(_, Operation::Sample)) => match arguments.len() { + 2 => session().generator.clone(), + 3 => generator_of(&arguments[1]).ok_or_else(|| Error::domain("sample takes a •rand generator on the left"))?, + _ => return Err(Error::planning()), + }, + None if function.as_primitive() == Some(crate::primitive::Primitive::Random) => session().generator.clone(), + _ => return Err(Error::planning()), + }; + reserve(&generator, 1).map(Some) +} +pub(crate) fn reserve(generator: &Generator, draws: u64) -> Result<[Value; 2], Error> { + let mut counter = locked(generator); + let count = counter.count; counter.count = counter.count.wrapping_add(draws); + let key = counter.key.0.chunks_exact(8).map(|word| i64::from_le_bytes(word.try_into().unwrap())).collect(); + Ok([Value::integers([4], key)?, Value::integer(count as i64)]) +} + +pub(crate) fn stream(key: &Value, count: &Value, place: u64) -> Result { + if key.len() != 4 { return Err(Error::planning()); } + let mut bytes = [0; 32]; + for (word, value) in bytes.chunks_exact_mut(8).zip(key.elements()) { + word.copy_from_slice(&value.as_number().ok_or_else(Error::planning)?.integer()?.to_le_bytes()); + } + let count = count.as_number().ok_or_else(Error::planning)?.integer()? as u64; + Ok(Key(bytes).stream(count.wrapping_add(place))) } diff --git a/src/element.rs b/src/element.rs deleted file mode 100644 index a74107b8..00000000 --- a/src/element.rs +++ /dev/null @@ -1,170 +0,0 @@ -//! How the element types of compact storage read their items from a `Value` and build arrays from them. Each type's fill and its conversions to other types are rustymath's `Element` and `Source`, and `Converts` for the conversions that marks make. -use crate::{Value, array::Storage, number::{Numeric, int::Int}}; -use num_complex::{Complex32, Complex64}; -use rustymath::{ - buffer::{ComplexBuf, Complexes, Element, FloatWidth, IntBuf, Ints, Source, View, Width, read_all}, tolerant::{Exactly, Tolerant}, - with_complexes, with_floats, with_ints -}; -use std::borrow::Cow; - -/// An element type of compact storage, read from and built into `Value`s. -pub(crate) trait Stored: Element { - /// The items of `value`, when its storage has this type. - fn slice(value: &Value) -> Option<&[Self]> { Self::of(value.view()?) } - fn of_ints_mut(_ints: &mut IntBuf) -> Option<&mut [Self]> { None } - /// An array of `data`, stored as this element type. Integers keep their width. - fn build(shape: impl Into, data: Vec) -> Option { Value::from_storage(shape, Storage::Compact(Self::buffer(data))).ok() } - /// An array of `data` under `numeric`'s kinds. Only complex numbers build differently: dynamic kinds narrow them to floats where no item has an imaginary part, and static kinds keep them complex. - fn build_at(shape: impl Into, data: Vec, numeric: Numeric) -> Option { - if numeric.fixed.is_some() { Value::from_buffer_at(shape, Self::buffer(data), numeric.fixed).ok() } else { Self::build(shape, data) } - } - /// An array of `data`, with integers moved to the narrowest width that holds them, in a pass over them. Other element types are stored as `build` stores them. - fn build_narrowed(shape: impl Into, data: Vec) -> Option { Self::build(shape, data) } - fn build_narrowed_at(shape: impl Into, data: Vec, numeric: Numeric) -> Option { - if numeric.fixed.is_some() { Self::build_at(shape, data, numeric) } else { Self::build_narrowed(shape, data) } - } - /// This whole number as an integer of type `T`, when `T` holds it. Only the numbers that kernels give as integers have one. - fn whole(self) -> Option { None } - /// This number as an integer of type `T`, as Rust's `as` converts it (`rustymath::convert::saturated`), which static kinds use for kernels' results. Only floats have one. - fn saturated(self) -> Option { None } - /// `items` read as this element type, which only the complex element types can do. - fn from_complexes(_items: Complexes<'_>) -> Option> { None } - /// The real number `x` as this element type, which only the float and complex types have. A survey of the kernel tables gives them their constant arguments this way. - fn real(_x: f64) -> Option { None } -} - -impl Stored for bool {} -/// Complex numbers of each width. -macro_rules! complex_elements { - ($($t:ty: $width:ident),+) => {$( - impl Stored for $t { - fn build(shape: impl Into, data: Vec) -> Option { Value::complex(shape, ComplexBuf::$width(data)).ok() } - fn build_at(shape: impl Into, data: Vec, numeric: Numeric) -> Option { - if numeric.is_static() { Value::from_storage(shape, Storage::Compact(Self::buffer(data))).ok() } else { Self::build(shape, data) } - } - fn from_complexes(items: Complexes<'_>) -> Option> { Some(with_complexes!(items, |z| read_all(z))) } - fn real(x: f64) -> Option { Some(Self::new(x as _, 0.0)) } - } - )+}; -} -complex_elements!(Complex32: F32, Complex64: F64); -impl Stored for char {} - -/// An integer width of compact storage. -pub(crate) trait Whole: Stored + Int + rustymath::convert::Int + rustymath::reduce::Term + bytemuck::Pod { const WIDTH: Width; } -macro_rules! whole { - ($($t:ty: $width:ident),+) => {$( - impl Stored for $t { - fn of_ints_mut(ints: &mut IntBuf) -> Option<&mut [Self]> { if let IntBuf::$width(v) = ints { Some(v) } else { None } } - fn build_narrowed(shape: impl Into, data: Vec) -> Option { Value::from_storage(shape, Storage::narrowed(data)).ok() } - #[inline] - fn whole(self) -> Option { T::narrowed(self.to_i64()) } - } - impl Whole for $t { const WIDTH: Width = Width::$width; } - )+}; -} -whole!(u8: U8, i16: I16, i32: I32, i64: I64); - -/// A float width of compact storage. Kernels and the `Number` path compute at the width itself, with rustymath's functions, such as `exp`, `ln` and `gamma`, and `num_traits::Float`'s others. Kernels exist only for f32 and f64. `pervasive::halves` runs 16-bit floats at 32 bits. A function with no version at the width, such as tolerant gcd, computes at `f64` and rounds. `rustymath::Float::Working` is the type that `⌹` and `•decompose` compute in: f32 for f16, and the type itself otherwise. -pub(crate) trait Float: Stored + rustymath::Float { - const WIDTH: FloatWidth; - /// The signed integer width with this float's bytes, which monadic `⌊` and `⌈` write their results at first. - const INTEGERS: Width; - /// `value`'s compact numbers as this float type: borrowed when they have it already, otherwise read through `Source`. `None` for complex numbers, characters and mixed arrays. - fn compact(value: &Value) -> Option>; -} -macro_rules! float { - ($($t:ident: $width:ident, $integers:ident),+) => {$( - impl Stored for $t { - #[inline] - fn whole(self) -> Option { rustymath::convert::int(self) } - #[inline] - fn saturated(self) -> Option { Some(rustymath::convert::saturated(self)) } - fn real(x: f64) -> Option { Some(x as $t) } - } - impl Float for $t { - const WIDTH: FloatWidth = FloatWidth::$width; - const INTEGERS: Width = Width::$integers; - fn compact(value: &Value) -> Option> { read_as(value) } - } - )+}; -} -float!(f32: F32, I32, f64: F64, I64); -impl Stored for half::f16 { - #[inline] - fn whole(self) -> Option { rustymath::convert::int(self) } - #[inline] - fn saturated(self) -> Option { Some(rustymath::convert::saturated(self)) } -} -/// `half` computes each operation on 16-bit floats in f32 and rounds the result. Its conversions take one value at a time, through inline assembly that LLVM can't vectorize. Loops that convert 16-bit floats, such as the copies that `pervasive::halves` makes, therefore run one item at a time on the CPU. Eight-lane `fcvtl` and `fcvtn` assembly would vectorize them, as Rust's own `f16` would once it's stable. 16-bit floats needn't be fast on the CPU. -impl Float for half::f16 { - const WIDTH: FloatWidth = FloatWidth::F16; - const INTEGERS: Width = Width::I16; - fn compact(value: &Value) -> Option> { read_as(value) } -} - -/// `ints` at width `A`: borrowed when they have it already, otherwise converted. `A` must hold every item. -pub(crate) fn cast(ints: Ints<'_>) -> Cow<'_, [A]> { - if let Some(v) = A::of(View::Integers(ints)) { return Cow::Borrowed(v); } - with_ints!(ints, |v| Cow::Owned(v.iter().map(|&n| A::from_i64(n.to_i64())).collect())) -} - -/// Booleans as unsigned bytes of 0 and 1, read in place with no copy. -pub(crate) fn truth_bytes(truths: &[bool]) -> &[u8] { bytemuck::cast_slice(truths) } - -/// `value`'s compact numbers as `A`s: borrowed when they are `A`s already, otherwise read through `Source`. Complex numbers read only as complex numbers. `None` when `A` can't hold them. -pub(crate) fn read_as(value: &Value) -> Option> where bool: Source, u8: Source, i16: Source, i32: Source, i64: Source, - f64: Source, f32: Source, half::f16: Source { - if let Some(items) = A::slice(value) { return Some(Cow::Borrowed(items)); } - Some(Cow::Owned(match value.view()? { - View::Booleans(y) => read_all(y), View::Integers(ints) => with_ints!(ints, |y| read_all(y)), View::Floats(y) => with_floats!(y, |y| read_all(y)), - View::Complexes(z) => A::from_complexes(z)?, View::Characters(_) => return None - })) -} - -/// An item that converts to type `T` as fixed marks, width marks and programs' conversions convert it, where `t` is the tolerance of a float's floor. A float target reads the item as `Source` reads it. An integer target takes a Boolean as 0 or 1, wraps an integer to its width, and takes the floor of a float within `t`, saturated at its range as Rust's `as` saturates. An exact fraction converts exactly: a whole one wraps as an integer does, and any other takes its floor, saturated. -pub(crate) trait Converts: Copy { fn converted(self, t: impl Tolerant) -> T; } - -/// Writes `Converts` into the float types from each item type listed, through `Source`. -macro_rules! into_floats { - ($($s:ty),+) => {$( - impl Converts for $s { #[inline] fn converted(self, _: impl Tolerant) -> f32 { Source::::read(self) } } - impl Converts for $s { #[inline] fn converted(self, _: impl Tolerant) -> f64 { Source::::read(self) } } - )+}; -} -into_floats!(bool, u8, i16, i32, i64, half::f16, f32, f64); - -/// Writes `Converts` into each integer type listed, from Booleans, integers, floats and exact fractions. -macro_rules! into_wholes { - (@ints $t:ty: $($s:ty),+) => {$( - impl Converts<$t> for $s { #[inline] fn converted(self, _: impl Tolerant) -> $t { self as $t } } - )+}; - (@floats $t:ty: $($s:ty),+) => {$( - impl Converts<$t> for $s { #[inline] fn converted(self, t: impl Tolerant) -> $t { rustymath::convert::saturated::<$t, f64>(t.floor(f64::from(self))) } } - )+}; - ($($t:ty),+) => {$( - impl Converts<$t> for bool { #[inline] fn converted(self, _: impl Tolerant) -> $t { <$t>::from(self) } } - into_wholes!(@ints $t: u8, i16, i32, i64); - into_wholes!(@floats $t: half::f16, f32, f64); - impl Converts<$t> for &num_rational::BigRational { - fn converted(self, _: impl Tolerant) -> $t { - use num_traits::{Signed, ToPrimitive}; - if self.is_integer() { return crate::number::low_bits(self.numer()) as $t; } - let floor = self.floor().to_integer(); - floor.to_i64().map_or(if floor.is_negative() { <$t>::MIN } else { <$t>::MAX }, |n| n.clamp(<$t>::MIN as i64, <$t>::MAX as i64) as $t) - } - } - )+}; -} -into_wholes!(u8, i16, i32, i64); - -/// The numbers of `view` converted to `T` (`Converts`), each float floored within the tolerance that `numeric` gives its width. `None` for complex numbers and characters. -pub(crate) fn converted(view: View<'_>, numeric: Numeric) -> Option> where bool: Converts, u8: Converts, i16: Converts, i32: Converts, - i64: Converts, half::f16: Converts, f32: Converts, f64: Converts { - fn each, T>(items: &[S], t: impl Tolerant) -> Vec { items.iter().map(|&x| x.converted(t)).collect() } - Some(match view { - View::Booleans(v) => each(v, Exactly), View::Integers(ints) => with_ints!(ints, |v| each(v, Exactly)), - View::Floats(v) => rustymath::with_tolerance!(numeric.tolerance_at(v.tag()), |t| with_floats!(v, |v| each(v, t))), - View::Complexes(_) | View::Characters(_) => return None - }) -} diff --git a/src/error.rs b/src/error.rs index c52f0342..613f4c13 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,6 +2,8 @@ use rustymath::error::Kind; use std::{fmt, ops::Range, path::PathBuf, sync::Arc}; use unicode_width::UnicodeWidthStr; +pub(crate) type Result = std::result::Result; + /// Shared source identity and text. Syntax and errors retain only the sources they use. #[derive(Debug)] /// `file` marks a source read from a file. Its `name` is then the file's path. @@ -16,6 +18,9 @@ impl Source { #[derive(Clone, Debug)] pub struct Span { pub source: Arc, pub range: Range } +/// Two spans are equal where they cover the same bytes of one source. +impl PartialEq for Span { fn eq(&self, other: &Self) -> bool { Arc::ptr_eq(&self.source, &other.source) && self.range == other.range } } + #[derive(Clone, Debug)] pub enum ErrorKind { Syntax, @@ -24,8 +29,10 @@ pub enum ErrorKind { Unsupported, Interrupt, Timeout, - /// A batched run's stop, which holds the id of the level that owns the run and whether a later run of the same operand would stop again (`transform::Stop`). Only that level catches it. - Stop(crate::transform::Stop), + /// Only the matching map boundary consumes this tag. + BatchStop(usize), + /// A stand-in was observed while planning; BPL guards never catch it. + Planning, /// An error of a kind that rustymath's functions on arrays share: Length, Rank, Index, Limit, Domain or Overflow. Array(rustymath::error::Error), /// A kind that a program names when it signals an error, in upper case. @@ -49,7 +56,8 @@ impl ErrorKind { Self::Unsupported => "UNSUPPORTED", Self::Interrupt => "INTERRUPT", Self::Timeout => "TIMEOUT", - Self::Stop { .. } => "STOP", + Self::BatchStop(_) => "STOP", + Self::Planning => "RETRACE", Self::Array(e) => match e.kind { Kind::Domain => "DOMAIN", Kind::Length => "LENGTH", Kind::Limit | Kind::Overflow => "LIMIT", Kind::Rank => "RANK", Kind::Index => "INDEX", Kind::Interrupt => "INTERRUPT", Kind::Timeout => "TIMEOUT" @@ -63,24 +71,24 @@ impl ErrorKind { let name = name.to_uppercase(); Some(Self::builtin().into_iter().find(|k| k.name() == name).unwrap_or_else(|| Self::Custom(name.into()))) } - /// Whether an error of this kind controls evaluation rather than reporting a failure. An interrupt or a timeout ends the evaluation. A batched run's stop sends its level back to running cell by cell. A handler that catches every error lets these pass. - pub(crate) fn is_control(&self) -> bool { matches!(self, Self::Interrupt | Self::Timeout | Self::Stop { .. }) } + /// Whether an error of this kind controls evaluation rather than reporting a failure. An interrupt or a timeout ends the evaluation. A batched run's stop sends its level back to running cell by cell. `Again` compiles its statement again, and `Retrace` traces its call again. A handler that catches every error lets these pass. + pub(crate) fn is_control(&self) -> bool { matches!(self, Self::Interrupt | Self::Timeout | Self::BatchStop(_) | Self::Planning) } } impl From for ErrorKind { fn from(e: rustymath::error::Error) -> Self { Self::Array(e) } } /// A kind with no message, for `Error::new` or `described` to supply one. impl From for ErrorKind { fn from(kind: Kind) -> Self { Self::Array(rustymath::error::Error::new(kind, "")) } } -/// `DOMAIN ERROR` and so on. Interrupts, timeouts, stops and unsupported features show their names alone. +/// `DOMAIN ERROR` and so on. Interrupts, timeouts, stops, `Retrace` and unsupported features show their names alone. impl fmt::Display for ErrorKind { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Unsupported | Self::Interrupt | Self::Timeout | Self::Stop { .. } => f.write_str(self.name()), _ => write!(f, "{} ERROR", self.name()) + Self::Unsupported | Self::Interrupt | Self::Timeout | Self::BatchStop(_) | Self::Planning => f.write_str(self.name()), _ => write!(f, "{} ERROR", self.name()) } } } -/// An error: its kind, message and span, and the calls it passed through. Boxing its fields keeps each `Result` to one pointer on the stack that deep recursion uses. +/// An error: its kind, message and span, and the calls it passed through. Boxing its fields keeps each `std::result::Result` to one pointer on the stack that deep recursion uses. #[derive(Clone, Debug)] pub struct Error(Box); @@ -88,6 +96,9 @@ pub struct Error(Box); #[derive(Clone, Debug)] pub struct ErrorData { pub kind: ErrorKind, pub message: String, pub span: Option, pub calls: Vec } +/// The Interfaces section's one-parameter `Result`, over `Error`. +pub(crate) type Result = std::result::Result; + impl std::ops::Deref for Error { type Target = ErrorData; fn deref(&self) -> &ErrorData { &self.0 } @@ -117,11 +128,14 @@ impl Error { #[cold] #[inline(never)] pub(crate) fn at(mut self, span: &Span) -> Self { if self.span.is_none() { self.span = Some(span.clone()); } self } + pub(crate) fn batch_stop(level: usize) -> Self { Self::new(ErrorKind::BatchStop(level), "host read of a batched value") } + pub(crate) fn batch_level(&self) -> Option { match &self.kind { ErrorKind::BatchStop(level) => Some(*level), _ => None } } + pub(crate) fn planning() -> Self { Self::new(ErrorKind::Planning, "planning input has no items") } + pub(crate) fn is_planning(&self) -> bool { matches!(self.kind, ErrorKind::Planning) } } impl From for Error { fn from(e: rustymath::error::Error) -> Self { Self::new(e, "") } } /// A batched run's stop. Only the level that owns the run catches it, so its span never shows. -impl From for Error { fn from(stop: crate::transform::Stop) -> Self { Self::new(stop, crate::transform::STOPPED) } } /// A message from numeric code, which reports a DOMAIN error as its message alone. impl From<&'static str> for Error { fn from(message: &'static str) -> Self { Self::domain(message) } } @@ -136,16 +150,16 @@ impl Span { match std::path::Path::new(&self.source.name).parent() { Some(dir) if relative && self.source.file => dir.join(path), _ => path.into() } } /// The bytes of the file `path` names. - pub(crate) fn read(&self, path: &str) -> Result, Error> { crate::host::read(&self.path(path)).map_err(|e| Error::io(path, e)) } + pub(crate) fn read(&self, path: &str) -> std::result::Result, Error> { crate::host::read(&self.path(path)).map_err(|e| Error::io(path, e)) } } /// Gives an error kind its message, where the kind carries none. An `ErrorKind` that wraps a rustymath error keeps that error's message. -pub(crate) trait Described { fn described(self, message: &str) -> Result; } -impl> Described for Result { fn described(self, message: &str) -> Result { self.map_err(|k| Error::new(k, message)) } } +pub(crate) trait Described { fn described(self, message: &str) -> std::result::Result; } +impl> Described for std::result::Result { fn described(self, message: &str) -> std::result::Result { self.map_err(|k| Error::new(k, message)) } } /// Places an error at a source span, where it has no span yet. -pub(crate) trait At { fn at(self, span: &Span) -> Result; } -impl> At for Result { fn at(self, span: &Span) -> Result { self.map_err(|e| e.into().at(span)) } } +pub(crate) trait At { fn at(self, span: &Span) -> std::result::Result; } +impl> At for std::result::Result { fn at(self, span: &Span) -> std::result::Result { self.map_err(|e| e.into().at(span)) } } // Render tabs at four-column stops. Escape other controls rather than sending them to the terminal. Source spans themselves remain unchanged UTF-8 byte offsets. fn display_text(text: &str) -> String { diff --git a/src/eval.rs b/src/eval.rs index da503d94..f0a613d8 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -1,101 +1,364 @@ use crate::execution::check; use crate::{ - At, Described, Error, ErrorKind, Output, ParseStatus, Parsed, Source, Span, Value, agreement::{Agree, Agreement}, - array::{Frame as ResultFrame, Gather, Lanes, generated_len}, display::Elide, number::Numeric, - primitive::{Builtin, FoldKind, OperandKind, Operation, OperatorKind, Primitive, Selection, Superscript, Targets}, symbols::Notation, - syntax::{Definition, DefinitionKind, ListForm, Node, NodeKind, StatementKind} + At, Described, Error, ErrorKind, Output, ParseStatus, Parsed, Source, Span, Value, + display::Elide, graph::Pending, number::{Mark, Numeric}, + primitive::{Builtin, Operation, Primitive, Superscript}, symbols::Notation, + syntax::{Definition, DefinitionKind, ListForm, Node, NodeKind, StatementKind}, system::SystemFunction }; -use crate::{name::Name, transform::Plain}; +use crate::{array::TreeRoot, name::Name}; use rustymath::hashing::{HashMap, HashMapExt}; use rustymath::{agreement::Mapping, error::Kind}; -use std::{borrow::Cow, convert::Infallible, sync::Arc}; +use std::{borrow::Cow, convert::Infallible, hash::Hasher, sync::Arc}; mod assign; mod binder; mod bindings; mod dissect; -mod rewrite; -mod operators; +pub(crate) mod operators; mod session; mod tree; -pub(crate) use binder::before; pub(crate) use bindings::Place; -pub(crate) use operators::{cell_constant, fold, folded_along, folded_cells, matrices, primitive_expansion, primitive_fold, primitive_product, whole_cells}; -pub(crate) use rewrite::{combined, mapping_patterns, rewrites}; -pub(crate) use tree::Recording; -use {binder::*, bindings::{Bindings, Frame, FrameBindings}, dissect::*, operators::*}; +pub(crate) use operators::primitive_expansion; +pub(crate) use tree::before; +use tree::{atop, bound_right, derived, tails}; +use {binder::*, bindings::{Bindings, Frame}, dissect::*, operators::*}; +/// Part 19: a function value, a shared handle to its data. +#[derive(Clone)] +pub struct Function(pub(crate) Arc); +impl std::fmt::Debug for Function { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.text(Elide::NONE, &mut 40)) } } + +/// One positional recipe for interpreter bindings and recorded function values. #[derive(Clone, Debug)] -pub struct Function { node: Arc } - -#[derive(Debug)] -/// A function's node, with what `Function::new` works out from it once. `rewritten` holds the functions that compute its monadic and dyadic calls through a fast combination (`rewrite`), and `needs` the mask of the primitives and operators it's built from (`rewrite::built_from`). -struct FunctionData { kind: FunctionNode, environment: Option, late: bool, rewritten: [Option; 2], needs: u64, owns: bool } - -/// Compares the values held in functions and operators: exactly for `==`, or by `≡`'s rule for `matches`. Its flag marks a datum, an operator's array operand (`is_data`), as against an array that acts as a function. -type ValueRule<'a, E> = dyn FnMut(&Value, &Value, bool) -> Result + 'a; -fn same_values(x: &Value, y: &Value, _: bool) -> Result { Ok(x.same(y)) } -/// Whether two operator applications' operands match, with `values` comparing the values in them. -fn operands_equal(a: &Operand, b: &Option, x: &Operand, y: &Option, values: &mut ValueRule<'_, E>) -> Result { - Ok(a.equal_by(x, values)? && match (b, y) { (Some(b), Some(y)) => b.equal_by(y, values)?, (b, y) => b.is_none() && y.is_none() }) +pub(crate) enum FunctionBinding { + Input(I), + Rebuild { template: Function, dependencies: D }, } -/// Whether a program takes datum `a` as an input, as it takes a free name that holds an array: where a program can hold it (`program::Desc::describes`). A datum is an operator's array operand, such as the rank in `f⍤k`, or the array that a function binds as its left argument (`a↣f`) or its right (`f↢a`). A train binds each array tine this way, so `w×⊢` is `(w↣×)∘⊢`. A program holds any other datum, and an array that acts as a function, as part of the function's structure. -fn is_data(a: &Value) -> bool { crate::program::Desc::describes(a) } -/// What `Function::structure` puts in place of each datum that a program takes as an input (`is_data`): the number 0. A structure keeps any other datum as it is. A program can take this number, so a structure holds it only in place of a datum, and `Function::has_structure` tells a placeholder by its value. -fn placeholder() -> crate::Number { crate::Number::from_integer(0) } +/// A function's datum, or its position in the call's canonical capture tail. +#[derive(Clone, Debug)] +pub(crate) enum FunctionOperand { Value { value: Value, literal: bool }, Input { position: TreeRoot, literal: bool } } + +/// A call-time capture. Its place identifies aliases updated by this call. +pub(crate) struct Capture { pub(crate) name: Option, pub(crate) place: Option, pub(crate) value: Value, pub(crate) literal: bool } -/// What `Function::rebuilt` puts in each part of a function's node: a function, and a datum (`is_data`), with the span that an error in the rebuilt function points at. -trait Rebuild { - fn function(&mut self, f: &Function) -> Result; - fn datum(&mut self, a: &Value) -> Result; - fn span(&self, f: &Function) -> Span; +impl FunctionOperand { + pub(crate) fn value(value: Value, literal: bool) -> Self { Self::Value { value, literal } } + pub(crate) fn literal(&self) -> bool { match self { Self::Value { literal, .. } | Self::Input { literal, .. } => *literal } } + pub(crate) fn stored(&self) -> Option<&Value> { match self { Self::Value { value, .. } => Some(value), Self::Input { .. } => None } } + fn observed(&self, read: crate::ReadKind) -> Result { + match self { Self::Value { value, literal } => Ok(Self::value(value.read(read)?, *literal)), + Self::Input { .. } => Err(Error::domain("a callable template must be bound before observation")) } + } + fn matches(&self, other: &Self, numeric: Numeric) -> Result { + if self.literal() != other.literal() { return Ok(false); } + match (self.stored(), other.stored()) { (Some(x), Some(y)) => x.matches_within(y, numeric), + _ => Err(Error::domain("a callable template must be bound before comparison")) } + } + pub(crate) fn resolve<'a>(&'a self, origin: &'a crate::program::Origin<&'a [Value]>) -> Cow<'a, Value> { + match self { Self::Value { value, .. } => Cow::Borrowed(value), Self::Input { position, .. } => Function::capture_at(origin, position) } + } + pub(crate) fn resolve_in_call<'a>(&'a self, call: &'a crate::primitive::CallView<'_>, read: crate::ReadKind) -> Result, Error> { + match self { Self::Value { value, .. } => value.read_borrowed(read), + Self::Input { position, .. } => Function::capture_value(call, position, read) } + } } -/// Resolves each late-bound part (`Function::resolve`), keeping each datum as it is. An error in a rebuilt function shows the function's text. -struct Resolving<'a> { session: &'a Session, resolved: &'a mut HashMap, depth: usize } -impl Rebuild for Resolving<'_> { - fn function(&mut self, f: &Function) -> Result { f.resolve(self.session, self.resolved, self.depth + 1) } - fn datum(&mut self, a: &Value) -> Result { Ok(a.clone()) } - fn span(&self, f: &Function) -> Span { Span::whole(Source::new("", f.bpl())) } +impl From for FunctionOperand { fn from(value: Value) -> Self { Self::value(value, false) } } + +impl Function { + /// The original argument roots, excluding functions, parameters and captures. + pub(crate) fn argument_roots(origin: &crate::program::Origin<&[Value]>) -> crate::error::Result<[Option; 2]> { + if matches!(origin.operation, Operation::Loop { .. }) { + let inputs = crate::program::LoopInputs::of(origin, origin.list.len())?; + return Ok([inputs.data_root, Some(inputs.carry_root)]); + } + let positions = origin.call_view(&origin.list).explicit_argument_roots(); + let roots: Vec<_> = origin.trees.as_ref().map_or_else( + || positions.clone().map(|index| TreeRoot { index, leaves: index..index + 1 }).collect(), + |trees| trees.roots().skip(positions.start).take(positions.len()).collect()); + match roots.as_slice() { + [right] => Ok([None, Some(right.clone())]), + [left, right] => Ok([Some(left.clone()), Some(right.clone())]), + _ => Err(Error::domain("a function needs one or two arguments")), + } + } + pub(crate) fn argument_ranges(origin: &crate::program::Origin<&[Value]>) -> crate::error::Result<[Option>; 2]> { + Ok(Self::argument_roots(origin)?.map(|root| root.map(|root| root.leaves))) + } + + pub(crate) fn body_arguments<'a>(origin: &crate::program::Origin<&'a [Value]>) -> crate::error::Result> { + let roots = Self::argument_roots(origin)?; + let right = roots[1].as_ref().expect("a call has a right argument"); + let first = roots[0].as_ref().map_or(right.leaves.start, |left| left.leaves.start); + let Some(trees) = &origin.trees else { return Ok(Cow::Borrowed(&origin.list[first..right.leaves.end])); }; + Ok(Cow::Owned(roots.into_iter().flatten().map(|root| trees.at_root(&root, origin.list).into_owned()).collect())) + } + + pub(crate) fn captures<'a>(origin: &crate::program::Origin<&'a [Value]>) -> &'a [Value] { &origin.list[origin.capture_start()..] } + pub(crate) fn capture_at<'a>(origin: &crate::program::Origin<&'a [Value]>, root: &TreeRoot) -> Cow<'a, Value> { + let root = root.shifted(origin.capture_root_start(), origin.capture_start()); + origin.trees.as_ref().map_or_else(|| Cow::Borrowed(&origin.list[root.leaves.start]), |trees| trees.at_root(&root, origin.list)) + } + pub(crate) fn capture_value<'a>(call: &'a crate::primitive::CallView<'_>, root: &TreeRoot, read: crate::ReadKind) -> Result, Error> { + let root = root.shifted(call.capture_root_start(), call.capture_start()); + match call.trees { + None => call.value(root.leaves.start, read), + Some(trees) => trees.read_root(&root, |position| Ok(call.value(position, read)?.into_owned())).map(Cow::Owned), + } + } + + /// Runs this body against the complete, snapshot-bound call. + pub(crate) fn run(&self, origin: crate::program::Origin<&[Value]>) -> Result { + check()?; + let session = session(); + if !session.deeper() { return Err(Error::new(Kind::Limit, format!("evaluation depth exceeds {MAX_CALL_DEPTH}"))); } + let result = (|| { + let _numeric = crate::settings::activate(&origin)?; + session.with_call_context(self, origin.reborrow(), || { + if let Some(value) = crate::derivative::custom_rule(self, &origin)? { return Ok(value); } + self.run_body(origin.reborrow()) + }) + })(); + session.ascend(); + result + } + + /// Projects a body call without executing it. `from` names each argument's old first leaf. + pub(crate) fn bound(&self, parent: &crate::program::Origin<&[Value]>, arguments: &[Value], from: &[Option]) -> Result>, Error> { + let roots: Vec<_> = std::iter::once(Value::Function(self.clone())).chain(arguments.iter().cloned()).collect(); + let sources: Vec<_> = std::iter::once(None).chain(from.iter().copied()).collect(); + projected_call(parent, Operation::Loop { exit: crate::primitive::Exit::Once, order: crate::primitive::Order::Forward, body_result: crate::primitive::LoopResult::Carry }, &roots, &sources) + } + pub(crate) fn prototype_bound(&self, parent: &crate::program::Origin<&[Value]>, arguments: &[Value], from: &[Option]) -> Result { + session().prototype_mode(true, || { let origin = self.bound(parent, arguments, from)?; self.run(origin.reborrow()) }) + } + + pub(crate) fn call_bound(&self, parent: &crate::program::Origin<&[Value]>, arguments: &[Value], from: &[Option]) -> Result { + let origin = self.call_origin(parent, arguments, from)?; + Operation::dispatch(origin.reborrow())?.assemble() + } } -/// Puts what its closure gives for each datum that a program takes as an input (`is_data`) in the datum's place, and keeps any other (`Function::with_data`, `Function::structure`). An error in a rebuilt function points at its span, the call that rebuilds it, because the function's text would hold its data, which can be large. -struct Data<'a>(&'a mut dyn FnMut(&Value) -> Value, &'a Span); -impl Rebuild for Data<'_> { - fn function(&mut self, f: &Function) -> Result { f.rebuilt(self) } - fn datum(&mut self, a: &Value) -> Result { Ok(if is_data(a) { (self.0)(a) } else { a.clone() }) } - fn span(&self, _: &Function) -> Span { self.1.clone() } + +/// Interfaces "Function values", part 19: a function value's data. +#[derive(Clone)] +pub(crate) struct FunctionData { + /// Interfaces "Function values": its node. + pub(crate) node: FunctionNode, + /// Interfaces "Function values": its environment, parts 18-19's own. + pub(crate) environment: Environment, + /// Part 19: its attached rules, beside `environment`; rules aren't operands. + pub(crate) rules: Option, } impl Function { - /// Whether the functions are built from matching parts, with `values` comparing the values in them. A dfn matches only itself, and a late-bound name matches the same expression. - fn equal_by(&self, other: &Self, values: &mut ValueRule<'_, E>) -> Result { + /// Appends a live call's captures in the one canonical order; absence is metadata, not a value. + pub(crate) fn captured(&self, bindings: &mut CaptureBindings, emit: &mut dyn FnMut(Capture) -> TreeRoot) -> Result<(), Error> { + if bindings.scope(self).is_some() { return Ok(()); } + let index = bindings.scopes.len(); + bindings.scopes.push(CaptureScope { source: self.address(), parent: self.0.environment.parent, module: self.0.environment.module, + provenance: Some(ScopeSource::Input(bindings.order.len() as u32)), names: Box::new([]), operands: Box::new([]) }); + bindings.order.push(index as u32); + bindings.implicit_generator |= self.implicit_generator(); bindings.acts |= self.acts(); bindings.draws |= self.draws(); + let reads: &[Name] = match self.node() { FunctionNode::Defined(d) => d.reads(), FunctionNode::LateBound(name) => std::slice::from_ref(name), _ => &[] }; + let mut names = Vec::with_capacity(reads.len()); + for &name in reads { + let (place, position) = match session().capture_binding(self.0.environment.parent, self.0.environment.module, name)? { + Some((place, value)) => { + Self::capture_functions(&value, bindings, emit)?; + (Some(place), Some(emit(Capture { name: Some(name), place: Some(place), value, literal: false }))) + } + None => (None, None), + }; + names.push(CapturedName { name, position, place }); + } + if let FunctionNode::Applied { op: Operator::Closure(f), .. } = self.node() { f.captured(bindings, emit)?; } + let mut operands = Vec::with_capacity(self.operands().len()); + for (i, operand) in self.operands().iter().enumerate() { + let FunctionOperand::Value { value, .. } = operand else { panic!("a template's captures are already bound by its origin"); }; + let kind = self.node().operand_kind(i); + if kind != OperandKind::Opaque { Self::capture_functions(value, bindings, emit)?; } + operands.push((kind == OperandKind::Opaque || kind == OperandKind::Data && !matches!(value, Value::Function(_))) + .then(|| emit(Capture { name: None, place: None, value: value.clone(), literal: operand.literal() }))); + } + for rule in self.rules().into_iter().flat_map(|rules| rules.inverse.iter().chain(&rules.derivative)) { rule.captured(bindings, emit)?; } + bindings.scopes[index].names = names.into(); + bindings.scopes[index].operands = operands.into(); + Ok(()) + } + + pub(crate) fn capture_functions(value: &Value, bindings: &mut CaptureBindings, emit: &mut dyn FnMut(Capture) -> TreeRoot) -> Result<(), Error> { + visit_functions(value, &mut |function| function.captured(bindings, emit)) + } + + /// Closure conversion uses the already-snapshotted scope, never live name lookup. + pub(crate) fn extracted(&self, parent: &crate::program::Origin<&[Value]>) -> Result<(Self, Vec, Option>, Arc), Error> { + let (mut values, mut from) = (Vec::new(), Vec::new()); + let value = Value::Function(self.clone()); + let (bindings, trees, _) = parent.bindings.project(std::slice::from_ref(&value), parent, &mut values, &mut from)?; + let template = self.template(&bindings).map_err(|_| Error::domain("a recursive callable cannot be recorded"))?; + Ok((template, values, trees, bindings)) + } + + pub(crate) fn template(&self, bindings: &CaptureBindings) -> Result { + self.template_in(bindings, &mut Vec::new()) + } + fn template_in(&self, bindings: &CaptureBindings, active: &mut Vec) -> Result { + if active.contains(&self.address()) { return Err(crate::program::Reason::Recursion); } + active.push(self.address()); + let scope = bindings.scope(self); + let operands = self.operands().iter().enumerate().map(|(i, operand)| { + if let Some(position) = scope.and_then(|(_, scope)| scope.operands[i].clone()) { return Ok(FunctionOperand::Input { position, literal: operand.literal() }); } + Ok(match operand { + FunctionOperand::Value { value: Value::Function(f), .. } => FunctionOperand::value(Value::Function(f.template_in(bindings, active)?), operand.literal()), + operand => operand.clone(), + }) + }).collect::, _>>()?; + let operator = match self.node() { FunctionNode::Applied { op: Operator::Closure(f), .. } => Some(Operator::Closure(f.template_in(bindings, active)?)), _ => None }; + let rules = self.rules().map(|rules| Ok::<_, crate::program::Reason>(Rules { + inverse: rules.inverse.as_ref().map(|f| f.template_in(bindings, active)).transpose()?, + derivative: rules.derivative.as_ref().map(|f| f.template_in(bindings, active)).transpose()?, + })).transpose()?; + let environment = Environment { parent: None, module: 0, closed: scope.map(|(index, _)| index) }; + active.pop(); + let node = match self.node().with_operands(operands, operator) { + FunctionNode::System { function, made } => FunctionNode::System { function: function.native_tag(), made }, + node => node, + }; + Ok(Self(Arc::new(FunctionData { node, environment, rules }))) + } + + /// Rebuilds only a function value actually produced by the current run. + pub(crate) fn rebuilt(&self, values: &[Value], trees: Option<&crate::array::Trees>, bindings: &CaptureBindings) -> Result { + let value = |position: &TreeRoot| trees.map_or_else(|| values[position.leaves.start].clone(), |trees| trees.at_root(position, values).into_owned()); + let scope = bindings.scope(self).map(|(_, scope)| scope); + let operands = self.operands().iter().enumerate().map(|(i, operand)| { + if let Some(position) = scope.and_then(|scope| scope.operands[i].as_ref()) { return Ok(FunctionOperand::value(value(position), operand.literal())); } + Ok(match operand { + FunctionOperand::Input { position, .. } => FunctionOperand::value(value(position), operand.literal()), + FunctionOperand::Value { value: Value::Function(f), .. } if self.node().operand_kind(i) != OperandKind::Opaque => + FunctionOperand::value(Value::Function(f.rebuilt(values, trees, bindings)?), operand.literal()), + operand => operand.clone(), + }) + }).collect::, Error>>()?; + let operator = match self.node() { FunctionNode::Applied { op: Operator::Closure(f), .. } => Some(Operator::Closure(f.rebuilt(values, trees, bindings)?)), _ => None }; + let rules = self.rules().map(|rules| Ok::<_, Error>(Rules { inverse: rules.inverse.as_ref().map(|f| f.rebuilt(values, trees, bindings)).transpose()?, + derivative: rules.derivative.as_ref().map(|f| f.rebuilt(values, trees, bindings)).transpose()? })).transpose()?; + let environment = scope.map_or_else(|| self.0.environment.clone(), |scope| Environment { parent: scope.parent, module: scope.module, closed: None }); + let node = match self.node() { + FunctionNode::System { function, .. } if function.native_opaque() => { + let data = operands.first().and_then(FunctionOperand::stored).ok_or_else(Error::planning)?; + FunctionNode::System { function: function.rebuild_native(data)?, made: None } + } + node => node.with_operands(operands, operator), + }; + Ok(Self(Arc::new(FunctionData { node, environment, rules }))) + } + + pub(crate) fn structure_eq(&self, other: &Self) -> bool { self.equal_by(other, true, &mut |a, b| Ok::<_, Infallible>(a.same(b))).unwrap() } + pub(crate) fn matches(&self, other: &Self, numeric: Numeric) -> Result { + self.equal_by(other, false, &mut |a, b| a.matches_within(b, numeric)) + } + fn equal_by(&self, other: &Self, structure: bool, values: &mut dyn FnMut(&Value, &Value) -> Result) -> Result { use FunctionNode as N; - if Arc::ptr_eq(&self.node, &other.node) { return Ok(true); } - Ok(match (&self.node.kind, &other.node.kind) { - (N::Primitive(x), N::Primitive(y)) => x == y, (N::System(x), N::System(y)) => x == y, - (N::LateBound(_, x), N::LateBound(_, y)) => x.source.text == y.source.text, (N::Fold(f, x), N::Fold(g, y)) => x == y && f.equal_by(g, values)?, - (N::Inverse(f), N::Inverse(g)) => f.equal_by(g, values)?, (N::Axis(f, a), N::Axis(g, b)) => f.equal_by(g, values)? && values(a, b, true)?, - (N::Defined(x), N::Defined(y)) => x == y, (N::Derived(c, a, b), N::Derived(d, x, y)) => c == d && operands_equal(a, b, x, y, values)?, - (N::Ruled(f, r), N::Ruled(g, s)) => f.equal_by(g, values)? && r.equal_by(s, values)?, - (N::Modified(o, a), N::Modified(p, b)) => o == p && a.equal_by(b, values)?, - (N::Composed(o, [a, b]), N::Composed(p, [x, y])) => o == p && a.equal_by(x, values)? && b.equal_by(y, values)?, - (N::Array(a), N::Array(b)) => values(a, b, false)?, - (N::Fork([f, g, h]), N::Fork([x, y, z])) => f.equal_by(x, values)? && g.equal_by(y, values)? && h.equal_by(z, values)?, _ => false - }) + if Arc::ptr_eq(&self.0, &other.0) { return Ok(true); } + let same = match (self.node(), other.node()) { + (N::Primitive(a), N::Primitive(b)) => a == b, + (N::System { function: a, .. }, N::System { function: b, .. }) => if structure { a.native_tag() == b.native_tag() } else { a == b }, + (N::Defined(a), N::Defined(b)) => Arc::ptr_eq(a, b), (N::LateBound(a), N::LateBound(b)) => a == b, + (N::Applied { op: a, .. }, N::Applied { op: b, .. }) => match (a, b) { + (Operator::Kind(a), Operator::Kind(b)) => a == b, + (Operator::Closure(a), Operator::Closure(b)) => a.equal_by(b, structure, values)?, _ => false, + }, + (N::Train(_), N::Train(_)) | (N::Array(_), N::Array(_)) => true, _ => false, + }; + if !same || !structure && (self.0.environment.parent, self.0.environment.module) != (other.0.environment.parent, other.0.environment.module) { return Ok(false); } + let (a, b) = (self.operands(), other.operands()); + if a.len() != b.len() { return Ok(false); } + for (i, (a, b)) in a.iter().zip(b.iter()).enumerate() { + if a.literal() != b.literal() { return Ok(false); } + let same = match (a, b) { + _ if self.node().operand_kind(i) == OperandKind::Opaque => true, + (FunctionOperand::Value { value: Value::Function(a), .. }, FunctionOperand::Value { value: Value::Function(b), .. }) => a.equal_by(b, structure, values)?, + (FunctionOperand::Value { value: Value::Function(_), .. }, _) | (_, FunctionOperand::Value { value: Value::Function(_), .. }) => false, + _ if structure && self.node().operand_kind(i) == OperandKind::Data => true, + (FunctionOperand::Value { value: a, .. }, FunctionOperand::Value { value: b, .. }) => values(a, b)?, + (FunctionOperand::Input { position: a, .. }, FunctionOperand::Input { position: b, .. }) => a == b, _ => false, + }; + if !same { return Ok(false); } + } + let rules = |f: &Self| [f.rules().and_then(|rules| rules.inverse.as_ref()), f.rules().and_then(|rules| rules.derivative.as_ref())]; + for (a, b) in rules(self).into_iter().zip(rules(other)) { + match (a, b) { (Some(a), Some(b)) if a.equal_by(b, structure, values)? => {}, (None, None) => {}, _ => return Ok(false) } + } + Ok(true) + } + + pub(crate) fn structure_hash(&self, state: &mut H) { + use std::hash::Hash; + std::mem::discriminant(self.node()).hash(state); + match self.node() { + FunctionNode::Primitive(p) => p.hash(state), FunctionNode::System { function, .. } => function.name.hash(state), + FunctionNode::Defined(d) => (Arc::as_ptr(d) as usize).hash(state), FunctionNode::LateBound(name) => name.hash(state), + FunctionNode::Applied { op: Operator::Kind(kind), .. } => kind.hash(state), + FunctionNode::Applied { op: Operator::Closure(f), .. } => f.structure_hash(state), _ => {} + } + self.operands().len().hash(state); + for (i, operand) in self.operands().iter().enumerate() { + operand.literal().hash(state); + match operand { + _ if self.node().operand_kind(i) == OperandKind::Opaque => 0u8.hash(state), + FunctionOperand::Value { value: Value::Function(f), .. } => f.structure_hash(state), + _ if self.node().operand_kind(i) == OperandKind::Data => 0u8.hash(state), + FunctionOperand::Value { value: value, .. } => hash_structure_value(value, state), FunctionOperand::Input { position: position, .. } => position.hash(state), + } + } + for rule in self.rules().into_iter().flat_map(|rules| rules.inverse.iter().chain(&rules.derivative)) { rule.structure_hash(state); } + } + + pub(crate) fn observed(&self, read: crate::ReadKind) -> Result { + let operands = self.node().operands().iter().map(|operand| operand.observed(read)).collect::, _>>()?; + let operator = match self.node() { FunctionNode::Applied { op: Operator::Closure(f), .. } => Some(Operator::Closure(f.observed(read)?)), _ => None }; + let rules = self.rules().map(|rules| Ok::<_, Error>(Rules { inverse: rules.inverse.as_ref().map(|f| f.observed(read)).transpose()?, + derivative: rules.derivative.as_ref().map(|f| f.observed(read)).transpose()? })).transpose()?; + Ok(Self(Arc::new(FunctionData { node: self.node().with_operands(operands, operator), environment: self.0.environment.clone(), rules }))) + } +} + +fn hash_structure_value(value: &Value, state: &mut H) { + use std::hash::Hash; + match value { + Value::Number(number) => match number.to_float() { Ok(value) => (if value == 0.0 { 0.0 } else { value }).to_bits().hash(state), Err(_) => value.identity_hash(state) }, + Value::Character(c) => c.hash(state), Value::Function(f) => f.structure_hash(state), + Value::Array(_) => { value.shape().hash(state); for item in value.elements() { hash_structure_value(&item, state); } } + value => value.identity_hash(state), } - /// Whether the functions match within tolerance `t`, as `≡` compares them: built from matching parts, with `≡`'s rule for the values in them. - pub(crate) fn matches(&self, other: &Self, t: Numeric, span: &Span) -> Result { - self.equal_by(other, &mut |x, y, _| x.matches_within(y, t, span)) +} + +impl Operator { + fn observed(&self, read: crate::program::trace::ReadKind) -> Result { + match self { Self::Kind(kind) => Ok(Self::Kind(kind.clone())), Self::Closure(function) => Ok(Self::Closure(function.observed(read)?)) } } - /// The identity that a program's replay compares a function read through a free name by: a dfn's `Definition`, as "Definitions in a numeric recording" in `meta/recorded.md` says, or else the function's node. - pub(crate) fn identity(&self) -> usize { - match self.node() { FunctionNode::Defined(c) => Arc::as_ptr(&c.definition) as usize, _ => Arc::as_ptr(&self.node) as usize } + fn identical(&self, other: &Self) -> bool { match (self, other) { (Self::Kind(a), Self::Kind(b)) => a == b, (Self::Closure(a), Self::Closure(b)) => a.address() == b.address(), _ => false } } +} +impl OperatorValue { + pub(crate) fn observed(&self, read: crate::program::trace::ReadKind) -> Result { + Ok(match self { Self::Operator(op) => Self::Operator(op.observed(read)?), Self::Bound { op, right } => Self::Bound { op: op.observed(read)?, right: right.observed(read)? } }) + } + pub(crate) fn matches(&self, other: &Self, numeric: Numeric) -> Result { + let operators = |a: &Operator, b: &Operator| match (a, b) { + (Operator::Kind(a), Operator::Kind(b)) => Ok(a == b), + (Operator::Closure(a), Operator::Closure(b)) => a.matches(b, numeric), + _ => Ok(false), + }; + match (self, other) { + (Self::Operator(a), Self::Operator(b)) => operators(a, b), + (Self::Bound { op: a, right: x }, Self::Bound { op: b, right: y }) => Ok(operators(a, b)? && x.matches(y, numeric)?), + _ => Ok(false), + } } } -impl PartialEq for Function { fn eq(&self, other: &Self) -> bool { let Ok(equal) = self.equal_by(other, &mut same_values); equal } } -const MAX_RESOLUTION_DEPTH: usize = 128; -/// The depth of evaluation allowed, counting each call of a function other than a primitive, each statement of a dfn other than its last, and each group. In the browser the engine's stack is the limit. Plain recursion counts two a level and gets 120 levels from 244. Recursion through an outer product counts three, for 79. Under Node 26's TurboFan on Apple silicon, a level takes 1.78 KB of stack for plain recursion and through an operator's operand, 1.95 KB for recursion that assigns, 2.00 KB through Rank, 2.20 KB through Each and 2.75 KB through an outer product (29 October 2026). x86-64's V8 takes more for the same calls, by an amount no one has measured. The limit fits the deepest of these recursions into `tests/wasm.mjs`'s 80% of a Chrome worker's stack where a level takes up to 1.39 times its stack on Apple silicon. +/// Browser stack limits require a lower call-depth cap. const MAX_CALL_DEPTH: usize = if cfg!(web) { 244 } else { 20_000 }; /// Lexical frames allowed. They live on the heap, so the browser allows as many as native builds. const MAX_FRAME_DEPTH: usize = 20_000; @@ -115,22 +378,6 @@ fn room() -> bool { /// The LIMIT error for evaluation deeper than `MAX_CALL_DEPTH`. #[cold] fn depth_error(span: &Span) -> Error { Error::new(Kind::Limit, format!("evaluation depth exceeds {MAX_CALL_DEPTH}")).at(span) } -/// Reports `report` for `function` at `span`'s position. The `fusion` pref writes it, and `•explain` records the `items` items of pending arrays that `function` computed (`explain::forced`). `watched` reports this way. -pub(crate) fn fusion_report(span: &Span, report: &str, function: &str, items: usize) { - session().fusion_reports.set(session().fusion_reports.get() + 1); - let (line, column) = span.position(); - if items > 0 && crate::explain::recording() { crate::explain::forced(function.to_owned(), line, column, items); } - if crate::settings::reports_fusion() { session().execution.write(&format!("{report} at {function} ({}:{line}:{column})\n", span.source.name)); } -} -/// `run` under `•prefs ["fusion":$t]` or `•explain`. Where no call inside it reported, it reports, as `name`, the items of pending arrays that it computed, or else `other`'s report, if `other` gives one (`fusion_report`). A function call (`Function::reported`) and an operation's call (`Operation::call`) report through it. -pub(crate) fn watched(span: &Span, name: impl FnOnce() -> String, run: impl FnOnce() -> T, other: impl FnOnce() -> Option<&'static str>) -> T { - let (reports, before) = (session().fusion_reports.get(), crate::explain::computed_so_far()); - let result = run(); - let items = crate::explain::computed_so_far() - before; - let report = if items > 0 { Some("fusion ends") } else { other() }; - if let (Some(report), true) = (report, session().fusion_reports.get() == reports) { fusion_report(span, report, &name(), items); } - result -} /// The name classes that `•nl` and `name_class` report, as in Dyalog. const SUBJECT_CLASS: i64 = 2; const FUNCTION_CLASS: i64 = 3; @@ -151,862 +398,671 @@ fn released(name: impl std::fmt::Display, span: &Span) -> Error { trait Argument { fn value(&self) -> &Value; fn owned(self) -> Value; + /// The argument as a `Cow`, borrowed where it's borrowed and owned where its caller gave it up, as an operator kind's call takes it (`OperatorInfo::call`). + fn into_cow<'a>(self) -> Cow<'a, Value> where Self: 'a; } impl Argument for &Value { #[inline(always)] fn value(&self) -> &Value { self } #[inline(always)] fn owned(self) -> Value { self.clone() } + #[inline(always)] + fn into_cow<'a>(self) -> Cow<'a, Value> where Self: 'a { Cow::Borrowed(self) } } impl Argument for Cow<'_, Value> { #[inline(always)] fn value(&self) -> &Value { self } #[inline(always)] fn owned(self) -> Value { self.into_owned() } + #[inline(always)] + fn into_cow<'a>(self) -> Cow<'a, Value> where Self: 'a { self } } impl Argument for Value { #[inline(always)] fn value(&self) -> &Value { self } #[inline(always)] fn owned(self) -> Value { self } + #[inline(always)] + fn into_cow<'a>(self) -> Cow<'a, Value> where Self: 'a { Cow::Owned(self) } } /// The error for a selection target that does something other than select from its array, such as assigning the selection. const INVALID_SELECTION: &str = "invalid selective-assignment expression"; -#[derive(Debug)] -enum FunctionNode { +/// The same node represents a real function and a data-free template. +#[derive(Clone)] +pub(crate) enum FunctionNode { Primitive(Primitive), - System(crate::system::SystemFunction), - LateBound(Arc, Span), - Fold(Function, FoldKind), - Inverse(Function), - Axis(Function, Value), - Defined(Closure), - Derived(Closure, Operand, Option), - /// A function with the hand-written inverse or derivative that `f⁻¹←g` or `f∂←g` attaches. Calling it calls the function. - Ruled(Function, Rules), - Modified(OperatorKind, Operand), - Composed(OperatorKind, [Operand; 2]), - Fork([Function; 3]), - /// An array that holds a function as a direct item. Calling it calls each such item on the whole arguments, and its other items are their own results. - Array(Value), + System { function: SystemFunction, made: Option }, + Defined(Arc), + LateBound(Name), + Applied { op: Operator, operands: Box<[FunctionOperand]> }, + Train(Box<[FunctionOperand]>), + Array(FunctionOperand), } -/// The hand-written rules that selective assignment attaches to a function: `f⁻¹←g` sets the inverse and `f∂←g` the derivative. -#[derive(Clone, Debug, Default)] -struct Rules { inverse: Option, derivative: Option } -impl Rules { - fn functions(&self) -> impl Iterator { self.inverse.iter().chain(&self.derivative) } - fn equal_by(&self, other: &Self, values: &mut ValueRule<'_, E>) -> Result { - let mut same = |a: &Option, b: &Option| match (a, b) { - (Some(a), Some(b)) => a.equal_by(b, values), _ => Ok(a.is_none() && b.is_none()) - }; - Ok(same(&self.inverse, &other.inverse)? && same(&self.derivative, &other.derivative)?) - } +#[derive(Clone, Default)] +pub(crate) struct Rules { pub(crate) inverse: Option, pub(crate) derivative: Option } + +/// Lexical locations belong to live values only. A template names its snapshot scope. +#[derive(Clone, Default)] +pub(crate) struct Environment { + pub(super) parent: Option, + pub(super) module: usize, + pub(crate) closed: Option, } -/// A system name as `notation` spells it. -fn system_name(name: &str, notation: Notation) -> String { - name.strip_prefix('•').map_or_else(|| name.into(), |name| format!("{}{name}", notation.glyph("•"))) +/// Positional capture metadata. Runtime locations are rebound from each current call. +#[derive(Clone, Default, PartialEq, Eq, Hash)] +pub(crate) struct CaptureBindings { pub(crate) scopes: Vec, pub(crate) order: Vec, pub(crate) implicit_generator: bool, pub(crate) acts: bool, pub(crate) draws: bool } + +#[derive(Clone, Default, PartialEq, Eq, Hash)] +pub(crate) struct CaptureScope { + pub(crate) source: usize, + pub(crate) parent: Option, + pub(crate) module: usize, + pub(crate) names: Box<[CapturedName]>, + pub(crate) operands: Box<[Option]>, + pub(crate) provenance: Option, } -/// The name of `f.g`, where `f` and `g` are primitives. -pub(crate) fn product(f: &Function, g: &Function) -> Option { Some(Operation::Product(f.as_primitive()?, g.as_primitive()?)) } +#[derive(Clone, PartialEq, Eq, Hash)] +pub(crate) enum ScopeSource { Input(u32), Made { context: u32, aliases: Box<[Option]> } } -impl FunctionNode { - /// The operation this node names: a primitive, a fold or scan by a primitive, or an inner product of two primitives. - fn operation(&self) -> Option { - match self { - Self::Primitive(p) => Some(Operation::Primitive(*p)), Self::Fold(f, kind) => Some(Operation::Fold(f.dyadic_primitive()?, *kind)), - Self::Composed(OperatorKind::Product, [Operand::Function(f), Operand::Function(g)]) => product(f, g), _ => None - } +#[derive(Clone, PartialEq, Eq, Hash)] +pub(crate) struct CapturedName { pub(crate) name: Name, pub(crate) position: Option, pub(crate) place: Option } + +impl CaptureBindings { + pub(crate) fn empty() -> Arc { + static EMPTY: std::sync::LazyLock> = std::sync::LazyLock::new(|| Arc::new(CaptureBindings::default())); + EMPTY.clone() } - /// Calls `visit` on each operand, left to right, without copying it. Primitives, system functions, dfns and late-bound names have none. - fn visit_operands<'a>(&'a self, mut visit: impl FnMut(OperandRef<'a>)) { - use OperandRef as R; - match self { - Self::Primitive(_) | Self::System(_) | Self::LateBound(..) | Self::Defined(_) => {} Self::Fold(f, _) | Self::Inverse(f) => visit(R::Function(f)), - Self::Axis(f, a) => { visit(R::Function(f)); visit(R::Value(a)) } Self::Modified(_, a) => visit(a.into()), Self::Array(a) => visit(R::Value(a)), - Self::Composed(_, operands) => operands.iter().for_each(|a| visit(a.into())), Self::Fork(fs) => fs.iter().for_each(|f| visit(R::Function(f))), - Self::Derived(_, a, b) => std::iter::once(a).chain(b).for_each(|a| visit(a.into())), - Self::Ruled(f, rules) => std::iter::once(f).chain(rules.functions()).for_each(|f| visit(R::Function(f))) + + pub(crate) fn reindexed(&self, mut position: impl FnMut(&TreeRoot) -> Option) -> Option { + let mut result = self.clone(); + for &index in &self.order { + let scope = &mut result.scopes[index as usize]; + for name in &mut scope.names { if let Some(old) = name.position.as_ref() { name.position = Some(position(old)?); } } + for operand in &mut scope.operands { if let Some(old) = operand.as_ref() { *operand = Some(position(old)?); } } } + Some(result) } - /// The operands, left to right (`visit_operands`). - fn operands(&self) -> Vec { - let mut out = Vec::new(); - self.visit_operands(|a| out.push(a.owned())); - out + pub(crate) fn scope(&self, function: &Function) -> Option<(u32, &CaptureScope)> { + self.order.iter().find_map(|&index| (self.scopes[index as usize].source == function.address()).then(|| (index, &self.scopes[index as usize]))) + .or_else(|| function.0.environment.closed.filter(|index| self.order.contains(index)).map(|index| (index, &self.scopes[index as usize]))) } - /// The node's label in a function tree: its glyph, or its source for a function array or a node without operands. - fn label(&self, el: Elide) -> String { - match self { - Self::Primitive(p) => el.notation.glyph(p.glyph()).into(), - Self::System(f) => system_name(f.name, el.notation) + &f.argument().map_or(String::new(), |a| crate::array::quoted(&a)), - Self::Ruled(f, _) => f.node().label(el), Self::LateBound(_, span) => span.source.text.clone(), - Self::Defined(c) | Self::Derived(c, ..) => c.text().into(), Self::Fold(_, h) => el.notation.glyph(h.glyph()).into(), - Self::Inverse(_) => "⁻¹".into(), Self::Axis(..) => el.notation.glyph("⍠").into(), - Self::Modified(OperatorKind::Super(Superscript::Mark(mark)), _) => mark.marker().into(), Self::Array(a) => a.source(el), - Self::Modified(op, _) | Self::Composed(op, _) => el.notation.glyph(op.glyph()).into(), Self::Fork(_) => "fork".into() + + /// Cached structure has no lexical location from a previous call. + pub(crate) fn stripped(&self) -> Self { + let mut result = self.clone(); + for scope in &mut result.scopes { + scope.source = 0; scope.parent = None; scope.module = 0; + for name in &mut scope.names { name.place = None; } } + result } -} -impl Builtin { - /// This built-in function's `Function`, from one table that every session and thread shares. The table is built on the first read. It holds each system function, each primitive that `Primitive::all` lists, and the primitives without a glyph: `Bracket`, the `Exact` functions that `xₓ` and `xᵣy` call, and the functions that `•special` makes. `Arc`, the form that `○⁻¹` calls, and `ᵇ`'s tables never become functions. A fast combination's function (`Special`) has no entry, and becomes a function of its own on each read. - pub(crate) fn function(self) -> Function { - static TABLE: std::sync::LazyLock> = std::sync::LazyLock::new(|| { - let primitives = Primitive::all().chain([Primitive::Bracket]).chain(crate::primitive::Exact::ALL.map(Primitive::Exact)) - .chain(crate::primitive::Special::ALL.map(Primitive::Special)) - .map(|p| (Builtin::Primitive(p), Function::plain(FunctionNode::Primitive(p)))); - primitives.chain(crate::system::Entry::all().map(|entry| (Builtin::System(entry), entry.built()))).collect() - }); - let Builtin::Primitive(p) = self else { return TABLE[&self].clone() }; - TABLE.get(&self).cloned().unwrap_or_else(|| Function::plain(FunctionNode::Primitive(p))) + /// Rebinds a cached scope layout to this call's values and lexical locations. + pub(crate) fn in_context(self: &Arc) -> Arc { + if self.order.iter().enumerate().all(|(ordinal, &index)| matches!(self.scopes[index as usize].provenance, Some(ScopeSource::Input(i)) if i == ordinal as u32)) { return self.clone(); } + let mut bindings = (**self).clone(); + for (ordinal, &index) in self.order.iter().enumerate() { bindings.scopes[index as usize].provenance = Some(ScopeSource::Input(ordinal as u32)); } + Arc::new(bindings) + } + pub(crate) fn rebind_into(&self, target: &crate::program::Origin<&[Value]>, current: &crate::program::Origin<()>) -> Option { + self.rebind(target.list.len().checked_sub(target.capture_start())?, current) + } + pub(crate) fn rebind(&self, capture_leaf_count: usize, current: &crate::program::Origin<()>) -> Option { + let input = |ordinal: u32| current.bindings.order.get(ordinal as usize).and_then(|&index| current.bindings.scopes.get(index as usize)); + let mut result = self.stripped(); + for &index in &self.order { + let expected = &self.scopes[index as usize]; + let scope = &mut result.scopes[index as usize]; + match &expected.provenance { + Some(ScopeSource::Input(ordinal)) => { + let actual = input(*ordinal)?; + if expected.names.len() != actual.names.len() || expected.operands.len() != actual.operands.len() + || expected.names.iter().zip(&actual.names).any(|(a, b)| a.name != b.name || a.position.is_some() != b.position.is_some()) { return None; } + (scope.source, scope.parent, scope.module) = (actual.source, actual.parent, actual.module); + for (name, actual) in scope.names.iter_mut().zip(&actual.names) { name.place = actual.place; } + } + Some(ScopeSource::Made { context, aliases }) => { + let context = input(*context)?; + scope.module = context.module; + if aliases.len() != scope.names.len() { return None; } + for (name, alias) in scope.names.iter_mut().zip(aliases) { + name.place = match alias { Some(index) => { + let original = context.names.get(*index as usize)?; + if original.name != name.name { return None; } + original.place + }, None => None }; + } + } + None if scope.names.is_empty() => {} + None => return None, + } + if scope.names.iter().filter_map(|name| name.position.as_ref()).chain(scope.operands.iter().flatten()) + .any(|position| position.leaves.end > capture_leaf_count) { return None; } + } + Some(result) } -} -impl Function { - #[inline] - fn node(&self) -> &FunctionNode { &self.node.kind } - /// The primitive this function is, where it is one. - pub(crate) fn as_primitive(&self) -> Option { match self.node() { FunctionNode::Primitive(p) => Some(*p), _ => None } } - /// The primitive this function is when called with two arguments: its own primitive, or for `gᵇ`, `Primitive::Bitwise` of `g`'s truth table, where `g`'s Boolean kernel gives one (`pervasive::truth_table`). A dyadic call of `gᵇ`, and folds and scans, which call their operand with two arguments, use it to run `gᵇ` through the kernels. A monadic `gᵇ` reads `g`'s monadic form, which this table doesn't describe. - pub(crate) fn dyadic_primitive(&self) -> Option { - match self.node() { - FunctionNode::Primitive(p) => Some(*p), - FunctionNode::Modified(OperatorKind::Bitwise, Operand::Function(g)) => crate::pervasive::truth_table(g.dyadic_primitive()?).map(Primitive::Bitwise), - _ => None, + pub(crate) fn structure_eq(&self, other: &Self) -> bool { + self.acts == other.acts && self.draws == other.draws && self.implicit_generator == other.implicit_generator && self.order.len() == other.order.len() && self.order.iter().zip(&other.order).all(|(&a, &b)| { + let (a, b) = (&self.scopes[a as usize], &other.scopes[b as usize]); + a.provenance == b.provenance && a.operands == b.operands && a.names.len() == b.names.len() && a.names.iter().zip(&b.names).all(|(a, b)| (a.name, &a.position) == (b.name, &b.position)) + }) + } + pub(crate) fn structure_hash(&self, state: &mut H) { + use std::hash::Hash; + self.order.len().hash(state); self.implicit_generator.hash(state); self.acts.hash(state); self.draws.hash(state); + for &index in &self.order { + let scope = &self.scopes[index as usize]; + scope.operands.hash(state); scope.provenance.hash(state); + for name in &scope.names { name.name.hash(state); name.position.hash(state); } } } - /// The operation this function names, where it names one (`FunctionNode::operation`). - pub(crate) fn operation(&self) -> Option { self.node().operation() } - /// The dfn that this function runs, where it's a dfn or a dfn operator's derived function. - pub(crate) fn definition(&self) -> Option<&Definition> { - match self.node() { FunctionNode::Defined(c) | FunctionNode::Derived(c, ..) => Some(c.definition.as_ref()), _ => None } + + /// Appends only these callables' captured leaves to an origin under construction. + pub(crate) fn project(&self, values: &[Value], parent: &crate::program::Origin<&[Value]>, + list: &mut Vec, from: &mut Vec>) -> Result<(Arc, Option>, u64), Error> { + let start = list.len(); + let mut projection = CaptureProjection { source: self, parent, list, from, start, + bindings: Self { scopes: vec![CaptureScope::default(); self.scopes.len()], order: Vec::new(), implicit_generator: false, acts: false, draws: false }, + trees: crate::array::Trees::leaves(0), has_trees: false, positions: HashMap::new(), literals: 0 }; + for value in values { projection.value_functions(value)?; } + let trees = projection.has_trees.then(|| Arc::new(projection.trees)); + Ok((Arc::new(projection.bindings), trees, projection.literals)) } - /// The dfn that keeps the stops of this function's batched runs (`batch::tried`): its own dfn, or a fold's operand's, because a fold by a dfn calls the dfn at each stage. - pub(crate) fn remembers(&self) -> Option<&Definition> { match self.node() { FunctionNode::Fold(f, _) => f.remembers(), _ => self.definition() } } - /// The call that a running level can take over when `dispatch` calls this function with `left`: an attached derivative, or a system function or its inverse. Other calls route where their operations run. - fn op(&self, _: Option<&Value>) -> Option> { - use crate::transform::Op; - match self.node() { - FunctionNode::Ruled(f, Rules { derivative: Some(g), .. }) => Some(Op::Ruled(f, g)), FunctionNode::System(f) => Some(Op::System(self, f)), - FunctionNode::Inverse(g) if let FunctionNode::System(f) = g.node() => Some(Op::System(self, f)), _ => None +} + +/// A temporary builder for the origin's existing input vector, not a callable or execution view. +struct CaptureProjection<'a, 'v> { + source: &'a CaptureBindings, + parent: &'a crate::program::Origin<&'v [Value]>, + list: &'a mut Vec, + from: &'a mut Vec>, + start: usize, + bindings: CaptureBindings, + trees: crate::array::Trees, + has_trees: bool, + positions: HashMap, + literals: u64, +} + +impl CaptureProjection<'_, '_> { + fn root(&mut self, root: &TreeRoot) -> TreeRoot { + if let Some(position) = self.positions.get(root) { return position.clone(); } + let source = root.shifted(self.parent.capture_root_start(), self.parent.capture_start()); + let first = self.list.len() - self.start; + let index = self.trees.roots().count(); + if let Some(trees) = &self.parent.trees { + self.trees.append_root(trees, &source); self.has_trees = true; + self.list.extend_from_slice(&self.parent.list[source.leaves.clone()]); + } else { self.trees.push(&self.parent.list[source.leaves.start], self.list); } + self.from.extend(source.leaves.map(Some)); + let position = TreeRoot { index, leaves: first..self.list.len() - self.start }; + self.positions.insert(root.clone(), position.clone()); + position + } + fn fresh_root(&mut self, value: &Value, literal: bool) -> TreeRoot { + let first = self.list.len() - self.start; + let index = self.trees.roots().count(); + self.has_trees |= value.holds_mixed(); + self.trees.push(value, self.list); + let end = self.list.len() - self.start; + if literal { for leaf in first..end.min(64) { self.literals |= 1 << leaf; } } + self.from.resize(self.list.len(), None); + TreeRoot { index, leaves: first..end } + } + fn root_functions(&mut self, root: &TreeRoot) -> Result<(), Error> { + let values = self.parent.list; + for position in root.shifted(self.parent.capture_root_start(), self.parent.capture_start()).leaves { self.value_functions(&values[position])?; } + Ok(()) + } + fn value_functions(&mut self, value: &Value) -> Result<(), Error> { visit_functions(value, &mut |function| self.function(function)) } + fn function(&mut self, function: &Function) -> Result<(), Error> { + let source = self.source; + let existing = source.scope(function); + let index = if let Some((index, _)) = existing { index } else { + if self.bindings.order.iter().any(|&index| self.bindings.scopes[index as usize].source == function.address()) { return Ok(()); } + assert!(function.0.environment.closed.is_none(), "a template's scope belongs to its current origin"); + let index = self.bindings.scopes.len() as u32; + self.bindings.scopes.push(CaptureScope::default()); index + }; + if self.bindings.order.contains(&index) { return Ok(()); } + self.bindings.order.push(index); + self.bindings.implicit_generator |= function.implicit_generator(); self.bindings.acts |= function.acts(); self.bindings.draws |= function.draws(); + let mut projected = existing.map_or_else(|| CaptureScope { source: function.address(), parent: function.0.environment.parent, + module: function.0.environment.module, provenance: None, names: Box::new([]), operands: vec![None; function.operands().len()].into() }, |(_, scope)| scope.clone()); + if existing.is_some() { + for name in &mut projected.names { + if let Some(position) = name.position.as_ref() { self.root_functions(position)?; name.position = Some(self.root(position)); } + } + } else { + let reads: &[Name] = match function.node() { FunctionNode::Defined(d) => d.reads(), FunctionNode::LateBound(name) => std::slice::from_ref(name), _ => &[] }; + let mut names = Vec::with_capacity(reads.len()); + for &name in reads { + let entry = match session().capture_binding(projected.parent, projected.module, name)? { + Some((place, value)) => { self.value_functions(&value)?; CapturedName { name, position: Some(self.fresh_root(&value, false)), place: Some(place) } } + None => CapturedName { name, position: None, place: None }, + }; + names.push(entry); + } + projected.names = names.into(); } - } - /// `f` and the text after the dot of a function built from `f.name` or `f.[I]`: the field name, or the selectors. - fn dotted(&self, el: Elide) -> Option<(&Function, String)> { - let FunctionNode::Composed(OperatorKind::Atop, [Operand::Function(s), Operand::Function(f)]) = self.node() else { return None }; - let FunctionNode::Composed(OperatorKind::Before, [Operand::Value(a), Operand::Function(b)]) = s.node() else { return None }; - match b.as_primitive()? { - Primitive::Mix => crate::keyed::name(a).map(|name| (f, name.to_string())), Primitive::Bracket => Some((f, a.operand(el))), _ => None + if existing.is_none() && let Some(context) = session().bindings.capture_context(function.0.environment.parent) { + let aliases = |name: &CapturedName| name.place.and_then(|place| context.names.iter().position(|name| name.place == Some(place))); + projected.provenance = match &context.provenance { + Some(ScopeSource::Input(input)) => Some(ScopeSource::Made { context: *input, + aliases: projected.names.iter().map(|name| aliases(name).map(|i| i as u32)).collect() }), + Some(ScopeSource::Made { context: input, aliases: inherited }) => Some(ScopeSource::Made { context: *input, + aliases: projected.names.iter().map(|name| aliases(name).and_then(|i| inherited[i])).collect() }), + None => None, + }; } - } - pub(crate) fn environment(&self) -> Option { self.node.environment } - fn late(&self) -> bool { self.node.late } - /// Whether the function can use arguments it owns: a dfn, or a composition or fork with such an operand. `dispatch` lends its arguments to any other composition or fork, which would only drop them. - fn owns(&self) -> bool { self.node.owns } - /// The function that `value` gives where a call needs one: a function, or the callable node of an array that holds a function (`from_array`). - fn from_value(value: Binding, span: &Span) -> Result { - match value { - Binding::Function(f) => Ok(f), Binding::Value(a) if a.holds_functions() => Self::from_array(a, span), - _ => Err(Error::new(ErrorKind::Syntax, "call requires a function expression").at(span)) + if let FunctionNode::Applied { op: Operator::Closure(function), .. } = function.node() { self.function(function)?; } + for (i, operand) in function.operands().iter().enumerate() { + let kind = function.node().operand_kind(i); + if let Some(position) = existing.and_then(|(_, scope)| scope.operands[i].as_ref()) { + if kind != OperandKind::Opaque { self.root_functions(position)?; } + projected.operands[i] = Some(self.root(position)); + } else if let FunctionOperand::Value { value, .. } = operand { + if kind != OperandKind::Opaque { self.value_functions(value)?; } + if existing.is_none() && (kind == OperandKind::Opaque || kind == OperandKind::Data && !matches!(value, Value::Function(_))) { + projected.operands[i] = Some(self.fresh_root(value, operand.literal())); + } + } } + for rule in function.rules().into_iter().flat_map(|rules| rules.inverse.iter().chain(&rules.derivative)) { self.function(rule)?; } + self.bindings.scopes[index as usize] = projected; + Ok(()) } - /// The functions that a composition or fork runs, each with the valence it runs at, and the array that a bound composition (`a↣f` or `f↢a`) binds. `None` for any other function. - fn called_parts(&self, dyadic: bool) -> Option<(Vec<(&Function, bool)>, Option<&Value>)> { - use {FunctionNode as N, Operand::{Function as F, Value as V}, OperatorKind::*}; - Some(match self.node() { - N::Composed(Atop, [F(f), F(g)]) => (vec![(g, dyadic), (f, false)], None), N::Composed(Over, [F(f), F(g)]) => (vec![(g, false), (f, dyadic)], None), - N::Composed(Before, [V(a), F(f)]) | N::Composed(After, [F(f), V(a)]) => (vec![(f, true)], Some(a)), - N::Modified(Commute, F(f)) => (vec![(f, true)], None), N::Fork([f, g, h]) => (vec![(f, dyadic), (h, dyadic), (g, true)], None), _ => return None - }) +} + +#[derive(Clone, Debug)] +pub enum Operator { Kind(OperatorKind), Closure(Function) } + +#[derive(Clone, Debug)] +pub enum OperatorValue { Operator(Operator), Bound { op: Operator, right: FunctionOperand } } + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum OperatorKind { + Reduce, ReduceFirst, Scan, ScanFirst, Each, Commute, Inverse, Outer, Key, Derivative, Optimize, Bitwise, + Mark(Mark), Under, Atop, Over, Rank, Axis, Stencil, Dot, Power, Before, After, Valences, Agenda, + Field(Name), Index, + Framed { axes: usize, atoms: Box<[bool]> }, + JvpStep { packing: Box<[bool]> }, + VjpStep { direction: Direction }, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub(crate) enum Direction { Forward, Backward } + +pub(crate) struct OperatorInfo { + pub(crate) call: fn(&Function, crate::program::Origin<&[Value]>) -> crate::error::Result, + pub(crate) spelling: fn(&OperatorKind) -> String, + pub(crate) inverse: fn(&Function, crate::program::Origin<&[Value]>) -> crate::error::Result, + pub(crate) discards: fn(&OperatorKind, &[Value], bool) -> bool, + pub(crate) python: &'static str, + pub(crate) glyph: &'static str, + pub(crate) dyadic: bool, + pub(crate) operands: fn(&OperatorKind, usize, &FunctionOperand) -> OperandKind, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum OperandKind { Structure, Data, Opaque } + +impl OperatorKind { + pub(crate) fn info(&self) -> &'static OperatorInfo { operators::info(self) } + pub(crate) fn glyph(&self) -> String { (self.info().spelling)(self) } + pub(crate) fn dyadic(&self) -> bool { self.info().dyadic } +} + +impl Operator { + pub fn bpl(&self) -> String { match self { Self::Kind(kind) => kind.glyph(), Self::Closure(f) => f.bpl() } } + pub(crate) fn environment(&self) -> Option { match self { Self::Kind(_) => None, Self::Closure(f) => f.environment() } } + pub(crate) fn dyadic(&self) -> bool { match self { Self::Kind(kind) => kind.dyadic(), Self::Closure(f) => f.definition().is_some_and(|d| d.kind == DefinitionKind::DyadicOperator) } } + pub fn derive(&self, left: Value, right: Option) -> Result { + self.derive_operands(left.into(), right.map(FunctionOperand::from), &Span::whole(Source::new("", ""))) } - /// Whether the function is `⍉`, with or without a bound left argument. Its inverse gives each item exactly once. - fn transposes(&self) -> bool { - match self.node() { - FunctionNode::Primitive(Primitive::Transpose) => true, - FunctionNode::Composed(OperatorKind::Before, [Operand::Value(_), Operand::Function(f)]) => f.transposes(), _ => false - } + pub(crate) fn derive_operands(&self, left: FunctionOperand, right: Option, span: &Span) -> Result { + if self.dyadic() != right.is_some() { return Err(Error::domain("invalid operator operands").at(span)); } + Function::new(FunctionNode::Applied { op: self.clone(), operands: std::iter::once(left).chain(right).collect() }, span) } - /// Whether the function can drop items of its argument. Under uses an inverse only for a function that can't, as BQN's spec requires. An inverse drops items only where its function does, except inverse transpose, which never drops any. The inverse of a catenate, BPL's one other inverse that drops items, checks the items it drops. Dfns, system functions, functions with an inverse that `f⁻¹←g` attaches, and constants count as keeping every item. - fn discards(&self, dyadic: bool) -> bool { - use {FunctionNode as N, Operand::{Function as F, Value as V}, OperatorKind::*}; - if let Some((parts, _)) = self.called_parts(dyadic) { return parts.iter().any(|&(f, v)| f.discards(v)); } - match self.node() { - N::Primitive(p) => p.discards(dyadic), N::Fold(f, _) => f.discards(true) || matches!(f.node(), N::Primitive(Primitive::Identity(_))), - N::Ruled(_, Rules { inverse: Some(_), .. }) => false, N::Inverse(f) if f.transposes() => false, - N::Axis(f, _) | N::Inverse(f) | N::Ruled(f, _) | N::Modified(Each, F(f)) | N::Composed(Power | Rank, [F(f), V(_)]) => f.discards(dyadic), - N::Modified(Commute, V(_)) | N::System(_) | N::Defined(_) | N::Derived(..) | N::LateBound(..) => false, - N::Array(a) => a.elements().any(|e| matches!(e, Value::Function(f) if f.discards(dyadic))), - node => node.operands().iter().any(|o| matches!(o, F(f) if f.discards(false) || f.discards(true))) - } + fn text(&self, _: Elide) -> String { self.bpl() } +} + +impl OperatorValue { + pub(crate) fn environment(&self) -> Option { match self { Self::Operator(op) => op.environment(), Self::Bound { op, right } => op.environment().max(right.stored().and_then(Value::environment)) } } + pub(crate) fn derive_operands(&self, left: FunctionOperand, right: Option, span: &Span) -> Result { + match self { Self::Operator(op) => op.derive_operands(left, right, span), Self::Bound { op, right: fixed } if right.is_none() => op.derive_operands(left, Some(fixed.clone()), span), + _ => Err(Error::domain("a bound operator takes one operand").at(span)) } + } + pub fn bpl(&self) -> String { match self { Self::Operator(op) => op.bpl(), Self::Bound { op, right } => format!("{}{}", op.bpl(), right.stored().map_or_else(|| "?".into(), |value| value.operand(Elide::NONE))) } } + fn text(&self, _: Elide) -> String { self.bpl() } +} + +impl FunctionNode { + fn operands(&self) -> &[FunctionOperand] { + match self { Self::Applied { operands, .. } | Self::Train(operands) => operands, + Self::Array(value) => std::slice::from_ref(value), Self::System { made, .. } => made.as_slice(), _ => &[] } } - fn tree(&self, el: Elide, budget: &mut usize) -> crate::display::Tree { - use crate::display::Tree; - if let FunctionNode::Ruled(f, _) = self.node() { return f.tree(el, budget); } - if *budget == 0 { return Tree::leaf("…"); } - *budget -= 1; - let node = self.node(); - if let FunctionNode::Array(_) = node { return Tree::leaf(node.label(el)); } - Tree { label: node.label(el), children: node.operands().iter().map(|a| a.tree(el, budget)).collect() } + fn operand_kind(&self, position: usize) -> OperandKind { + match self { Self::Applied { op: Operator::Kind(kind), operands } => (kind.info().operands)(kind, position, &operands[position]), + Self::System { function, .. } if function.native_opaque() => OperandKind::Opaque, + Self::Applied { op: Operator::Closure(_), .. } | Self::Train(_) | Self::Array(_) | Self::System { .. } => OperandKind::Data, + _ => OperandKind::Structure } } - pub(crate) fn text(&self, el: Elide, budget: &mut usize) -> String { - if *budget == 0 { return "…".into(); } - *budget -= 1; - if let Some((f, key)) = self.dotted(el) { return format!("{}.{}", f.left_text(el, budget), key); } - match self.node() { - FunctionNode::Primitive(_) | FunctionNode::System(_) | FunctionNode::Array(_) => self.node().label(el), - FunctionNode::LateBound(_, span) => span.source.text.clone(), - FunctionNode::Defined(c) => c.text().into(), - FunctionNode::Fold(f, h) => format!("{}{}", f.left_text(el, budget), el.notation.glyph(h.glyph())), - FunctionNode::Inverse(f) => f.superscripted("⁻¹", el, budget), - FunctionNode::Modified(OperatorKind::Super(Superscript::Mark(mark)), Operand::Function(f)) => f.superscripted(mark.marker(), el, budget), - FunctionNode::Axis(f, a) => format!("{}{}{}", f.left_text(el, budget), el.notation.glyph("⍠"), a.operand(el)), - FunctionNode::Modified(op, a) => format!("{}{}", a.left_text(el, budget), el.notation.glyph(op.glyph())), - FunctionNode::Composed(op, [a, b]) => format!("{}{}{}", a.left_text(el, budget), el.notation.glyph(op.glyph()), b.right_text(el, budget)), - FunctionNode::Fork(fs) => { - // Tines sit side by side, with a space only where two would read as one token. - let word = |c: char| { - let c = crate::symbols::canonical(c); - crate::syntax::name_char(c) || c.is_ascii_digit() || "¯.•⎕\"'".contains(c) - }; - let mut text = String::new(); - for f in fs { - let tine = f.left_text(el, budget); - if text.chars().last().is_some_and(word) && tine.chars().next().is_some_and(word) { text.push(' '); } - text.push_str(&tine); - } - text - } - FunctionNode::Derived(c, a, b) => { - format!("{}{}{}", a.left_text(el, budget), c.text(), b.as_ref().map_or(String::new(), |b| b.right_text(el, budget))) - } - FunctionNode::Ruled(f, _) => f.text(el, budget), + fn with_operands(&self, operands: Box<[FunctionOperand]>, operator: Option) -> Self { + match self { + Self::Applied { op, .. } => Self::Applied { op: operator.unwrap_or_else(|| op.clone()), operands }, + Self::Train(_) => Self::Train(operands), Self::Array(_) => Self::Array(operands.into_vec().pop().unwrap()), + Self::System { function, .. } => Self::System { function: function.clone(), made: operands.into_vec().pop() }, + node => node.clone(), } } - /// An operator takes the whole function to its left, so only a train needs parentheses there. A function array reads as its array would. - fn left_text(&self, el: Elide, budget: &mut usize) -> String { +} + +impl Function { + fn operands(&self) -> Cow<'_, [FunctionOperand]> { match self.node() { - FunctionNode::Fork(_) => format!("({})", self.text(el, budget)), FunctionNode::Array(a) => a.operand(el), _ => self.text(el, budget) + FunctionNode::System { function, made: None } => function.native_data().map_or_else(|| Cow::Borrowed(&[] as &[_]), |value| Cow::Owned(vec![value.into()])), + node => Cow::Borrowed(node.operands()), } } - /// The function with the superscript `mark` after it. Text that ends in a number goes in parentheses, because a superscript there would apply to the number. So does text with a space, which would split the run. - pub(crate) fn superscripted(&self, mark: &str, el: Elide, budget: &mut usize) -> String { - let text = self.left_text(el, budget); - if text.contains(' ') || text.ends_with(|c: char| c.is_ascii_digit() || matches!(crate::symbols::canonical(c), '∞' | 'ₓ')) { - format!("({text}){mark}") - } else { format!("{text}{mark}") } + pub(crate) fn node(&self) -> &FunctionNode { &self.0.node } + pub(crate) fn rules(&self) -> Option<&Rules> { self.0.rules.as_ref() } + pub(crate) fn address(&self) -> usize { Arc::as_ptr(&self.0) as usize } + pub(crate) fn readdressed(&self) -> Self { Self(Arc::new((*self.0).clone())) } + pub(crate) fn identity(&self) -> usize { match self.node() { FunctionNode::Defined(d) => Arc::as_ptr(d) as usize, _ => self.address() } } + pub(crate) fn environment(&self) -> Option { + let mut frame = self.0.environment.parent; + if let FunctionNode::Applied { op, .. } = self.node() { frame = frame.max(op.environment()); } + for operand in self.operands() { if let FunctionOperand::Value { value: value, .. } = operand { frame = frame.max(value.environment()); } } + for rule in self.rules().into_iter().flat_map(|rules| rules.inverse.iter().chain(&rules.derivative)) { frame = frame.max(rule.environment()); } + frame + } + pub(crate) fn as_primitive(&self) -> Option { match (self.node(), self.rules()) { (FunctionNode::Primitive(p), None) => Some(*p), _ => None } } + pub(crate) fn dyadic_primitive(&self) -> Option { + if self.rules().is_some() { return None; } + match self.node() { FunctionNode::Applied { op: Operator::Kind(OperatorKind::Bitwise), operands } => match operands.as_ref() { + [FunctionOperand::Value { value: Value::Function(f), .. }] => f.dyadic_primitive()?.truth_table().map(Primitive::Bitwise), _ => None }, + _ => self.as_primitive() } } - /// An operator takes one token to its right, so anything larger than one token needs parentheses there. - fn right_text(&self, el: Elide, budget: &mut usize) -> String { - if let FunctionNode::Array(a) = self.node() { return a.operand(el); } - let text = self.text(el, budget); - let token = match self.node() { - FunctionNode::Primitive(_) | FunctionNode::System(_) | FunctionNode::Defined(_) => true, - FunctionNode::LateBound(..) => text.chars().all(crate::syntax::name_char), _ => false - }; - if token { text } else { format!("({text})") } - } - /// A train. Each subject first binds the part after it, so `2×` is `2↣×` (`train_parts`). The train then builds from its last part leftwards (`train_shape`). So `32+1.8×` is `(32↣+)∘(1.8↣×)`, `⊢-2×⊢` is `⊢-(2↣×)∘⊢`, and `2|#⊢` is `(2↣|)#⊢`. - fn train(tines: Vec, span: &Span) -> Result { - let parts = Self::train_parts(&tines, |f| Ok(f.clone()), |a, f: Result| before(a.clone(), f?, span)); - Self::train_shape(parts, &mut (), |_, f| f, |_, g, f, result| Self::new(FunctionNode::Fork([g?, f?, result]), span), |_, f, result| atop(f?, result, span)) - } - /// A train's parts, last part first, each with whether it's bound. Each function tine makes a part (`function`), and each subject binds the part after it as its left argument (`bind`). `train` builds a train's function from these parts, and `operators::train_call` calls them. - fn train_parts<'t, P>(tines: &'t [Tine], function: impl Fn(&'t Function) -> P, bind: impl Fn(&'t Value, P) -> P) -> impl Iterator { - let mut i = tines.len(); - std::iter::from_fn(move || { - i = i.checked_sub(1)?; - let Tine::Function(f) = &tines[i] else { unreachable!("a train ends in a function") }; - let mut part = (function(f), false); - while let Some(Tine::Array(a)) = i.checked_sub(1).map(|j| &tines[j]) { - i -= 1; - part = (bind(a, part.0), true); - } - Some(part) - }) + pub(crate) fn definition(&self) -> Option<&Definition> { + match self.node() { FunctionNode::Defined(d) => Some(d), FunctionNode::Applied { op: Operator::Closure(f), .. } => f.definition(), _ => None } + } + pub(crate) fn system_function(&self) -> Option<&SystemFunction> { match self.node() { FunctionNode::System { function, .. } => Some(function), _ => None } } + fn operand_slot(&self, bindings: &CaptureBindings, position: usize) -> Option { + bindings.scope(self).and_then(|(_, scope)| scope.operands.get(position).cloned().flatten()) } - /// A train's structure, from its parts, last part first, each with whether it's bound, as `2↣×` is. A bound part takes one argument, so it makes an atop with what's built (`atop(f, result)`). Any other part makes a fork with the part before it (`fork(g, f, result)`), or an atop when it comes first. Each call gets `context`, which `train_call` uses for the train's arguments. `train` builds a train's function by this structure, and `operators::train_call` calls a train's tines by it. - fn train_shape( - mut parts: impl Iterator, context: &mut C, last: impl FnOnce(&mut C, P) -> Result, - mut fork: impl FnMut(&mut C, P, P, R) -> Result, mut atop: impl FnMut(&mut C, P, R) -> Result) -> Result { - let (part, _) = parts.next().expect("a train ends in a function"); - let mut result = last(context, part)?; - while let Some((f, bound)) = parts.next() { - let g = if bound { None } else { parts.next() }; - result = match g { Some((g, _)) => fork(context, g, f, result)?, None => atop(context, f, result)? }; + pub(crate) fn made_source<'a>(&'a self, bindings: &CaptureBindings) -> Option> { + let FunctionNode::System { function, made } = self.node() else { return None; }; + if let Some(position) = self.operand_slot(bindings, 0) { + return Some(Cow::Owned(FunctionOperand::Input { position, literal: made.as_ref().is_some_and(FunctionOperand::literal) })); } - Ok(result) - } - /// The function of primitive `p`, from the table that `Builtin::function` reads. - pub(crate) fn primitive(p: Primitive) -> Self { Builtin::Primitive(p).function() } - /// A function that returns `a` whatever its arguments, as `a⍨` writes it. - fn constant(a: Value, span: &Span) -> Result { Self::new(FunctionNode::Modified(OperatorKind::Commute, Operand::Value(a)), span) } - /// A function that calls each function among `a`'s items, for an array that holds one as a direct item. - fn from_array(a: Value, span: &Span) -> Result { Self::new(FunctionNode::Array(a), span) } - /// The array that a function from `constant` returns, or `None` for any other function. - fn as_constant(&self) -> Option<&Value> { match self.node() { FunctionNode::Modified(OperatorKind::Commute, Operand::Value(a)) => Some(a), _ => None } } - pub(crate) fn system(f: crate::system::SystemFunction) -> Self { Self::plain(FunctionNode::System(f)) } - /// A function of `kind`, a primitive's or a system function's node. Neither holds an environment or a rewrite. A primitive keeps its needs mask (`rewrite::glyph`). - fn plain(kind: FunctionNode) -> Self { - let needs = if let FunctionNode::Primitive(p) = &kind { rewrite::glyph(*p) } else { 0 }; - Self { node: Arc::new(FunctionData { kind, environment: None, late: false, rewritten: [None, None], needs, owns: false }) } + made.as_ref().map(Cow::Borrowed).or_else(|| function.native_data().map(|value| Cow::Owned(value.into()))) } - fn new(node: FunctionNode, span: &Span) -> Result { - use OperatorKind::*; - let node = match node { - FunctionNode::Modified(op, a) => { - let info = op.info(); - if matches!(op, Fold(_)) && matches!(&a, Operand::Value(v) if !v.holds_functions()) { - return Err(Error::domain("reduce and scan need a function operand. Replicate with #").at(span)); - } - let a = if matches!(op, Super(_)) { a } else { constant_operand(info.left, a, span)? }; - if info.dyadic() || !accepts(info.left, &a) { return Err(Error::domain("operator needs a function operand").at(span)); } - match (op, a) { - (Fold(kind), Operand::Function(f)) => FunctionNode::Fold(f, kind), - (Super(Superscript::Power(-1)), Operand::Function(f)) => return f.inverse(span), - (Super(Superscript::Power(n)), Operand::Function(f)) => { - FunctionNode::Composed(Power, [Operand::Function(f), Operand::Value(Value::integer(n))]) - } - (op @ Super(Superscript::Mark(_)), a @ Operand::Function(_)) => FunctionNode::Modified(op, a), - (Super(_), _) => return Err(Error::domain("ᵀ transposes an array, not a function").at(span)), - (op, a) => FunctionNode::Modified(op, a), - } - } - // An array has no meaning as an inner product's right operand. After a function, as after an array, text reads a field of the result and other arrays select from it. - FunctionNode::Composed(Product, [Operand::Function(f), Operand::Value(a)]) => { - let read = if crate::keyed::name(&a).is_some() { Primitive::Mix } else { Primitive::Bracket }; - let select = Self::new(FunctionNode::Composed(Before, [Operand::Value(a), Operand::Function(Self::primitive(read))]), span)?; - FunctionNode::Composed(Atop, [Operand::Function(select), Operand::Function(f)]) - } - FunctionNode::Composed(op, [a, b]) => { - let info = op.info(); - let a = constant_operand(info.left, a, span)?; - let mut b = match info.right { Some(r) => constant_operand(r, b, span)?, None => b }; - if !(accepts(info.left, &a) && info.right.is_some_and(|r| accepts(r, &b))) { return Err(Error::domain("invalid operator operands").at(span)); } - if matches!(op, Agenda) { - let Operand::Value(fs) = &b else { unreachable!() }; - if fs.shape().len() != 1 { return Err(Error::new(Kind::Rank, "agenda needs a vector of functions").at(span)); } - if fs.is_empty() { return Err(Error::domain("agenda needs a nonempty vector of functions").at(span)); } - // An array among the functions acts as a constant function. - let options = fs.elements().map(|e| match e { - Value::Function(f) => Ok(Value::Function(f)), a => Function::constant(a, span).map(Value::Function) - }).collect::, _>>()?; - // A selector for each cell meets the argument only when the function runs. - if let Operand::Value(index) = &a { if index.is_unit() { agenda_index(index, options.len()).at(span)?; } } - b = Operand::Value(Value::new(vec![options.len()], options).described("invalid agenda options").at(span)?); + pub(crate) fn operation(&self) -> Option { + use crate::primitive::Pairing; + if self.rules().is_some() { return None; } + match self.node() { + FunctionNode::Primitive(primitive) => Some(Operation::Primitive { primitive: *primitive, pairing: Pairing::Axes }), + FunctionNode::System { .. } => Some(Operation::System), + FunctionNode::Applied { op: Operator::Kind(kind), operands } => match (kind, &operands[..]) { + (OperatorKind::Reduce | OperatorKind::ReduceFirst | OperatorKind::Scan | OperatorKind::ScanFirst, [FunctionOperand::Value { value: Value::Function(f), .. }]) => { + Some(Operation::Fold { primitive: f.dyadic_primitive()?, first: matches!(kind, OperatorKind::ReduceFirst | OperatorKind::ScanFirst), + scan: matches!(kind, OperatorKind::Scan | OperatorKind::ScanFirst), pairing: Pairing::Axes }) } - match (op, a, b) { - (Axis, Operand::Function(f), Operand::Value(v)) => FunctionNode::Axis(f, v), (op, a, b) => FunctionNode::Composed(op, [a, b]) + (OperatorKind::Dot, [FunctionOperand::Value { value: Value::Function(f), .. }, FunctionOperand::Value { value: Value::Function(g), .. }]) => { + Some(Operation::Product { fold: f.dyadic_primitive()?, map: g.dyadic_primitive()?, contracted: 1, pairing: Pairing::Axes }) } - } - node => node, - }; - let (environment, late) = operand_dependencies(&node); - let (environment, late) = match &node { - FunctionNode::LateBound(..) => (environment, true), - FunctionNode::Defined(c) | FunctionNode::Derived(c, ..) => (environment.max(c.environment), late), _ => (environment, late) - }; - let owns = match &node { - FunctionNode::Defined(_) | FunctionNode::Derived(..) | FunctionNode::LateBound(..) => true, - FunctionNode::Composed(_, operands) => operands.iter().any(|o| matches!(o, Operand::Function(f) if f.owns())), - FunctionNode::Fork(fns) => fns.iter().any(Function::owns), _ => false - }; - let needs = rewrite::built_from(&node); - let mut f = Self { node: Arc::new(FunctionData { kind: node, environment, late, rewritten: [None, None], needs, owns }) }; - let rewritten = rewrite::function(&f, span); - if rewritten.iter().any(Option::is_some) && let Some(data) = Arc::get_mut(&mut f.node) { data.rewritten = rewritten; } - Ok(f) - } - /// `call_with` on borrowed arguments. - #[inline(always)] - fn call(&self, left: Option<&Value>, right: &Value, span: &Span) -> Result { self.call_with(left, right, span) } - /// Calls the function, first growing the stack when it's nearly full, except for a primitive (`call_primitive`). A dfn binds arguments that the caller gives up (`Argument`) to `⍺` and `⍵` without copying them, as the binder gives its own, so the dfn's last read of either leaves its reader the only holder (`syntax::release`). `call`, `call_with`, `dispatch` and `call_array` are always inlined, so that each BPL call adds fewer frames to the host stack. In the browser that stack limits the depth of recursion. An error that the function's code raised without a span takes the call's span. - #[inline(always)] - fn call_with(&self, left: Option, right: R, span: &Span) -> Result { - check().at(span)?; - // A primitive other than `⍎` never calls back into BPL, so it skips the depth, capture and stack bookkeeping below. - if let FunctionNode::Primitive(p) = self.node() { - if !matches!(p, Primitive::Execute) && !crate::explain::reporting() { - return self.call_primitive(*p, left.as_ref().map(L::value), right.value(), span); - } + _ => None, + }, + _ => None, } - session().descend(span)?; - let capture = session().capture.replace(false); - // This is `stacker::maybe_grow`, without its closure's frame, judging the room left as `room` does. - let room = room(); - let result = if crate::explain::reporting() { self.reported(left, right, room, span) } - else if room { self.dispatch(left, right, span) } - else { self.grown(left, right, span) }; - session().capture.set(capture); - session().ascend(); - result.map_err(|e| e.at(span)) - } - /// A call of the primitive `p`: on numbers, a form that applies a kernel to each pair of items runs it at once (`Primitive::scalar_call`), and any other call goes through `dispatch`. - #[inline(never)] - fn call_primitive(&self, p: Primitive, left: Option<&Value>, right: &Value, span: &Span) -> Result { - let scalar = if session().transforming() { None } else { p.scalar_call(left, right) }; - scalar.map_or_else(|| self.dispatch(left, right, span), |result| result.map(Bound::from)).map_err(|e| e.at(span)) - } - /// `dispatch` under `•prefs ["fusion":$t]` or `•explain`. Where no call inside it reported, it reports the items of pending arrays that this call computed, with the function and its position. Under the `fusion` pref it also reports each argument that `∂` carries, a dual or a traced value, whose items the call read one by one (`derivative`), keeping that argument's carrier through the call to see. It keeps nothing else, so the call holds its arguments as an unreported call does: a kept argument would keep the arrays it holds alive, which changes the routes that ask whether something else holds an array (`Value::folded`). It claims the routes that ran item by item inside it and that no call inside it claimed (`explain::claim_itemwise`). `watched` makes each report. Out of line, so that `call` pays one branch. - #[cold] - #[inline(never)] - fn reported(&self, left: Option, right: R, room: bool, span: &Span) -> Result { - use crate::transform::Carrier; - // Each carrier of `∂` whose items weren't built, kept to see whether the call builds them. - let carried = |v: &Value| v.carrier().filter(|c| matches!(**c, Carrier::Dual(_) | Carrier::Traced(_)) && !c.items_built()).cloned(); - let carriers = if crate::settings::reports_fusion() { [left.as_ref().map(L::value).and_then(carried), carried(right.value())] } else { [None, None] }; - let mark = crate::explain::itemwise_mark(); - let run = move || if room { self.dispatch(left, right, span) } else { self.grown(left, right, span) }; - let unbuilt = || carriers.iter().flatten().any(Carrier::items_built).then_some("∂ runs item by item"); - let result = watched(span, || self.bpl(), run, unbuilt); - if let Some(mark) = mark { crate::explain::claim_itemwise(mark, || self.bpl(), span); } - result } - // Inlined into `call`, this would add `stacker::grow`'s setup to the stack frame of every BPL call. - #[inline(never)] - fn grown(&self, left: Option, right: R, span: &Span) -> Result { - stacker::grow(STACK_SEGMENT, move || self.dispatch(left, right, span)) - } - // Inlined into `dispatch`, this would put a second copy of `call` and `dispatch` in the stack frame of every BPL call. - #[inline(never)] - fn ruled(&self, left: Option<&Value>, right: &Value, span: &Span) -> Result { self.call(left, right, span) } - /// `traced` run, from a program, on this function, or on one with its data replaced (`program::tacit`), and these arguments, or `None` where no program takes the call and the interpreter runs it. `∂` asks first (`derivative::differentiate`). A dfn keeps its programs on its definition, which every call of its text shares. Any other function's programs are kept by its structure (`program::Tacit`). - pub(crate) fn programmed( - &self, left: Option<&Value>, right: &Value, span: &Span, traced: &dyn Fn(&Function, Option<&Value>, &Value) -> Result - ) -> Option> { - match self.node() { - FunctionNode::Defined(closure) => { - crate::program::call(closure.definition.programs(), closure.environment, left, right, span, &[], &|_, x, y| traced(self, x, y)) - } - _ => crate::program::tacit(self, left, right, span, traced), + fn new(node: FunctionNode, span: &Span) -> Result { + if let FunctionNode::Applied { op, operands } = &node && operands.len() != 1 + usize::from(op.dyadic()) { + return Err(Error::domain("invalid operator operands").at(span)); } + Ok(Self::of(node, None)) } - /// Calls the function that computes this call through a fast combination, where the form rewriter gave one (`rewrite`) and no running level carries an argument. Otherwise it sends compositions, forks and dfns straight to their handlers, with the arguments as the caller gave them, and other functions to `apply`. - #[inline(always)] - fn dispatch(&self, left: Option, right: R, span: &Span) -> Result { - let resolved; - let f = if self.late() { resolved = self.resolve(session(), &mut HashMap::new(), 0)?; &resolved } else { self }; - let (x, y) = (left.as_ref().map(L::value), right.value()); - if session().transforming() { - if let Some(result) = f.op(x).and_then(|op| crate::transform::call(op, x, y, span)) { return result.map(Bound::from); } - } else if let Some(rewritten) = &f.node.rewritten[usize::from(x.is_some())] { - return Self::through(rewritten, left, right, span); - } - match f.node() { - FunctionNode::Composed(..) if f.owns() => composition(f, left, right, span), - FunctionNode::Composed(..) => composition(f, x, y, span), - FunctionNode::Fork(tines) if f.owns() => fork_call(tines, left, right, span), - FunctionNode::Fork(tines) => fork_call(tines, x, y, span), - FunctionNode::Ruled(f, _) => f.ruled(x, y, span), - FunctionNode::Defined(_) | FunctionNode::Derived(..) => session().call_defined(f, left, right).map_err(|mut e| { - e.calls.push(span.clone()); - e - }), - _ => f.apply(x, y, span), - } + fn of(node: FunctionNode, rules: Option) -> Self { Self(Arc::new(FunctionData { node, environment: Environment::default(), rules })) } + pub(crate) fn applied_kind(kind: OperatorKind, operands: Vec) -> Result { + let span = Span::whole(Source::new("", kind.glyph())); + Self::new(FunctionNode::Applied { op: Operator::Kind(kind), operands: operands.into_iter().map(FunctionOperand::from).collect() }, &span) } - /// The call through `rewritten`, the function that the form rewriter gave for this call: a structural combination's own code (`rewrite::direct`), or `rewritten`'s dispatch. Out of line, since `dispatch` is inlined at every call site and few calls come here. - #[inline(never)] - fn through(rewritten: &Function, left: Option, right: R, span: &Span) -> Result { - let (x, y) = (left.as_ref().map(L::value), right.value()); - if let Some((s, bound)) = rewrite::direct(rewritten) { check()?; return combined(s, bound.or(x), y, span).map(Bound::from); } - rewritten.dispatch(left, right, span) + pub(crate) fn defined(definition: Arc, parent: Option, module: usize) -> Self { + Self(Arc::new(FunctionData { node: FunctionNode::Defined(definition), environment: Environment { parent, module, closed: None }, rules: None })) } - /// The function's result on these arguments, as an array. - #[inline(always)] - pub(crate) fn call_array(&self, left: Option<&Value>, right: &Value, span: &Span) -> Result { self.call(left, right, span)?.array(span) } - /// This function with `g` attached as its inverse, as `f⁻¹←g` attaches it. A derivative already attached stays. - pub fn with_inverse(&self, g: Function) -> Self { self.attached(|rules| rules.inverse = Some(g)) } - /// This function with `g` attached as its derivative, as `f∂←g` attaches it. An inverse already attached stays. - pub fn with_derivative(&self, g: Function) -> Self { self.attached(|rules| rules.derivative = Some(g)) } - fn attached(&self, set: impl FnOnce(&mut Rules)) -> Self { - let (f, mut rules) = match self.node() { FunctionNode::Ruled(f, rules) => (f.clone(), rules.clone()), _ => (self.clone(), Rules::default()) }; - set(&mut rules); - Self::new(FunctionNode::Ruled(f, rules), &Span::whole(Source::new("", ""))).expect("attaching a rule builds no operator node") + pub(crate) fn named(name: Name) -> Self { + let bindings = &session().bindings; + Self(Arc::new(FunctionData { node: FunctionNode::LateBound(name), environment: Environment { parent: bindings.current(), module: bindings.module(), closed: None }, rules: None })) } - fn call_prototype(&self, left: Option<&Value>, right: &Value, span: &Span) -> Result { - session().prototype_mode(true, || self.call(left, right, span)) + pub(crate) fn primitive(p: Primitive) -> Self { Self::of(FunctionNode::Primitive(p), None) } + pub(crate) fn system(function: SystemFunction) -> Self { Self::of(FunctionNode::System { function, made: None }, None) } + pub(crate) fn made(function: SystemFunction, made: Value) -> Self { Self::of(FunctionNode::System { function, made: Some(FunctionOperand::from(made)) }, None) } + fn from_array(value: Value, span: &Span) -> Result { Self::new(FunctionNode::Array(FunctionOperand::from(value)), span) } + fn constant(value: Value, _: &Span) -> Result { Operator::Kind(OperatorKind::Commute).derive(value, None) } + pub(crate) fn commuted(&self, _: &Span) -> Result { Operator::Kind(OperatorKind::Commute).derive(Value::Function(self.clone()), None) } + fn as_constant(&self) -> Option<&Value> { + match self.node() { FunctionNode::Applied { op: Operator::Kind(OperatorKind::Commute), operands } => match operands.as_ref() { + [FunctionOperand::Value { value: value, .. }] if !matches!(value, Value::Function(_)) => Some(value), _ => None }, _ => None } } - fn inverse(&self, span: &Span) -> Result { - match self.node() { - FunctionNode::Inverse(f) => Ok(f.clone()), - FunctionNode::Ruled(f, Rules { inverse: Some(g), .. }) => { - Self::new(FunctionNode::Ruled(g.clone(), Rules { inverse: Some(f.clone()), derivative: None }), span) - } - _ => Self::new(FunctionNode::Inverse(self.clone()), span), - } + pub(crate) fn train_operands(tines: Vec, span: &Span) -> Result { + match tines.as_slice() { [FunctionOperand::Value { value: Value::Function(f), .. }] => Ok(f.clone()), _ => Self::new(FunctionNode::Train(tines.into()), span) } } - fn resolve(&self, session: &Session, resolved: &mut HashMap, depth: usize) -> Result { - if !self.late() { return Ok(self.clone()); } - let id = Arc::as_ptr(&self.node) as usize; - if let Some((_, f)) = resolved.get(&id) { return Ok(f.clone()); } - let result = if let FunctionNode::LateBound(parsed, origin) = self.node() { - crate::execution::check().at(origin)?; - if depth >= MAX_RESOLUTION_DEPTH { return Err(Error::new(Kind::Limit, "late-bound function resolution exceeds 128 levels").at(&origin)); } - let bound = session.bindings.at_top_level(None, || session.bind(&parsed.statements[0].nodes)); - Self::from_value(bound?.value, origin)?.resolve(session, resolved, depth + 1)? - } else { self.rebuilt(&mut Resolving { session, resolved, depth })? }; - // Keep the source node alive so its address cannot be reused during resolution. - resolved.insert(id, (self.clone(), result.clone())); - Ok(result) - } - /// This function with each part of its node replaced as `rebuild` says: each function part, and each datum (`is_data`). A primitive, a system function, a late-bound name, a dfn or a function array has no parts, and stays as it is. - fn rebuilt(&self, rebuild: &mut dyn Rebuild) -> Result { - use FunctionNode as N; - fn operand(a: &Operand, rebuild: &mut dyn Rebuild) -> Result { - Ok(match a { Operand::Function(f) => Operand::Function(rebuild.function(f)?), Operand::Value(v) => Operand::Value(rebuild.datum(v)?) }) - } - let node = match self.node() { - N::Fold(f, h) => N::Fold(rebuild.function(f)?, *h), - N::Inverse(f) => N::Inverse(rebuild.function(f)?), - N::Axis(f, a) => N::Axis(rebuild.function(f)?, rebuild.datum(a)?), - N::Derived(c, a, b) => N::Derived(c.clone(), operand(a, rebuild)?, b.as_ref().map(|b| operand(b, rebuild)).transpose()?), - N::Ruled(f, rules) => { - let f = rebuild.function(f)?; - N::Ruled( - f, Rules { inverse: rules.inverse.as_ref().map(|g| rebuild.function(g)).transpose()?, - derivative: rules.derivative.as_ref().map(|g| rebuild.function(g)).transpose()? }) - } - N::Modified(op, a) => N::Modified(*op, operand(a, rebuild)?), - N::Composed(op, [a, b]) => N::Composed(*op, [operand(a, rebuild)?, operand(b, rebuild)?]), - N::Fork([a, b, c]) => N::Fork([rebuild.function(a)?, rebuild.function(b)?, rebuild.function(c)?]), - N::Primitive(_) | N::System(_) | N::LateBound(..) | N::Defined(_) | N::Array(_) => return Ok(self.clone()), - }; - Self::new(node, &rebuild.span(self)) - } - /// This function with each datum that a program takes as an input (`is_data`) replaced, in order, by the next of `values`. An error in it points at `span`, the call that needs it. - pub(crate) fn with_data(&self, values: &mut dyn Iterator, span: &Span) -> Result { - self.rebuilt(&mut Data(&mut |_| values.next().expect("a value for each datum"), span)) - } - /// The structure that `program::Tacit` keeps this function's programs by: the function with a placeholder (`placeholder`) for each datum that a program takes as an input. The data go in `data`, in the order that `with_data` replaces them. An error in it points at `span`, the call that needs it. - pub(crate) fn structure(&self, data: &mut Vec, span: &Span) -> Result { - self.rebuilt(&mut Data(&mut |a| { data.push(a.clone()); Value::Number(placeholder()) }, span)) - } - /// Whether this function has `structure` (`Function::structure`): the same parts and values, with a datum that a program takes as an input wherever `structure` has a placeholder. Those data go in `data`, in the order that `with_data` replaces them. - pub(crate) fn has_structure(&self, structure: &Self, data: &mut Vec) -> bool { - data.clear(); - let Ok(same) = self.equal_by(structure, &mut |x, y, datum| { - Ok::<_, Infallible>(if datum && is_data(x) { data.push(x.clone()); matches!(y, Value::Number(n) if *n == placeholder()) } else { x.same(y) }) - }); - same - } - fn apply(&self, left: Option<&Value>, right: &Value, span: &Span) -> Result { - use {crate::primitive::Extension, FunctionNode::{Defined, Derived, Fold, Fork}}; - // A form whose own code handles only arguments up to its ranks applies to cells of those ranks through Rank. A builtin that takes items maps over a right argument that holds several, with the left argument whole for each, and takes each row of a character array as one text item. - let extension = match self.node() { - FunctionNode::Primitive(p) => p.facts(left.is_some()).extension, FunctionNode::System(f) => { f.check(left)?; f.extension } _ => Extension::Whole - }; - let cells = match (extension, self.node()) { - (Extension::Rank, FunctionNode::Primitive(p)) => p.cells(left, right), - (Extension::Items(item), _) if item.maps(right) => return each_whole_left(self, left, right, span), - (Extension::Items(item), _) => item.rows(left, right), _ => None - }; - if let Some(cells) = cells { - let ranks = Value::integers(vec![cells.len()], cells.into_iter().map(i64::from).collect()).described("invalid cell ranks").at(span)?; - return rank(self, &ranks, left, right, span); - } - let array = match self.node() { - // `dispatch` calls these itself. - FunctionNode::LateBound(..) | FunctionNode::Composed(..) | FunctionNode::Ruled(..) | Defined(_) | Derived(..) | Fork(_) => unreachable!(), - FunctionNode::Primitive(Primitive::Execute) => return session().execute(left, right, span), - FunctionNode::Inverse(f) => return inverse(f, left.map(|a| (a, true)), right, span), - FunctionNode::Modified(OperatorKind::Each, Operand::Function(f)) => return each(f, left, right, span), - FunctionNode::Modified(OperatorKind::Bitwise, Operand::Function(f)) => bitwise(self, f, left, right, span), - FunctionNode::Modified(OperatorKind::Super(Superscript::Mark(mark)), Operand::Function(f)) => marked(f, *mark, left, right, span), - FunctionNode::Modified(OperatorKind::Outer, Operand::Function(f)) => return outer(f, left, right, span), - FunctionNode::Modified(OperatorKind::Key, Operand::Function(_)) => return key(self, left, right, span), - FunctionNode::Modified(OperatorKind::Differentiate, Operand::Function(f)) => crate::derivative::differentiate(f, left, right, span), - FunctionNode::Modified(OperatorKind::Commute, Operand::Function(f)) => return f.call(Some(right), left.unwrap_or(right), span), - FunctionNode::Modified(OperatorKind::Commute, Operand::Value(a)) => Ok(a.clone()), - FunctionNode::Modified(OperatorKind::Optimize, Operand::Function(f)) => crate::optimize::optimize(f, left, right, span), - FunctionNode::Modified(..) => unreachable!(), - FunctionNode::Primitive(p) => p.call(left, right, span), - FunctionNode::System(f) => { - if let (true, Some(prototype)) = (session().prototype.get(), f.prototype) { return Ok(Bound::from(prototype(span))); } - match &f.call { - crate::system::Call::Value(call) | crate::system::Call::Active(call) => call(left, right, span), - crate::system::Call::Effect(call) => { - crate::transform::acts().at(span)?; - return call(left, right, span).map(|(value, shy)| Bound { shy, ..Bound::from(value) }); - } - crate::system::Call::Element(tag) => crate::xml::element(tag, left, right), - crate::system::Call::Edge(mode) => crate::system::edged(*mode, left.expect("an edge function is dyadic"), right), - crate::system::Call::Canvas(source) => crate::canvas::draw(source, left, right), - #[cfg(web)] - crate::system::Call::Js(f) => return crate::js::call(f, left, right).map(|v| v.map_or_else(|| Bound::new(Binding::NoResult), Bound::from)), - crate::system::Call::Mime => session().mime(left, right, span), - crate::system::Call::Session(call) => call(session(), left, right, span), - crate::system::Call::Prefs => return crate::display::prefs(session(), right).map(|(value, shy)| Bound { shy, ..Bound::from(value) }), - crate::system::Call::Time => session().system_time(left, right, span), - crate::system::Call::Regex(regex, operation) => crate::regex::call(regex, *operation, left, right), - crate::system::Call::Distribution(d, operation) => crate::distribution::call(d, *operation, left, right, span), - crate::system::Call::Generator(g, _) => crate::primitive::drawn_value(Some(g), left, right, span), - crate::system::Call::Load => return session().load(right, span), - } - } - Fold(operand, kind) => fold(operand, *kind, None, left, right, span), - FunctionNode::Axis(f, spec) => return along_axes(f, spec, left, right, span), - FunctionNode::Array(items) => return function_array(items, left, right, span), - }?; - Ok(Bound::from(array)) + fn attached(&self, set: impl FnOnce(&mut Rules)) -> Self { + let mut data = (*self.0).clone(); let rules = data.rules.get_or_insert_default(); set(rules); Self(Arc::new(data)) } -} - -impl Function { - pub fn bpl(&self) -> String { self.text(Elide::NONE, &mut 1000) } - /// The system function behind this function, if it is one. - pub(crate) fn system_function(&self) -> Option<&crate::system::SystemFunction> { match self.node() { FunctionNode::System(f) => Some(f), _ => None } } - /// The name of the operator, train or composition that builds this function, and its operands. `None` for a dfn or a name. - pub fn parts(&self) -> Option<(String, Vec)> { - let node = self.node(); - let label = match node { - FunctionNode::Defined(_) | FunctionNode::Derived(..) | FunctionNode::LateBound(..) => return None, - FunctionNode::Ruled(f, _) => return f.parts(), - // Python names these nodes with words. - FunctionNode::Inverse(_) => "inverse".into(), - FunctionNode::Axis(..) => "axis".into(), - FunctionNode::Array(_) => "fn".into(), - node => node.label(Elide::NONE), - }; - Some((label, node.operands())) + pub fn with_inverse(&self, g: Function) -> Self { self.attached(|rules| rules.inverse = Some(g)) } + pub fn with_derivative(&self, g: Function) -> Self { self.attached(|rules| rules.derivative = Some(g)) } + fn with_rules(&self, rules: Option) -> Self { let mut data = (*self.0).clone(); data.rules = rules; Self(Arc::new(data)) } + fn inverse(&self, _: &Span) -> Result { + if let Some(inverse) = self.rules().and_then(|rules| rules.inverse.clone()) { return Ok(inverse); } + Operator::Kind(OperatorKind::Inverse).derive(Value::Function(self.clone()), None) } - pub fn inspect(&self, session: &Session) -> crate::Inspection { - if let FunctionNode::LateBound(parsed, _) = self.node() { - if let Some([Node { kind: NodeKind::Name(name, _), .. }]) = parsed.statements.first().map(|s| &s.nodes[..]) { - if let Some((_, Binding::Function(f))) = session.bindings.lookup(*name) { return crate::Inspection::new("function", f.bpl()); } - } - } - crate::Inspection::new("function", self.bpl()) - } - - /// The builtin function spelled `name`: a glyph, or a system name, which starts with `•`. - pub fn builtin(name: &str) -> Option { Builtin::find(name).map(Builtin::function) } - - /// The function that operator `kind` derives from `operands`, or a primitive's train or composition that `kind` names. - pub fn build(kind: &str, operands: Vec) -> Result { - let span = Span::whole(Source::new("", kind)); - let fun = |a: &Operand| Self::from_value(a.value(), &span); - let constant = |a: &Operand| match a { Operand::Value(v) => Self::constant(v.clone(), &span), _ => fun(a) }; - let glyph = kind.parse::().ok().map(crate::symbols::canonical); - let operator = glyph.and_then(OperatorKind::from_glyph); - let node = match (kind, operands.as_slice(), operator) { - ("fn", [a], ..) => return fun(a), ("fork", [a, b, c], ..) => FunctionNode::Fork([constant(a)?, fun(b)?, constant(c)?]), - ("axis", [f, Operand::Value(axis)], ..) => FunctionNode::Axis(fun(f)?, axis.clone()), ("⁻¹", [f], ..) => return fun(f)?.inverse(&span), - (_, [f], _) if let Some(mark) = crate::number::Mark::of(kind) => FunctionNode::Modified(OperatorKind::Super(Superscript::Mark(mark)), f.clone()), - (_, [f], Some(op)) if !op.info().dyadic() => FunctionNode::Modified(op, f.clone()), - (_, [a, b], Some(op)) if op.info().right.is_some() => FunctionNode::Composed(op, [a.clone(), b.clone()]), - _ => return Err(Error::new(ErrorKind::Syntax, "invalid function construction").at(&span)) - }; - Self::new(node, &span) + pub(crate) fn operand<'a>(&'a self, origin: &'a crate::program::Origin<&'a [Value]>, position: usize) -> Cow<'a, Value> { + if let Some(slot) = self.operand_slot(&origin.bindings, position) { return Self::capture_at(origin, &slot); } + self.node().operands()[position].resolve(origin) } -} - -// A lexical link is an index into active frames, never an owning reference. BPL results/array elements are arrays and assignments are local. Public function export rejects frame references throughout the function graph. -#[derive(Clone, Debug)] -pub(crate) struct Closure { definition: Arc, environment: Option, module: usize } - -impl Closure { fn text(&self) -> &str { self.definition.span.text() } } -impl PartialEq for Closure { - fn eq(&self, other: &Self) -> bool { - Arc::ptr_eq(&self.definition, &other.definition) && (self.environment, self.module) == (other.environment, other.module) + fn call(&self, left: Option<&Value>, right: &Value, span: &Span) -> Result { Bound::from_outputs(self.call_outputs(left, right, span)?, span) } + fn call_with(&self, left: Option, right: R, span: &Span) -> Result { + self.call(left.as_ref().map(Argument::value), right.value(), span) } -} -struct ArrayBinding { place: Place, value: Value } - -#[derive(Clone, Debug)] -/// An operand of an operator: an array or a function. -pub enum Operand { Value(Value), Function(Function) } -impl Operand { - fn equal_by(&self, other: &Self, values: &mut ValueRule<'_, E>) -> Result { - match (self, other) { - (Self::Value(x), Self::Value(y)) => values(x, y, true), (Self::Function(f), Self::Function(g)) => f.equal_by(g, values), _ => Ok(false) - } + fn call_shape<'a>(&self, arguments: &'a [Value]) -> (Operation, Cow<'a, [Value]>) { + let operation = self.operation().unwrap_or(Operation::Loop { exit: crate::primitive::Exit::Once, order: crate::primitive::Order::Forward, body_result: crate::primitive::LoopResult::Carry }); + let values = if operation.functions() == 0 { Cow::Borrowed(arguments) } + else { Cow::Owned(std::iter::once(Value::Function(self.clone())).chain(arguments.iter().cloned()).collect()) }; + (operation, values) } -} -/// An operand that a node holds, borrowed (`FunctionNode::visit_operands`). -#[derive(Clone, Copy)] -enum OperandRef<'a> { Value(&'a Value), Function(&'a Function) } -impl<'a> From<&'a Operand> for OperandRef<'a> { - fn from(a: &'a Operand) -> Self { match a { Operand::Value(v) => Self::Value(v), Operand::Function(f) => Self::Function(f) } } -} -impl OperandRef<'_> { - fn owned(self) -> Operand { match self { Self::Value(v) => Operand::Value(v.clone()), Self::Function(f) => Operand::Function(f.clone()) } } - fn environment(self) -> Option { match self { Self::Value(v) => v.environment(), Self::Function(f) => f.environment() } } -} - -#[derive(Clone, Debug)] -pub(crate) enum OperatorNode { Defined(Closure), Primitive(OperatorKind), Subscript(Value), Bound(Arc, Operand) } -impl OperatorNode { - fn equal_by(&self, other: &Self, values: &mut ValueRule<'_, E>) -> Result { - Ok(match (self, other) { - (Self::Defined(x), Self::Defined(y)) => x == y, (Self::Primitive(x), Self::Primitive(y)) => x == y, - (Self::Subscript(x), Self::Subscript(y)) => values(x, y, true)?, - (Self::Bound(n, a), Self::Bound(m, b)) => n.equal_by(m, values)? && a.equal_by(b, values)?, _ => false - }) + pub(crate) fn origin(&self, arguments: &[Value], literals: u64) -> Result>, Error> { + let (operation, roots) = self.call_shape(arguments); + operation.origin(&roots, literals << operation.functions()) } -} - -/// An operator held as a value, as in a record field. -#[derive(Clone, Debug)] -pub struct Operator(Arc); -impl Operator { - fn equal_by(&self, other: &Self, values: &mut ValueRule<'_, E>) -> Result { - Ok(Arc::ptr_eq(&self.0, &other.0) || self.0.equal_by(&other.0, values)?) + pub(crate) fn call_origin(&self, parent: &crate::program::Origin<&[Value]>, arguments: &[Value], from: &[Option]) -> Result>, Error> { + let (operation, roots) = self.call_shape(arguments); + let sources: Vec<_> = std::iter::repeat_n(None, operation.functions()).chain(from.iter().copied()).collect(); + projected_call(parent, operation, &roots, &sources) } - /// Whether the operators match within tolerance `t`, as `≡` compares them: built from matching parts, with `≡`'s rule for the values in them. - pub(crate) fn matches(&self, other: &Self, t: Numeric, span: &Span) -> Result { - self.equal_by(other, &mut |x, y, _| x.matches_within(y, t, span)) + fn call_outputs(&self, left: Option<&Value>, right: &Value, span: &Span) -> Result, Error> { + check().at(span)?; + let args: Vec<_> = left.into_iter().chain([right]).cloned().collect(); + let mut origin = self.origin(&args, 0).at(span)?; origin.site = Some(span.clone()); + Operation::dispatch(origin.reborrow()).at(span) } -} -impl PartialEq for Operator { fn eq(&self, other: &Self) -> bool { let Ok(equal) = self.equal_by(other, &mut same_values); equal } } - -/// The innermost environment that a node's operands read, and whether any operand is late-bound. -fn operand_dependencies(node: &FunctionNode) -> (Option, bool) { - let (mut environment, mut late) = (None, false); - node.visit_operands(|a| { - environment = environment.max(a.environment()); - late |= matches!(a, OperandRef::Function(f) if f.late()) - }); - (environment, late) -} - -/// Whether an operand is of a kind an operator accepts. -fn accepts(kind: OperandKind, a: &Operand) -> bool { - match kind { OperandKind::Function => matches!(a, Operand::Function(_)), OperandKind::Array => matches!(a, Operand::Value(_)), OperandKind::Any => true } -} - -/// An operand where an operator takes a function. An array that holds a function as a direct item calls its items there (`Function::from_array`). Any other array acts as a constant function, `A⍨`, as in `0⊘1`. -fn constant_operand(kind: OperandKind, a: Operand, span: &Span) -> Result { - match (kind, a) { - (OperandKind::Function, Operand::Value(v)) => { - Ok(Operand::Function(if v.holds_functions() { Function::from_array(v, span)? } else { Function::constant(v, span)? })) + pub(crate) fn call_array(&self, left: Option<&Value>, right: &Value, span: &Span) -> Result { self.call_outputs(left, right, span)?.assemble().at(span) } + fn run_body(&self, origin: crate::program::Origin<&[Value]>) -> Result { + match self.node() { + FunctionNode::Applied { op: Operator::Kind(kind), .. } => (kind.info().call)(self, origin), + FunctionNode::Defined(_) | FunctionNode::Applied { op: Operator::Closure(_), .. } => session().call_defined(self, origin), + FunctionNode::Train(_) => operators::train_call(self, origin), + FunctionNode::Array(_) => operators::function_array(self, origin), + FunctionNode::LateBound(_) => session().call_named(self, origin), + FunctionNode::Primitive(_) | FunctionNode::System { .. } => { + let args = Self::body_arguments(&origin)?; + let ranges = Self::argument_ranges(&origin)?; + let from: Vec<_> = ranges.iter().flatten().map(|range| Some(range.start)).collect(); + self.call_bound(&origin, &args, &from) + } } - (_, a) => Ok(a), } -} - -impl OperatorNode { - fn text(&self, el: Elide) -> String { - match self { - Self::Defined(c) => c.text().into(), Self::Primitive(op) => el.notation.glyph(op.glyph()).into(), - Self::Subscript(index) => format!("{}({})", el.notation.glyph(OperatorKind::Axis.glyph()), Operand::Value(index.clone()).text(el, &mut 1000)), - Self::Bound(op, right) => format!("{}({})", op.text(el), right.text(el, &mut 1000)) + fn discards(&self, dyadic: bool) -> bool { + if self.rules().is_some_and(|rules| rules.inverse.is_some()) { return false; } + match self.node() { + FunctionNode::Primitive(p) => p.discards(dyadic), + FunctionNode::Applied { op: Operator::Kind(kind), operands } => { + let values: Vec<_> = operands.iter().filter_map(|operand| match operand { FunctionOperand::Value { value: value, .. } => Some(value.clone()), _ => None }).collect(); + (kind.info().discards)(kind, &values, dyadic) + } + FunctionNode::Train(_) => operators::train_discards(self, dyadic), + _ => false, } } - fn is_dyadic(&self) -> bool { - match self { - Self::Defined(c) => c.definition.kind == DefinitionKind::DyadicOperator, Self::Primitive(op) => op.info().dyadic(), Self::Subscript(_) | Self::Bound(..) => false + pub(crate) fn implicit_generator(&self) -> bool { + match self.node() { + FunctionNode::Primitive(primitive) => primitive.implicit_generator(), + FunctionNode::System { function, .. } => function.implicit_generator(), + FunctionNode::Defined(definition) => definition.implicit_generator(), + _ => false, } } - - fn derive(self, operand: Operand, span: &Span) -> Result { - let node = match self { - Self::Defined(c) => FunctionNode::Derived(c, operand, None), - Self::Primitive(op) => FunctionNode::Modified(op, operand), - Self::Subscript(index) => FunctionNode::Composed(OperatorKind::Axis, [operand, Operand::Value(index)]), - Self::Bound(op, right) => match Arc::unwrap_or_clone(op) { - Self::Defined(c) => FunctionNode::Derived(c, operand, Some(right)), Self::Primitive(op) => FunctionNode::Composed(op, [operand, right]), - Self::Subscript(_) | Self::Bound(..) => unreachable!() - }, - }; - Function::new(node, span).map(Binding::Function) + pub(crate) fn draws(&self) -> bool { + self.implicit_generator() || matches!(self.node(), FunctionNode::System { function, .. } if function.call.draws()) } - /// The innermost frame the operator depends on: its dfn's, or that of an operand bound to it. - fn environment(&self) -> Option { - match self { - Self::Defined(c) => c.environment, Self::Primitive(_) | Self::Subscript(_) => None, - Self::Bound(op, right) => op.environment().max(OperandRef::from(right).environment()) + pub(crate) fn acts(&self) -> bool { + self.acting(&mut rustymath::hashing::HashSet::default()) + } + fn acting(&self, seen: &mut rustymath::hashing::HashSet) -> bool { + if !seen.insert(self.identity()) { return false; } + match self.node() { + FunctionNode::Primitive(Primitive::Execute) => true, + FunctionNode::System { function, .. } => function.acts(), + FunctionNode::Defined(d) => d.acts(), + FunctionNode::Applied { op: Operator::Closure(f), .. } if f.acting(seen) => true, + _ => self.operands().iter().any(|operand| matches!(operand, FunctionOperand::Value { value: Value::Function(f), .. } if f.acting(seen))), } } -} - -impl Operator { - pub(crate) fn text(&self, el: Elide) -> String { self.0.text(el) } - pub(crate) fn environment(&self) -> Option { self.0.environment() } - /// The function `f op` or `f op g` derives, with `left` as `f` and `right` as `g`. - pub fn derive(&self, left: Operand, right: Option) -> Result { - let span = Span::whole(Source::new("", self.to_string())); - let span = &span; - let node = match right { - Some(right) if self.0.is_dyadic() => OperatorNode::Bound(self.0.clone(), right), None if !self.0.is_dyadic() => (*self.0).clone(), - _ => return Err(Error::domain(if self.0.is_dyadic() { "this operator takes two operands" } else { "this operator takes one operand" }).at(span)) + pub fn bpl(&self) -> String { self.text(Elide::NONE, &mut 1000) } + pub(crate) fn text(&self, el: Elide, budget: &mut usize) -> String { + if *budget == 0 { return "…".into(); } *budget -= 1; + let text = |operand: &FunctionOperand, right: bool, budget: &mut usize| match operand { + FunctionOperand::Value { value: Value::Function(f), .. } => if right { f.right_text(el, budget) } else { f.left_text(el, budget) }, + FunctionOperand::Value { value: value, .. } => value.operand(el), FunctionOperand::Input { position: position, .. } => format!("${}", position.index), }; - Function::from_value(node.derive(left, span)?, span) + match self.node() { + FunctionNode::Primitive(p) => el.notation.glyph(p.glyph()).into(), + FunctionNode::System { function, .. } => system_name(function.name, el.notation), + FunctionNode::Defined(d) => d.span.text().into(), FunctionNode::LateBound(name) => name.text().into(), + FunctionNode::Array(value) => text(value, false, budget), + FunctionNode::Applied { op, operands } => { + let left = text(&operands[0], false, budget); + let right = operands.get(1).map(|operand| text(operand, true, budget)).unwrap_or_default(); + format!("{left}{}{right}", op.bpl()) + } + FunctionNode::Train(tines) => { + let mut result = String::new(); + for tine in tines { + let next = text(tine, false, budget); + if result.chars().last().is_some_and(crate::syntax::name_char) && next.chars().next().is_some_and(crate::syntax::name_char) { result.push('⁝'); } + result.push_str(&next); + } + result + } + } } -} - -impl std::fmt::Display for Operator { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.text(Elide::NONE)) } } - -impl Operand { - fn text(&self, el: Elide, budget: &mut usize) -> String { match self { Self::Value(a) => a.source(el), Self::Function(f) => f.text(el, budget) } } fn left_text(&self, el: Elide, budget: &mut usize) -> String { - match self { Self::Value(a) => a.operand(el), Self::Function(f) => f.left_text(el, budget) } + if matches!(self.node(), FunctionNode::Train(_)) { format!("({})", self.text(el, budget)) } else { self.text(el, budget) } } fn right_text(&self, el: Elide, budget: &mut usize) -> String { - match self { Self::Value(a) => a.operand(el), Self::Function(f) => f.right_text(el, budget) } + if matches!(self.node(), FunctionNode::Primitive(_) | FunctionNode::System { .. } | FunctionNode::Defined(_) | FunctionNode::LateBound(_)) { self.text(el, budget) } + else { format!("({})", self.text(el, budget)) } } + pub(crate) fn superscripted(&self, mark: &str, el: Elide, budget: &mut usize) -> String { format!("{}{mark}", self.left_text(el, budget)) } fn tree(&self, el: Elide, budget: &mut usize) -> crate::display::Tree { - match self { Self::Function(f) => f.tree(el, budget), _ => crate::display::Tree::leaf(self.text(el, budget)) } + if *budget == 0 { return crate::display::Tree::leaf("…"); } *budget -= 1; + let label = match self.node() { FunctionNode::Applied { op, .. } => op.bpl(), FunctionNode::Train(_) => "train".into(), _ => return crate::display::Tree::leaf(self.text(el, budget)) }; + crate::display::Tree { label, children: self.operands().iter().map(|operand| match operand { + FunctionOperand::Value { value: Value::Function(f), .. } => f.tree(el, budget), FunctionOperand::Value { value: value, .. } => crate::display::Tree::leaf(value.operand(el)), + FunctionOperand::Input { position: position, .. } => crate::display::Tree::leaf(format!("${}", position.index)), + }).collect() } + } + pub fn parts(&self) -> Option<(String, Vec)> { + let name = match self.node() { FunctionNode::Applied { op: Operator::Kind(kind), .. } => kind.info().python, + FunctionNode::Train(_) => "train", FunctionNode::Array(_) => "fn", FunctionNode::Primitive(_) | FunctionNode::System { .. } => return Some((self.bpl(), vec![])), _ => return None }; + Some((name.into(), self.operands().iter().map(|operand| match operand { FunctionOperand::Value { value: value, .. } => value.clone(), _ => unreachable!("only real function values are inspected") }).collect())) + } + pub fn inspect(&self, _: &Session) -> crate::Inspection { crate::Inspection::new("function", self.bpl()) } + pub fn builtin(name: &str) -> Option { + match crate::inspection::item(name, false)? { NodeKind::Function(builtin) => Some(builtin.function()), _ => None } + } + pub fn build(name: &str, operands: Vec) -> Result { + if operands.is_empty() { return Self::builtin(name).ok_or_else(|| Error::domain("unknown function")); } + if name == "train" { return Self::train_operands(operands.into_iter().map(FunctionOperand::from).collect(), &Span::whole(Source::new("", ""))); } + if name == "fn" && operands.len() == 1 { return Self::from_array(operands.into_iter().next().unwrap(), &Span::whole(Source::new("", ""))); } + let kind = OperatorKind::all().find(|kind| kind.info().python == name).ok_or_else(|| Error::domain("unknown operator"))?; + let mut operands = operands.into_iter(); Operator::Kind(kind).derive(operands.next().ok_or_else(|| Error::domain("missing operator operand"))?, operands.next()) } - - fn from_value(value: Binding) -> Self { - match value { Binding::Value(a) => Self::Value(a), Binding::Function(f) => Self::Function(f), _ => unreachable!() } - } - fn value(&self) -> Binding { match self { Self::Value(a) => Binding::Value(a.clone()), Self::Function(f) => Binding::Function(f.clone()) } } } +fn system_name(name: &str, notation: Notation) -> String { name.strip_prefix('•').map_or_else(|| name.into(), |name| format!("{}{name}", notation.glyph("•"))) } + +pub(super) fn operation_call(operation: Operation, left: Option<&Value>, right: &Value, span: &Span) -> Result { + let args: Vec<_> = left.into_iter().chain([right]).cloned().collect(); operation.call(&args, 0).map_err(|error| error.at(span)) +} #[derive(Clone, Debug)] pub(crate) enum Binding { NoResult, - /// `⍺` in a call with one argument. It drops out of the expression that uses it. Absent, Value(Value), - Function(Function), - Operator(OperatorNode), - /// The slot of a dfn's local, `⍵` or `⍺` after its last read (`syntax::release`). Lookup stops there, and a read of it is a VALUE error. + Function(FunctionBinding), + Operator(OperatorValue), Released, } +impl From for Binding { fn from(function: Function) -> Self { Self::Function(FunctionBinding::Input(function)) } } impl Binding { fn class(&self) -> i64 { - match self { - Self::NoResult | Self::Absent | Self::Released => 0, Self::Value(_) => SUBJECT_CLASS, Self::Function(_) => FUNCTION_CLASS, - Self::Operator(_) => OPERATOR_CLASS - } + match self { Self::Value(_) => SUBJECT_CLASS, Self::Function(_) => FUNCTION_CLASS, Self::Operator(_) => OPERATOR_CLASS, _ => 0 } } fn inspection(&self, session: &Session) -> Option { Some(match self { - Self::Function(f) => f.inspect(session), Self::Operator(op) => crate::Inspection::new("operator", op.text(Elide::NONE)), - Self::Value(a) => crate::Inspection::new("array", a.to_string()), Self::NoResult | Self::Absent | Self::Released => return None + Self::Function(_) => Function::from_value(self.clone(), &Span::whole(Source::new("", ""))).ok()?.inspect(session), + Self::Operator(op) => crate::Inspection::new("operator", op.bpl()), + Self::Value(value) => crate::Inspection::new("array", session.show(value).ok()?), _ => return None, }) } - #[inline] fn from_element(element: Value) -> Self { - match element { Value::Function(f) => Self::Function(f), Value::Operator(op) => Self::operator(op), _ => Self::Value(element) } + match element { Value::Function(function) => function.into(), Value::Operator(operator) => Self::Operator((*operator).clone()), value => Self::Value(value) } } - /// An operator element as a binding, out of line so that `from_element` stays small enough to inline. - #[cold] - fn operator(op: Operator) -> Self { Self::Operator(Arc::unwrap_or_clone(op.0)) } fn environment(&self) -> Option { - match self { Self::Value(a) => a.environment(), Self::Function(f) => f.environment(), Self::Operator(op) => op.environment(), _ => None } + match self { + Self::Value(value) => value.environment(), Self::Function(FunctionBinding::Input(function)) => function.environment(), + Self::Function(FunctionBinding::Rebuild { dependencies, .. }) => dependencies.environment(), Self::Operator(operator) => operator.environment(), _ => None, + } } - #[inline] - fn into_value(self, span: &Span) -> Result { match self { Self::Value(a) => Ok(a), other => other.into_other(span) } } - /// `into_value` for anything but an array, out of line so that `into_value` stays small enough to inline. - #[cold] - fn into_other(self, span: &Span) -> Result { + fn into_value(self, span: &Span) -> Result { match self { - Self::Value(a) => Ok(a), v @ Self::Function(_) => Ok(Value::Function(Function::from_value(v, span)?)), - Self::Operator(op) => Ok(Value::Operator(Operator(Arc::new(op)))), + Self::Value(value) => Ok(value), binding @ Self::Function(_) => Function::from_value(binding, span).map(Value::Function), + Self::Operator(operator) => Ok(Value::Operator(Arc::new(operator))), Self::NoResult => Err(Error::new(ErrorKind::Value, "expression produced no value").at(span)), - Self::Absent => Err(Error::new(ErrorKind::Value, ABSENT).at(span)), Self::Released => Err(released("a name", span)) + Self::Absent => Err(Error::new(ErrorKind::Value, ABSENT).at(span)), Self::Released => Err(released("a name", span)), } } } -struct Bound { value: Binding, shy: bool, expression: Option> } -struct Application { function: Function, left: Option, right: Value, span: Span, unshy: bool, expression: Option> } + +impl Function { + fn from_value(binding: Binding, span: &Span) -> Result { + match binding { + Binding::Function(FunctionBinding::Input(function)) | Binding::Value(Value::Function(function)) => Ok(function), + Binding::Function(FunctionBinding::Rebuild { template, dependencies }) => dependencies.rebuild(&template).map_err(|error| error.at(span)), + Binding::Value(value) if value.holds_functions() => Self::from_array(value, span), + _ => Err(Error::new(ErrorKind::Syntax, "call requires a function expression").at(span)), + } + } +} +pub(crate) struct Bound { value: Binding, shy: bool, expression: Option> } +struct Application { origin: crate::program::Origin>, span: Span, unshy: bool } enum Step { Done(Bound), Tail(Application) } /// An element as a binding: a function stays a function, and anything else is a value. @@ -1017,8 +1073,13 @@ impl From for Bound { impl Bound { #[inline] fn new(value: Binding) -> Self { Self { value, shy: false, expression: None } } + pub(super) fn from_outputs(outputs: crate::program::Outputs, span: &Span) -> Result { + let shy = outputs.shy; + let value = if outputs.as_ref().is_empty() { Binding::NoResult } else { Binding::from_element(outputs.assemble().at(span)?) }; + Ok(Self { value, shy, expression: None }) + } #[inline] - fn array(self, span: &Span) -> Result { self.value.into_value(span) } + pub(crate) fn array(self, span: &Span) -> Result { self.value.into_value(span) } fn result(self, span: &Span) -> Result { match self.value { Binding::Value(_) | Binding::Function(_) | Binding::NoResult => Ok(self), @@ -1031,42 +1092,34 @@ impl Bound { /// Final value and ordered output are independent. Errors retain already-produced output. #[derive(Debug, Default)] pub struct Evaluation { - pub value: Option, pub function: Option, pub operator: Option, pub output: Vec, pub error: Option + pub value: Option, pub function: Option, pub operator: Option, pub output: Vec, pub error: Option } impl Evaluation { pub fn output_text(&self) -> Vec<&str> { self.output.iter().map(Output::text).collect() } pub(crate) fn failed(error: Error) -> Self { Self { error: Some(error), ..Self::default() } } } +/// Calls and reads reach the thread's current session (`Session::current`), as JAX keeps its trace state per thread, and what they change has interior mutability. pub struct Session { pub(crate) execution: crate::execution::Execution, pub(crate) prefacing: std::cell::Cell, - /// Every binding, and where code is running (`bindings::Bindings`). bindings: Bindings, - /// The files being loaded. Loading one of them again is a cycle. loading: std::cell::RefCell>, - /// Empty vectors that the binder reuses for its stacks within one evaluation, so that an expression allocates none for them. `evaluation` frees them when it ends. - stacks: std::cell::RefCell>>, depth: std::cell::Cell, - /// Whether an empty map is calling its operand on prototypes to find its result's prototype. pub(crate) prototype: std::cell::Cell, capture: std::cell::Cell, - /// Reports that `•prefs ["fusion":$t]` has written. An enclosing call reports a pending argument only when no call inside it reported. - pub(crate) fusion_reports: std::cell::Cell, - /// The running `∂` calls and maps. - levels: std::cell::RefCell, - /// Whether a level is running. `begin` and `end` keep it in step with `levels`, so a call tests it without borrowing `levels`. - transforming: std::cell::Cell, - /// The session's random numbers, which `¿` and a distribution's `sample` draw when no `•rand` generator is given. + pub(crate) levels: crate::transform::Levels, + pub(crate) traces: std::cell::RefCell, pub(crate) generator: crate::distribution::Generator, - /// Each generator that a running batched try has drawn from, with its state before the try's first draw from it, for the try to put back where it falls back (`distribution::Drawn`). - pub(crate) drawn: crate::distribution::Drawn, - /// The program's command-line arguments. pub(crate) args: std::cell::RefCell>, - /// The programs that `∂` builds for functions other than dfns, kept by the functions' structure (`program::tacit`). - pub(crate) tacit: crate::program::Tacit, + pub(crate) error: rustymath::error::ErrorSlot, + pub(crate) token: std::cell::RefCell>, + pub(crate) guards: std::cell::RefCell>, } +#[derive(Default)] +pub(crate) struct GuardScope { pub(crate) nodes: Vec>, pub(crate) live: usize } + /// The process's one session, and its turns. A thread takes a turn to use the session, and another thread that wants a turn waits until it ends. `holder` is the thread with the turn and how many turns it holds. struct Slot { holder: std::sync::Mutex<(Option, usize)>, free: std::sync::Condvar, session: std::cell::UnsafeCell>, @@ -1123,11 +1176,118 @@ impl std::ops::Deref for Turn { impl Session { /// Runs `f` in prototype mode when `on`, then restores the caller's mode, after success and after an error. - fn prototype_mode(&self, on: bool, f: impl FnOnce() -> T) -> T { + pub(crate) fn prototype_mode(&self, on: bool, f: impl FnOnce() -> T) -> T { let previous = self.prototype.get(); self.prototype.set(previous | on); let result = f(); self.prototype.set(previous); result } + pub(crate) fn frame_depth(&self) -> usize { self.bindings.depth() } + /// Interfaces "Errors", Measures: at a checkpoint, takes the slot's error as `Err`, so a later read runs the failed work again and raises again. + pub fn raise(&self) -> Result<(), Error> { self.error.take().map_or(Ok(()), |error| Err(error.into())) } + /// Borrows the current session without clearing it. + pub fn current(look: impl FnOnce(&Session) -> R) -> R { + let turn = Turn::take(); turn.error.within(|| look(&turn)) + } + pub fn within(&self, run: impl FnOnce() -> R) -> R { + let _turn = Turn::take(); + assert!(std::ptr::eq(self, session()), "the turn owns this session"); + self.error.within(run) + } +} +pub(crate) fn projected_call(parent: &crate::program::Origin<&[Value]>, operation: Operation, args: &[Value], from: &[Option]) -> Result>, Error> { + let active = session().bindings.active_origin(parent)?; + let inherited = active.as_ref().map(|origin| origin.reborrow()); + let parent = inherited.as_ref().unwrap_or(parent); + assert_eq!(args.len(), from.len()); + let functions = operation.functions(); + let (trees, mut list) = if functions == 0 { (None, args.to_vec()) } else { + match crate::array::flatten(args) { Some((trees, leaves)) => (Some(trees), leaves), None => (None, args.to_vec()) } + }; + let explicit = list.len(); + let mut sources = vec![None; explicit]; + if let Some(trees) = &trees { + for (range, source) in trees.leaf_ranges().zip(from) { + for (offset, position) in range.enumerate() { sources[position] = source.map(|start| start + offset); } + } + } else { sources.copy_from_slice(from); } + for setting in operation.settings() { + let old = parent.operation.settings().iter().position(|s| s == setting) + .ok_or_else(|| Error::domain("a delegated operation is missing its captured numeric setting"))?; + let at = parent.explicit as usize + old; + list.push(parent.list[at].clone()); sources.push(Some(at)); + } + let capture_start = list.len(); + let (bindings, captured_trees, capture_literals) = if !args.iter().any(Value::holds_functions) { (CaptureBindings::empty(), None, 0) } else { + parent.bindings.project(args, parent, &mut list, &mut sources)? + }; + let result_trees = match (trees, captured_trees) { + (None, None) => None, + (trees, captures) => Some(Arc::new(trees.as_deref().cloned().unwrap_or_else(|| crate::array::Trees::leaves(explicit)) + .concat(&crate::array::Trees::leaves(capture_start - explicit)) + .concat(&captures.as_deref().cloned().unwrap_or_else(|| crate::array::Trees::leaves(list.len() - capture_start))))), + }; + let mut origin = parent.rebuilt(operation, list, &sources, explicit as u32, args.len() as u32, result_trees); + origin.bindings = bindings; + origin.literals |= capture_literals.checked_shl(capture_start as u32).unwrap_or(0); + Ok(origin) +} + +pub(crate) fn literal_leaves(trees: Option<&crate::array::Trees>, roots: u64) -> u64 { + let Some(trees) = trees else { return roots; }; + let mut leaves = 0; + for (root, range) in trees.leaf_ranges().enumerate() { + if root >= 64 || roots & (1 << root) == 0 { continue; } + for leaf in range.take_while(|&leaf| leaf < 64) { leaves |= 1 << leaf; } + } + leaves +} + +pub(crate) fn visit_functions(value: &Value, visit: &mut dyn FnMut(&Function) -> Result<(), E>) -> Result<(), E> { + match value { + Value::Function(function) => visit(function)?, + Value::Operator(operator) => match &**operator { + OperatorValue::Operator(Operator::Closure(function)) => visit(function)?, + OperatorValue::Bound { op, right } => { if let Operator::Closure(function) = op { visit(function)?; } if let Some(value) = right.stored() { visit_functions(value, visit)?; } } + _ => {} + }, + Value::Array(array) if value.holds_functions() => { + if let crate::array::ArrayData::Mixed { items, prototype, .. } = &**array { + for value in items.iter().chain(prototype.iter()) { visit_functions(value, visit)?; } + } + } + _ => {} + } + Ok(()) +} + + +pub(crate) fn map_roots(origin: &crate::program::Origin<&[Value]>, mut replace: impl FnMut(&TreeRoot) -> Result>>)>, Error>) -> Result>, Error> { + let roots: Vec<_> = origin.trees.as_ref().map_or_else( + || (0..origin.list.len()).map(|index| TreeRoot { index, leaves: index..index + 1 }).collect(), |trees| trees.roots().collect()); + let (mut values, mut trees, mut sources, mut mapped) = (Vec::new(), crate::array::Trees::leaves(0), Vec::new(), Vec::with_capacity(roots.len())); + let mut has_trees = origin.trees.is_some(); + for root in roots { + let first = values.len(); + if let Some((value, from)) = replace(&root)? { + has_trees |= value.holds_mixed(); trees.push(&value, &mut values); + if let Some(from) = from { assert_eq!(values.len() - first, from.len()); sources.extend(from); } + else { sources.extend(std::iter::repeat_n(None, values.len() - first)); } + } else { + if let Some(original) = &origin.trees { values.extend_from_slice(&origin.list[root.leaves.clone()]); trees.append_root(original, &root); } + else { values.push(origin.list[root.leaves.start].clone()); trees.push_leaf(); } + sources.extend(root.leaves.map(Some)); + } + mapped.push(TreeRoot { index: root.index, leaves: first..values.len() }); + } + let boundary = |index: usize| if index == mapped.len() { Some(values.len()) } else { mapped.get(index).map(|root| root.leaves.start) }; + let explicit = boundary(origin.explicit_roots as usize).ok_or_else(Error::planning)?; + let root_start = origin.capture_root_start(); + let tail = boundary(root_start).ok_or_else(Error::planning)?; + let bindings = origin.bindings.reindexed(|root| mapped.get(root_start + root.index) + .map(|new| TreeRoot { index: root.index, leaves: new.leaves.start - tail..new.leaves.end - tail })).ok_or_else(Error::planning)?; + let mut result = origin.rebuilt(origin.operation.clone(), values, &sources, explicit as u32, origin.explicit_roots, has_trees.then(|| Arc::new(trees))); + result.bindings = Arc::new(bindings); + Ok(result) } diff --git a/src/eval/assign.rs b/src/eval/assign.rs index 835f5da6..1af8e5bc 100644 --- a/src/eval/assign.rs +++ b/src/eval/assign.rs @@ -1,115 +1,36 @@ //! Assignment: plain, destructuring, modified and selective. -use super::tree::{Fact, Facts}; +use super::tree::{Facts, Last, Layer, Split, Target, To}; use super::*; use rustymath::error::Kind; +struct ArrayBinding { place: Place, value: Value } + impl Session { - /// Whether `nodes` is one name that can hold an array, or brackets of such names, as in `[a b]←`. - fn assignment_names(&self, nodes: &[Node]) -> bool { + /// Whether `nodes` is one name that can hold an array, or brackets of such names, as in `[a b]←`. `log` keeps what it reads about what names hold (`tree::Fact`). + pub(super) fn assignment_names(&self, nodes: &[Node], log: &mut Facts) -> bool { let [node] = nodes else { return false }; match &node.kind { NodeKind::Name(name, _) => { (self.bindings.current().is_some() && name.implicit().is_none()) - || !matches!(self.bindings.category(*name), Some(Category::Function | Category::Operator | Category::DyadicOperator)) + || !matches!(self.path_category(*name, &[], log), Category::Function | Category::Operator | Category::DyadicOperator) } - NodeKind::Group(inner) => self.assignment_names(inner), - // Inside brackets every name is a target, whatever it holds. A pattern's cells each have a key, which `targets` checks. - NodeKind::ArrayLiteral { cells, form: ListForm::Items | ListForm::Rows, record } => cells.iter().all(|c| { - if *record { crate::syntax::key_colon(c).is_some() } - else { matches!(&c[..], [Node { kind: NodeKind::Name(_, _), .. }]) || self.assignment_names(c) } - }), - _ => false, - } - } - - /// Whether `node` holds an array (`static_category`). - pub(super) fn holds_array(&self, node: &Node, log: &mut Facts) -> bool { self.static_category(node, log) == Category::Value } - - /// The category of what `node` holds, read without evaluating anything. A name has its binding's category, and a dot path such as `m.f` has its field's (`path_category`). Any other group or run has its expression's category, as the operand grammar reads it (`trailing_operand`): `(M←-)` and `(⌽@ 1⌷)` are functions. While a statement records, `log` keeps each name and dot path read (`tree::Fact`). - pub(super) fn static_category(&self, node: &Node, log: &mut Facts) -> Category { - use Category::*; - match &node.kind { - NodeKind::Function(_) | NodeKind::System(_) => Function, - NodeKind::Operator(op) => Category::of_operator(&self::OperatorNode::Primitive(*op)), - NodeKind::Subscript(_) => Operator, - NodeKind::Name(name, _) => self.path_category(*name, &[], log), - NodeKind::Dfn(d) => match d.kind { - DefinitionKind::Function => Function, DefinitionKind::MonadicOperator => Operator, DefinitionKind::DyadicOperator => DyadicOperator - }, - NodeKind::Group(_) | NodeKind::Run(_) => self.trailing_operand(std::slice::from_ref(node), 1, log).map_or(NoResult, |(_, c)| c), - _ => Value, + NodeKind::Group(inner) => self.assignment_names(inner, log), + _ => crate::syntax::plain_target(nodes), } } - /// The category of `name`'s binding, or of the field that `keys` reach from the array it holds (`Category::field`), read without evaluating anything. While a statement records, `log` keeps what it read (`tree::Fact`). + /// The category of `name`'s binding, or of the field that `keys` reach from the array it holds, as its role gives it (`Session::path_role`, which logs the fact), read without evaluating anything. An array that holds a function, an unbound name and an absent `⍺` count as arrays. pub(super) fn path_category(&self, name: Name, keys: &[Arc], log: &mut Facts) -> Category { - let category = - if keys.is_empty() { Category::named(self.bindings.category(name)) } else { Category::field(self.bindings.lookup(name).map(|(_, b)| b), keys) }; - if let Some(log) = log { - let fact = Fact { address: self.bindings.address(name), keys: keys.into(), category }; - if !log.contains(&fact) { log.push(fact); } - } - category - } - - pub(super) fn trailing_operand(&self, nodes: &[Node], end: usize, log: &mut Facts) -> Result<(usize, Category), Error> { - use Category::*; - if end == 0 { return Err(Error::new(ErrorKind::Syntax, "missing assignment operand").at(&nodes[0].span)); } - let mut start = end - 1; - let mut category = match &nodes[start].kind { - NodeKind::Group(inner) | NodeKind::Run(inner) => match dot_path(&nodes[start]) { - Some((name, keys)) => self.path_category(name, &keys, log), - None if inner.is_empty() => return Err(Error::new(ErrorKind::Syntax, "empty assignment operand").at(&nodes[start].span)), - // Rewrite the group's own dot access first, so that `(x.a).b` reads `(x.a)` as the field it names. - None => { let inner = self.members_with(inner, log); self.trailing_operand(&inner, inner.len(), log)?.1 } - }, - _ => self.static_category(&nodes[start], log), - }; - if matches!(category, Operator) && start > 0 { - let superscript = match &nodes[start].kind { NodeKind::Operator(OperatorKind::Super(s)) => Some(*s), _ => None }; - let (operand, operand_category) = self.trailing_operand(nodes, start, log)?; - start = operand; - // `ᵘ` always gives an array, and the other superscripts give one on an array. - category = match superscript { - Some(Superscript::Unit) => Value, - _ if matches!(operand_category, Value) && (superscript.is_some() || matches!(nodes[end - 1].kind, NodeKind::Subscript(_))) => Value, - _ => Function, - }; - } - if start > 0 && matches!(&nodes[start - 1].kind, NodeKind::Run(inner) if self.ends_in_dyadic_operator(inner, log)) { - // A run that ends in a dyadic operator holds its left operand, and takes this operand as its right one, as in `(⌽@ 1⌷)`. - start -= 1; - category = Function; - } else if start > 0 && matches!(Rule::get(self.static_category(&nodes[start - 1], log), category), Rule::BindRight) { - start = self.trailing_operand(nodes, start - 1, log)?.0; - category = Function; - } - Ok((start, category)) - } - - /// The target of `←` is the run before it: an array, followed by the function of a modified assignment such as `x+←1`. - pub(super) fn assignment_start(&self, nodes: &[Node]) -> Result { - if self.rule_target(nodes).is_some() { return Ok(0); } - let end = nodes.len(); - let (operand, category) = self.trailing_operand(nodes, end, &mut None)?; - // Inside a dfn a name before `←` is a target, unless an array comes directly before it. Then the name's value decides, as at top level: a function makes a modified assignment, as in `a(f)←3` and `(a)f←3`. - let modified = operand > 0 && matches!(category, Category::Function) - && (self.holds_array(&nodes[operand - 1], &mut None) || !self.assignment_names(&nodes[operand..])); - let start = if modified { self.trailing_operand(nodes, operand, &mut None)?.0 } else { operand }; - if start > 0 && self.holds_array(&nodes[start], &mut None) && self.holds_array(&nodes[start - 1], &mut None) { - let span = Span { source: nodes[start].span.source.clone(), range: nodes[start - 1].span.range.start..nodes[start].span.range.end }; - return Err(Error::new(ErrorKind::Syntax, STRAND_TARGET).at(&span)); - } - Ok(start) + let role = self.path_role(name, keys, log); + match role.0 { Category::Callable | Category::Absent => Category::Value, category => category } } /// Inside a dfn, `⍵` is a local name bound to the argument. The other argument and operand names can't be assigned. fn assignable(&self, name: Name) -> bool { name.implicit().is_none() || name == Name::OMEGA && self.bindings.current().is_some() } fn store(&self, name: Name, value: Binding, span: &Span) -> Result<(), Error> { if !self.assignable(name) { return Err(Error::new(ErrorKind::Syntax, "arguments and operands cannot be assigned here").at(span)); } - self.bindings.insert(self.bindings.here(name), value); - Ok(()) + self.store_at(self.bindings.here(name), value, span) } fn array_binding(&self, name: Name, span: &Span) -> Result { @@ -120,150 +41,112 @@ impl Session { Ok(ArrayBinding { place, value }) } - fn update_array(&self, binding: &ArrayBinding, value: Value, span: &Span) -> Result<(), Error> { - if value.environment() > binding.place.frame() { return Err(Error::domain("array would export a local closure").at(span)); } - if self.transforming() { self.record_write(binding.place).at(span)?; } - self.bindings.insert(binding.place, Binding::from_element(value)); + fn update_array(&self, binding: &ArrayBinding, value: Value, span: &Span) -> Result<(), Error> { self.store_at(binding.place, Binding::from_element(value), span) } + pub(super) fn store_at(&self, place: Place, value: Binding, span: &Span) -> Result<(), Error> { + let value = match value { + Binding::Function(FunctionBinding::Rebuild { .. }) => Function::from_value(value, span)?.into(), + value => value, + }; + if value.environment() > place.frame() { return Err(Error::domain("value would export a local closure").at(span)); } + if place.frame() != self.bindings.current() { + if self.bindings.get(place).is_some_and(|old| Category::bound(&old) != Category::bound(&value)) { + return Err(Error::new(ErrorKind::Syntax, "an assignment can't change a name outside its function between an array, a function and an operator").at(span)); + } + } + self.writing(place, span)?; + self.bindings.insert(place, value); Ok(()) } - /// The step after the binder, or a recording, finds where an assignment's target starts: `value` assigned to `target`, or with no value, `a(f)←`'s update (`modify_monadic`). A target that is a plain name with slot `local` in the current frame stores there, because storing a name computes nothing for `assign` to report. Gives the assignment's value, which is no result for an update. `span` is `←`'s. + /// Stores a plain assignment and returns its right argument. #[inline] - pub(super) fn assigned(&self, target: &[Node], value: Option, local: Option, span: &Span) -> Result { - let Some(value) = value else { - return if self.modify_monadic(target)? { Ok(Binding::NoResult) } else { Err(Error::new(ErrorKind::Syntax, NO_TARGET).at(span)) }; - }; + pub(super) fn assigned(&self, target: &Target, value: Option, local: Option, span: &Span) -> Result { + let Some(value) = value else { return Err(Error::new(ErrorKind::Syntax, NO_TARGET).at(span)) }; match local { - Some(k) => self.bindings.insert(Place::Slot(self.bindings.current().expect("a compiled statement runs in its dfn's frame"), k), value.clone()), - None => self.assign(target, &value)?, + Some(slot) => self.store_at(self.bindings.local(slot), value.clone(), span)?, + None => self.assign(target, None, &value)?, } Ok(value) } - /// Assigns `value` to `target`. `fusion_report` reports an assignment that computed a pending value, as display and destructuring do, as `←`. - pub(super) fn assign(&self, target: &[Node], value: &Binding) -> Result<(), Error> { - let watch = match value { Binding::Value(v) => self.watch(v), _ => None }; - self.assign_to(target, value).map_err(|e| e.at(&crate::syntax::cover(target)))?; - self.report_assigned(&target[0].span, watch); - Ok(()) + /// A modified assignment, kept apart from `assigned` so that a plain assignment passes no function: `f` updates the target with `value` as its right argument, or with no value, as `a(f)←` does, sets `a` to `f a`. Gives the assignment's value, which is no result for an update. + pub(super) fn modified(&self, target: &Target, f: Function, value: Option) -> Result { + let Some(value) = value else { + // The update runs as the dyadic form with `f∘⊣`, which ignores its right argument of 0. + let f = atop(f, Function::primitive(Primitive::Identity(true)), &target.at)?; + self.modify(target, &target.to, f, &Binding::Value(Value::integer(0)))?; + return Ok(Binding::NoResult); + }; + self.assign(target, Some(f), &value)?; + Ok(value) } - /// The count of items that pending arrays have computed, taken before an assignment that would compute `value`, a pending array, where `fusion_report` would report it. - fn watch(&self, value: &Value) -> Option { (crate::explain::reporting() && value.pending().is_some()).then(crate::explain::computed_so_far) } - - /// Reports, as `←`, the items that pending arrays computed since `watch` counted them. - fn report_assigned(&self, span: &Span, watch: Option) { - let Some(before) = watch else { return }; - let items = crate::explain::computed_so_far() - before; - if items > 0 { fusion_report(span, "fusion ends", "←", items); } + /// Applies the compiled target, with a modified assignment's function where supplied. + pub(super) fn assign(&self, target: &Target, modifier: Option, value: &Binding) -> Result<(), Error> { + match modifier { Some(f) => self.modify(target, &target.to, f, value), None => self.assign_to(target, &target.to, value) }.map_err(|error| error.at(&target.span)) } - fn assign_to(&self, target: &[Node], value: &Binding) -> Result<(), Error> { - let span = &target[0].span; - if let [target] = target { - match &target.kind { - NodeKind::Name(name, _) => { return self.store(*name, value.clone(), span); } - // Assigning to an unknown system name gives the VALUE error that reading it gives, so `•io←0` tells an APL user that BPL has no `•io`. - NodeKind::System(name) => return Err(undefined(name, span)), - NodeKind::Output => { - return match value { - Binding::Value(a) => { - self.effect(crate::transform::Effect::Acts).at(span)?; - self.execution.output(crate::OutputKind::Explicit, self.display_settings().explicit(a, self.bindings.current().is_some())); - Ok(()) - } - _ => return Err(Error::domain("output requires a subject").at(&target.span)), - }; - } - NodeKind::Group(nodes) => { - if self.assignment_names(nodes) { return self.assign_to(nodes, value); } - return self.assign_selected(nodes, None, value); - } - NodeKind::ArrayLiteral { cells, record, .. } if self.assignment_names(std::slice::from_ref(target)) => { - let right = value.clone().into_value(span)?; - let pairs = Self::destructure(self.targets(cells, *record)?, &right, span)?; - for (cell, item) in pairs.into_iter().rev() { self.assign_to(cell, &Binding::from_element(item))?; } - return Ok(()); - } - _ => (), + + /// Shows `value` as explicit output (`⎕←`), laid out as output inside a function is where `function` holds. A replay of an output effect shows it so too (`program::Act::run`). + pub(crate) fn explicit_output(&self, value: &crate::Value, function: bool) { self.execution.output(crate::OutputKind::Explicit, self.display_settings().explicit(value, function)); } + + fn assign_to(&self, target: &Target, to: &To, value: &Binding) -> Result<(), Error> { + match to { + To::Name(name, span) => self.store(*name, value.clone(), span), + To::Output(span) => match value { + Binding::Value(a) => Operation::Effect(crate::primitive::Act::Output).call(&[a.clone()], 0).map(|_| ()).map_err(|error| error.at(span)), + _ => Err(Error::domain("output requires a subject").at(span)), + }, + To::Names(cells, span) => { + let right = value.clone().into_value(span)?; + for (cell, item) in Self::destructure(cells, &right, span)?.into_iter().rev() { self.assign_to(target, cell, &Binding::from_element(item))?; } + Ok(()) } - } - if let Some((name, derivative)) = self.rule_target(target) { return self.attach_rule(name, derivative, value, span); } - match self.modified_target(target)? { - Some((array, modifier)) => self.modify(array, modifier, value), - None => self.assign_selected(target, None, value), + To::Rule(name, derivative, span) => self.attach_rule(*name, *derivative, value, span), + To::Selected(layer) => self.selected(layer, &mut vec![None; target.slots], None, value), + To::Invalid(e) => Err(e.clone()), } } /// The name in a target that attaches a hand-written rule to the function it holds, with whether the rule is its derivative: `f∂` for the derivative, or `f⁻¹` for the inverse. - fn rule_target(&self, nodes: &[Node]) -> Option<(Name, bool)> { + pub(super) fn rule_target(&self, nodes: &[Node], log: &mut Facts) -> Option<(Name, bool)> { let [Node { kind: NodeKind::Name(name, _), .. }, Node { kind: NodeKind::Operator(op), .. }] = nodes else { return None }; - let derivative = match op { OperatorKind::Differentiate => true, OperatorKind::Super(Superscript::Power(-1)) => false, _ => return None }; - matches!(self.bindings.category(*name), Some(Category::Function)).then_some((*name, derivative)) + let derivative = match op { OperatorKind::Derivative => true, OperatorKind::Inverse => false, _ => return None }; + (self.path_category(*name, &[], log) == Category::Function).then_some((*name, derivative)) } /// `f⁻¹←g` or `f∂←g`: the nearest binding of `f` gets `g` attached as its inverse or derivative. fn attach_rule(&self, name: Name, derivative: bool, value: &Binding, span: &Span) -> Result<(), Error> { if !self.assignable(name) { return Err(Error::new(ErrorKind::Syntax, "arguments and operands cannot be assigned here").at(span)); } - let Binding::Function(g) = value else { return Err(Error::domain("an inverse or derivative must be a function").at(span)) }; - let Some((place, Binding::Function(f))) = self.bindings.lookup(name) else { unreachable!("`rule_target` checked the binding") }; - if g.environment() > place.frame() { return Err(Error::domain("function would export a local closure").at(span)); } - let f = if derivative { f.with_derivative(g.clone()) } else { f.with_inverse(g.clone()) }; - self.bindings.insert(place, Binding::Function(f)); - Ok(()) - } - - /// Splits a modified assignment's target into its array part and its function, as `a` and `f` in `a(f)←`. - fn modified_target<'a>(&self, target: &'a [Node]) -> Result, Error> { - let (operand, category) = self.trailing_operand(target, target.len(), &mut None)?; - if operand == 0 || !matches!(category, Category::Function) { return Ok(None); } - Ok(self.modifier(&target[operand..])?.map(|f| (&target[..operand], f))) - } - - /// Updates the array part of a modified assignment's target with `modifier`, taking `value` as its right argument. - fn modify(&self, array: &[Node], modifier: Function, value: &Binding) -> Result<(), Error> { - let span = &array[0].span; - if self.assignment_names(array) { return self.modify_names(array, &value.clone().into_value(span)?, &modifier); } - self.assign_selected(array, Some(modifier), value) - } - - /// `a(f)←` with no value sets `a` to `f a`, and gives whether `target` has that form. It runs as the dyadic form with `f∘⊣`, which ignores its right argument of 0. - pub(super) fn modify_monadic(&self, target: &[Node]) -> Result { - let Some((array, f)) = self.modified_target(target)? else { return Ok(false) }; - let f = atop(f, Function::primitive(Primitive::Identity(true)), &target[0].span)?; - self.modify(array, f, &Binding::Value(Value::integer(0)))?; - Ok(true) + let g = Function::from_value(value.clone(), span)?; + let (place, binding) = self.bindings.lookup(name).expect("rule_target checked the binding"); + let f = Function::from_value(binding, span)?; + let f = if derivative { f.with_derivative(g) } else { f.with_inverse(g) }; + self.store_at(place, f.into(), span) + } + + /// Updates a modified assignment's target `to` with `modifier`, taking `value` as its right argument: names update the arrays they hold, and a selection writes through `write_modified`. + fn modify(&self, target: &Target, to: &To, modifier: Function, value: &Binding) -> Result<(), Error> { + match to { + To::Selected(layer) => self.selected(layer, &mut vec![None; target.slots], Some(modifier), value), + To::Invalid(e) => Err(e.clone()), + names => self.modify_names(names, &value.clone().into_value(&target.at)?, &modifier), + } } - fn modify_names(&self, nodes: &[Node], right: &Value, modifier: &Function) -> Result<(), Error> { - let [node] = nodes else { unreachable!() }; - match &node.kind { - NodeKind::Group(inner) => self.modify_names(inner, right, modifier), - NodeKind::Name(name, _) => { - let binding = self.array_binding(*name, &node.span)?; - // Joining along the leading axis grows the array in place. - if let Some(p @ (Primitive::Ravel | Primitive::CatenateFirst)) = modifier.as_primitive() { - let first = matches!(p, Primitive::CatenateFirst); - if let Some((cells, names)) = crate::primitive::append_plan(&binding.value, right, first)? { - if cells.environment() > binding.place.frame() { return Err(Error::domain("array would export a local closure").at(&node.span)); } - return self.write_kept(binding, &node.span, |value| { - value.append(&cells, names.as_deref()).described("catenate has duplicate axis keys").at(&node.span) - }); - } - } - let updated = modifier.call_array(Some(&binding.value), right, &node.span)?; - self.update_array(&binding, updated, &node.span) - } - NodeKind::ArrayLiteral { cells, record, .. } => { - for (cell, item) in Self::destructure(self.targets(cells, *record)?, right, &node.span)? { self.modify_names(cell, &item, modifier)?; } - Ok(()) + fn modify_names(&self, to: &To, right: &Value, modifier: &Function) -> Result<(), Error> { + match to { + To::Name(name, span) => { + let binding = self.array_binding(*name, span)?; + self.update_array(&binding, modifier.call_array(Some(&binding.value), right, span)?, span) } - _ => unreachable!(), + To::Names(cells, span) => { for (cell, item) in Self::destructure(cells, right, span)? { self.modify_names(cell, &item, modifier)?; } Ok(()) } + To::Invalid(error) => Err(error.clone()), _ => unreachable!("a modified target is a name or a destructuring pattern"), } } /// The cells of a destructuring target, each with the nodes it assigns and, in a pattern, the key it reads. A pattern cell `key:target` reads the item with that key, spelled: a name stands for itself and a string literal for its text, never for a value. `key:` assigns the name `key`. - fn targets<'a>(&self, cells: &'a [Vec], record: bool) -> Result>, &'a [Node])>, Error> { + pub(super) fn targets<'a>(&self, cells: &'a [Vec], record: bool, log: &mut Facts) -> Result>, &'a [Node])>, Error> { if !record { return Ok(cells.iter().map(|cell| (None, &cell[..])).collect()); } cells.iter().map(|cell| { let colon = crate::syntax::key_colon(cell) @@ -275,249 +158,124 @@ impl Session { _ => return Err(key_error()), }; let target = if colon + 1 == cell.len() { &cell[..colon] } else { &cell[colon + 1..] }; - if !(matches!(target, [Node { kind: NodeKind::Name(_, _), .. }]) || self.assignment_names(target)) { + if !(matches!(target, [Node { kind: NodeKind::Name(_, _), .. }]) || self.assignment_names(target, log)) { return Err(Error::new(ErrorKind::Syntax, "a pattern assigns to a name or to brackets of names").at(&target[0].span)); } Ok((Some(key), target)) }).collect() } - /// The item of `right` that each target takes: by key in a pattern, and otherwise by position, whatever keys `right` has. One item fills every positional target. - fn destructure<'a>(targets: Vec<(Option>, &'a [Node])>, right: &Value, span: &Span) -> Result, Error> { - let parts = || -> Result, Error> { - if right.shape().len() == 1 { Ok(right.elements().collect()) } else { right.major_cells().described("array exceeds limits").at(span) } - }; - if targets.iter().all(|(key, _)| key.is_none()) { - if right.is_singleton() { return Ok(targets.into_iter().map(|(_, target)| (target, right.at(0))).collect()); } - let parts = parts()?; - if parts.len() != targets.len() { return Err(Error::new(Kind::Length, "destructuring needs one item for each name").at(span)); } - return Ok(targets.into_iter().zip(parts).map(|((_, target), part)| (target, part)).collect()); - } - let keys = right.keys(0).ok_or_else(|| Error::domain("a pattern reads items by key, and these items have none").at(span))?; - let parts = parts()?; - targets.into_iter().map(|(key, target)| { - let key = key.expect("every cell of a pattern has a key"); - let i = keys.position(&key).ok_or_else(|| Error::new(ErrorKind::Value, format!("no item has the key {key}")).at(&target[0].span))?; - Ok((target, parts[i].clone())) + /// The item of `right` that each target in `cells` takes: by key in a pattern, and otherwise by position, whatever keys `right` has. One item fills every positional target. A vector's part is its item, and any other array's part is a major cell (`Cells::get`). + fn destructure<'a>(cells: &'a [(Option>, To, Span)], right: &Value, span: &Span) -> Result, Error> { + let positional = cells.iter().all(|(key, ..)| key.is_none()); + if positional && right.is_singleton() { return Ok(cells.iter().map(|(_, target, _)| (target, right.at(0))).collect()); } + let keys = if positional { None } else { Some(right.keys(0).ok_or_else(|| Error::domain("a pattern reads items by key, and these items have none").at(span))?) }; + let items = right.cells(right.shape().len().saturating_sub(1)).described("array exceeds limits").at(span)?; + if positional && items.len() != cells.len() { return Err(Error::new(Kind::Length, "destructuring needs one item for each name").at(span)); } + cells.iter().enumerate().map(|(i, (key, target, at))| { + let i = match keys { + Some(keys) => { + let key = key.as_ref().expect("every cell of a pattern has a key"); + keys.position(key).ok_or_else(|| Error::new(ErrorKind::Value, format!("no item has the key {key}")).at(at))? + } + None => i, + }; + let part = if items.shape().is_empty() { Ok(right.element(i)) } else { items.get(i) }; + Ok((target, part.described("array exceeds limits").at(span)?)) }).collect() } - fn modifier(&self, nodes: &[Node]) -> Result, Error> { - if nodes.is_empty() { return Ok(None); } - match self.bind(nodes)?.value { - Binding::Function(f) => Ok(Some(f)), _ => Err(Error::new(ErrorKind::Syntax, "modified assignment needs a function").at(&nodes[0].span)) - } - } - - fn extend_selected(&self, nodes: &mut Vec, descend: bool) -> Result<(), Error> { - if let [Node { kind: NodeKind::Group(inner), .. }] = nodes.as_mut_slice() { return self.extend_selected(inner, descend); } - // A key before `⊃`, `⌷` or `.[]` may name an entry that the container lacks. It can be a list of keys. - let Some(i) = nodes.iter().position(|n| matches!(n.kind.primitive(), Some(Primitive::Mix | Primitive::Index | Primitive::Bracket))) else { - return Ok(()); - }; - if i == 0 || i + 1 == nodes.len() || !nodes[..i].iter().all(crate::syntax::key_node) { return Ok(()); } - let span = nodes[0].span.clone(); - let value = self.array_result(&nodes[..i])?; - // `⌷` and `.[]` select in one level. A path's items go down level by level. - let pick = nodes[i].kind.primitive() == Some(Primitive::Mix); + /// Builds missing keyed containers. Each index expression runs once. + fn extend(&self, layer: &Layer, run: &mut [Option], descend: bool) -> Result<(), Error> { + let Some(split) = layer.split.as_ref().filter(|split| split.keyed) else { return Ok(()) }; + let value = self.index(split, run)?; + let pick = split.p == Primitive::Mix; let items = if pick { crate::primitive::coordinate_fields(&value).collect() } else { vec![value.clone()] }; - let extend = |target: &mut Value, write| { - if pick { crate::primitive::extend_path(&items, target, descend, write) } - else { crate::primitive::extend_indices(&items[0], target, descend, write) } - }; - // Replace the keys with their value, so that they are evaluated once. - nodes.splice(..i, [Node { kind: NodeKind::Literal(value), span: span.clone() }]); if !items.iter().flat_map(crate::primitive::coordinate_fields).any(|s| matches!(crate::keyed::Selector::of(&s), Ok(Some(_)))) { return Ok(()); } - let mut container = nodes.split_off(2); - self.extend_selected(&mut container, true)?; - if let Some((binding, path)) = self.direct_item(&mut container)? { - let mut target = crate::primitive::item_at(&binding.value, &path, None); - let lacks = extend(&mut target, false)?; - // Holding the item would make the write copy it. - drop(target); - if lacks { self.write_kept(binding, &span, |value| extend(value.item_mut(&path, false), true).map(drop))?; } - } else { - let mut target = self.array_result(&container)?; - if extend(&mut target, true)? { self.assign_selected(&container, None, &Binding::Value(target))?; } - } - nodes.extend(container); + let container = &split.container; + self.extend(container, run, true)?; + let mut target = self.read(container, run)?; + let changed = if pick { crate::primitive::extend_path(&items, &mut target, descend, true)? } + else { crate::primitive::extend_indices(&items[0], &mut target, descend, true)? }; + if changed { self.selected(container, run, None, &Binding::Value(target))?; } Ok(()) } /// The `←` of an assignment on the path from a selection target to its root array, as in `(U←T).[k]←v`. The root is the last node at each level. - fn root_assignment(&self, nodes: &[Node]) -> Option { - let nodes = self.selection_members(nodes); + pub(super) fn root_assignment(nodes: &[Node]) -> Option { if let Some(node) = nodes.iter().find(|n| matches!(n.kind, NodeKind::Assign)) { return Some(node.span.clone()); } - match &nodes.last()?.kind { NodeKind::Group(inner) | NodeKind::Run(inner) => self.root_assignment(inner), _ => None } - } - - fn assign_selected(&self, nodes: &[Node], modifier: Option, value: &Binding) -> Result<(), Error> { - if let Some(span) = self.root_assignment(nodes) { return Err(Error::new(ErrorKind::Syntax, INVALID_SELECTION).at(&span)); } - let right = &value.clone().into_value(&nodes[0].span)?; - let span = crate::syntax::cover(nodes); - let mark = crate::explain::itemwise_mark(); - let mut nodes = self.selection_members(nodes).into_owned(); - if modifier.is_none() { self.extend_selected(&mut nodes, false)?; } - let (binding, selection, values) = match self.direct_target(&mut nodes)? { - Some((binding, selection)) => (binding, selection, right.clone()), - None => { - let level = self.begin(crate::transform::Kind::Label); - let selected = self.selection_expression(&nodes, level); - self.end(); - let (binding, mut labels, selected) = selected?; - let read = labels.read(&selected, level); - // The labelled value holds the array too, so dropping it lets the write change the array in place. - drop(selected); - let read = read.ok_or_else(|| Error::domain("function is not valid for selective assignment").at(&span))?; - let (selection, values) = labels.replacements(read, right, &span).at(&span)?; - (binding, selection, values) + match &nodes.last()?.kind { NodeKind::Group(inner) | NodeKind::Run(inner) => Self::root_assignment(inner), _ => None } + } + + /// Walks the selected graph back to the root and builds its At update. + fn selected(&self, layer: &Layer, run: &mut [Option], modifier: Option, value: &Binding) -> Result<(), Error> { + let right = value.clone().into_value(&layer.at)?; + if modifier.is_none() { self.extend(layer, run, false)?; } + let binding = self.root(layer)?; + let root = crate::selection::rooted(&binding.value)?; + let target = self.fold(layer, run, root.clone())?; + let walk = crate::selection::selecting(&target, &root)? + .ok_or_else(|| Error::domain("cannot assign to an item that isn't from the original").at(&layer.span))?; + let updated = match modifier { + None => walk.write(right, None)?, + Some(f) => match f.as_primitive() { + Some(primitive) => walk.write(right, Some(primitive))?, + None => { + let each = Operator::Kind(OperatorKind::Each).derive(Value::Function(f), None)?; + walk.write(each.call_array(Some(&target), &right, &layer.span)?, None)? + } } }; - let result = match modifier { - Some(f) => self.write_modified(binding, &selection, &f, &values, &span), None => self.write_selection(binding, &selection, &values, &span) - }; - if let Some(mark) = mark { crate::explain::claim_itemwise(mark, || span.text().into(), &span); } - result + self.update_array(&binding, updated, &layer.span) } - /// Writes `values` at `selection` into the array that `binding` kept, then stores it under the name. - fn write_selection(&self, binding: ArrayBinding, selection: &Selection, values: &Value, span: &Span) -> Result<(), Error> { - // The items already in the array passed this check when they were stored. - if values.environment() > binding.place.frame() { return Err(Error::domain("array would export a local closure").at(span)); } - self.write_kept(binding, span, |value| selection.write_into(value, values, span)) - } - /// Runs `write` on the array that `binding` kept, then stores it under the name. The name's current binding goes first, so the write happens in place unless something else holds the array. Running levels record the write first (`Session::record_write`), so a batched map stops before it. `write` makes every check before it changes anything, so an error or a stop leaves the name as it was. `fusion_report` reports a write that computes a pending array, as `←`. - fn write_kept(&self, binding: ArrayBinding, span: &Span, write: impl FnOnce(&mut Value) -> Result<(), Error>) -> Result<(), Error> { - let ArrayBinding { place, mut value } = binding; - if self.transforming() { self.record_write(place).at(span)?; } - let watch = self.watch(&value); - // The kept array can be written in place only when the name no longer holds it too. - let current = self.bindings.remove(place); - let same = matches!(¤t, Some(Binding::Value(v)) if v.storage_id() == value.storage_id()); - let current = current.filter(|_| !same); - if let Err(e) = write(&mut value) { - if let Some(restored) = if same { Some(Binding::Value(value)) } else { current } { self.bindings.insert(place, restored); } - return Err(e.at(span)); + /// The binding of the array that `layer` ends in, its root. + fn root(&self, layer: &Layer) -> Result { + match &layer.last { + Last::Name(name, span) => self.array_binding(*name, span), + Last::Container => self.root(&layer.split.as_ref().expect("a split holds the container").container), + Last::Layer(inner) => self.root(inner), + Last::Invalid(e) => Err(e.clone()), } - self.report_assigned(span, watch); - self.bindings.insert(place, Binding::from_element(value)); - Ok(()) } - /// A target whose text gives its positions: `I⌷`, `.[I]` or a path `L⊃` of a name or of such a target, as dot access writes them. It gives the binding and the selection without the labels that other targets need. Each index is evaluated once and its node becomes its value, so a target that needs labels after all evaluates nothing twice. - fn direct_target(&self, nodes: &mut [Node]) -> Result, Error> { - let span = nodes[0].span.clone(); - match nodes { - [Node { kind: NodeKind::Group(inner) | NodeKind::Run(inner), .. }] => self.direct_target(inner), - [left, Node { kind: NodeKind::Function(Builtin::Primitive(p @ (Primitive::Index | Primitive::Bracket | Primitive::Mix))), .. }, rest @ ..] - if self.holds_array(left, &mut None) => - { - let Some((binding, path)) = self.direct_item(rest)? else { return Ok(None) }; - let left = self.evaluate_once(left)?; - let item = crate::primitive::item_at(&binding.value, &path, None); - let context = &span; - let selection = match p { - Primitive::Mix => crate::primitive::pick_selection(&left, &item)?, - _ => crate::primitive::squad_selection(&left, &item, matches!(p, Primitive::Bracket), &context)?, - }; - Ok(Some((binding, selection.within(&path)?))) - } - _ => Ok(None), + /// What `layer`'s code gives from `root`, the root array's value: through a split that the run takes, where `p` with the index value applies to the container's value (`Split::unit`), and otherwise the code before its last node, run on what the last node gives. + fn fold(&self, layer: &Layer, run: &mut [Option], root: Value) -> Result { + if let Some(split) = layer.split.as_ref().filter(|split| split.unit && split.taken(run)) { + let value = self.fold(&split.container, run, root)?; + let index = self.index(split, run)?; + return Function::primitive(split.p).call_array(Some(&index), &value, &split.p_at); } - } - - /// The array that `nodes` names, or the one whole item that a direct target reaches: its binding and its path. - fn direct_item(&self, nodes: &mut [Node]) -> Result)>, Error> { - match nodes { - [Node { kind: NodeKind::Name(name, _), span }] => Ok(Some((self.array_binding(*name, span)?, vec![]))), - [Node { kind: NodeKind::Group(inner) | NodeKind::Run(inner), .. }] => self.direct_item(inner), - _ => Ok(self.direct_target(nodes)?.and_then(|(binding, selection)| match (selection.frame, selection.targets) { - (ResultFrame::Direct, Targets::Offsets(offsets)) => Some((binding, offsets)), - (ResultFrame::Direct, Targets::Paths(mut paths)) if paths.len() == 1 => Some((binding, paths.remove(0))), _ => None - })), - } - } - - /// The value of `node`, which then becomes a literal, so that nothing evaluates it again. - fn evaluate_once(&self, node: &mut Node) -> Result { - let value = self.array_result(std::slice::from_ref(node))?; - node.kind = NodeKind::Literal(value.clone()); - Ok(value) - } - - /// The target `nodes` evaluated on its root name's array, labelled at label level `level` (`selection::labelled`), with the binding the name kept and the labels that turn the result's labels back into places. A group or run that ends the target is the root of the nodes before it. - fn selection_expression(&self, nodes: &[Node], level: u64) -> Result<(ArrayBinding, crate::selection::Labels, Value), Error> { - let members = self.selection_members(nodes); - let nodes = &members[..]; - let root = nodes.len() - 1; - let span = &nodes[root].span; - let (binding, labels, selected) = match &nodes[root].kind { - NodeKind::Name(name, _) => { - let binding = self.array_binding(*name, span)?; - let (labels, selected) = crate::selection::labelled(&binding.value, level, span)?; - (binding, labels, selected) - } - NodeKind::Group(inner) | NodeKind::Run(inner) => self.selection_expression(inner, level)?, - _ if root > 0 && self.holds_array(&nodes[root - 1], &mut None) && self.holds_array(&nodes[root], &mut None) => { - let span = Span { source: span.source.clone(), range: nodes[root - 1].span.range.start..span.range.end }; - return Err(Error::new(ErrorKind::Syntax, STRAND_TARGET).at(&span)); - } - _ => return Err(Error::new(ErrorKind::Syntax, "selection must end in an array name").at(span)), + let value = match &layer.last { + Last::Name(..) => root, + Last::Container => self.fold(&layer.split.as_ref().expect("a split holds the container").container, run, root)?, + Last::Layer(inner) => self.fold(inner, run, root)?, + Last::Invalid(_) => unreachable!("`root` raises the error before the walk"), }; - if root == 0 { return Ok((binding, labels, selected)); } - let seed = Entity::new(Term::Binding(Binding::Value(selected)), narrow(&span.range), false, None); - let selected = Binder::evaluate_seeded(&nodes[..root], self, seed)?.value.into_value(span)?; - Ok((binding, labels, selected)) - } - - /// Writes the new value of each target of `selection` into `binding`. `f` takes the target's item on its left and its value from `right` on its right. A target that repeats a position reads the result of the earlier one, so repeats accumulate. A target at a fill reads the fill, and its write is dropped. An error leaves the array unchanged. A pervasive primitive combines at the targets' places, as accumulation does (`Primitive::at`): in place where it can, reading a walk's targets without listing them, and otherwise in one call, with repeated positions taking rounds. Any other `f` runs once for each target. `•explain` records each path as an accumulation. - fn write_modified(&self, binding: ArrayBinding, selection: &Selection, f: &Function, right: &Value, span: &Span) -> Result<(), Error> { - let right = selection.checked(right).at(span)?; - if let (ResultFrame::Array(_), Some(writes), Some(p)) = (&selection.frame, selection.targets.writes(), f.as_primitive()) { - if p.facts(true).pervasive() { return self.write_kept(binding, span, |z| p.at(z, writes, &right, span)); } - } - // Each cell's own targets in a batched run combine on the batch's whole. Any other function stops the run. Its cells then run one at a time. - if let Targets::Cells(cells) = &selection.targets { - let Some(p) = f.as_primitive().filter(|p| p.facts(true).pervasive()) else { return Err(cells.stop().into()) }; - return self.write_kept(binding, span, |z| { *z = cells.combined(z, &right, p, span)?; Ok(()) }); - } - // Each target's path is read below, and the modifier never runs for a target at a fill. - if let Some(listed) = selection.listed() { return self.write_modified(binding, &listed, f, &right, span); } - if let Some((kept, right)) = selection.without_fills(&right, span).at(span)? { return self.write_modified(binding, &kept, f, &right, span); } - let original = &binding.value; - let mut results: Vec = Vec::with_capacity(selection.targets.len()); - let mut latest: HashMap<&[usize], usize> = HashMap::new(); - for i in 0..selection.targets.len() { - let path = selection.targets.path(i); - let item = match latest.get(path) { Some(&j) => results[j].clone(), None => crate::primitive::item_at(original, path, None) }; - results.push(f.call_array(Some(&item), &selection.item(&right, i), span)?); - latest.insert(path, i); - } - crate::explain::accumulated(selection.targets.len(), original.len(), "item by item"); - crate::explain::itemwise(rustymath::events::Route::Accumulation, selection.targets.len()); - let values = match &selection.frame { - ResultFrame::Direct => results.pop().unwrap_or(right), - ResultFrame::Array(layout) if !results.is_empty() => { - Value::new(layout.shape().to_vec(), results).described("invalid modified selection").at(span)? - } - ResultFrame::Array(_) => right, - }; - self.write_selection(binding, selection, &values, span) + let Some(prefix) = &layer.prefix else { return Ok(value) }; + let seed = Entity::new(Term::Binding(Binding::Value(value)), narrow(&layer.last_at.range), false, None); + let Step::Done(bound) = prefix.as_ref().map_err(Clone::clone)?.run_seeded(self, seed)? else { unreachable!("a target's code isn't in tail position") }; + bound.value.into_value(&layer.last_at) + } + + /// What `layer` gives as an expression: its code run on its root array's value. + fn read(&self, layer: &Layer, run: &mut [Option]) -> Result { + let root = self.root(layer)?.value; + self.fold(layer, run, root) + } + + /// The value of `split`'s index, evaluated once in a run, one level deeper, and kept at its slot. + fn index(&self, split: &Split, run: &mut [Option]) -> Result { + if let Some(value) = &run[split.slot] { return Ok(value.clone()); } + let index = split.index.as_ref().map_err(Clone::clone)?; + self.descend(&split.at)?; + let ran = index.run(self, false); + self.ascend(); + let Step::Done(bound) = ran? else { unreachable!("an index isn't in tail position") }; + let value = bound.array(&split.at)?; + run[split.slot] = Some(value.clone()); + Ok(value) } -} -/// The name and keys of the dot path that the group `node` is, such as `m.a.f`, which dot access writes as `"f"⊃"a"⊃m`. `None` for any other group. -fn dot_path(node: &Node) -> Option<(Name, Vec>)> { - let (mut node, mut keys) = (node, Vec::new()); - while let NodeKind::Group(inner) = &node.kind { - let [Node { kind: NodeKind::Literal(key), .. }, Node { kind: NodeKind::Function(Builtin::Primitive(Primitive::Mix)), .. }, root] = &inner[..] else { - return None; - }; - keys.push(crate::keyed::name(key)?); - node = root; - } - let NodeKind::Name(name, _) = &node.kind else { return None }; - keys.reverse(); - Some((*name, keys)) } diff --git a/src/eval/binder.rs b/src/eval/binder.rs index 6fc3f5de..d12fa11c 100644 --- a/src/eval/binder.rs +++ b/src/eval/binder.rs @@ -1,72 +1,55 @@ //! The binder: the right-to-left reduction of a run's items by their categories, and the rewriting of dot access. -use super::tree::{Facts, Recorder, Reduction}; +use super::bindings::Address; +use super::tree::{Entity, Fact, Facts, Keyed, Kind, Last, Layer, Level, Mark, Mismatch, Op, Postfix, Read, StatementCode, Split, Target, Term, To, constant, derived, lettered, narrow, paired, piece, postfixed, span_of}; use super::*; impl Session { + /// Whether the dot path that starts `nodes`, such as `mul` or `ops.mul`, names a function or operator, read without evaluating anything (`path_category`). + fn path_function(&self, nodes: &[Node], log: &mut Facts) -> bool { + let Some(NodeKind::Name(name, _)) = nodes.first().map(|n| &n.kind) else { return false }; + let keys: Vec> = nodes[1..].chunks_exact(2).map_while(|pair| match pair { + [Node { kind: NodeKind::Operator(OperatorKind::Dot), .. }, Node { kind: NodeKind::Name(key, _), .. }] => Some(key.text().into()), _ => None + }).collect(); + self.path_category(*name, &keys, log) != Category::Value + } +} + +/// The groupings that the walk reads (`Compiler::level`): dot reads, joined items and runs that start with an operator. Each reads categories through the compiler that walks the nodes, so the groups match how they compile. +impl Compiler<'_> { fn has_members(nodes: &[Node]) -> bool { nodes.iter().any(|n| matches!(n.kind, NodeKind::Subscript(_) | NodeKind::Joiner(_))) || nodes.windows(2).any(|w| { - matches!(w[0].kind, NodeKind::Operator(OperatorKind::Product)) + matches!(w[0].kind, NodeKind::Operator(OperatorKind::Dot)) && matches!(w[1].kind, NodeKind::Name(_, _) | NodeKind::Group(_) | NodeKind::ArrayLiteral { .. }) }) } - /// Rewrite a writable target's dot reads and numeric subscripts to selection calls. - pub(super) fn selection_members<'a>(&self, nodes: &'a [Node]) -> Cow<'a, [Node]> { - let members = self.members_with(nodes, &mut None); - let Some(last) = members.last() else { return members }; - match &last.kind { - NodeKind::Group(inner) | NodeKind::Run(inner) => { - let inner = self.selection_members(inner); - if matches!(inner, Cow::Borrowed(_)) { return members; } - let kind = if matches!(last.kind, NodeKind::Run(_)) { NodeKind::Run(inner.into_owned()) } else { NodeKind::Group(inner.into_owned()) }; - let mut out = members.into_owned(); - out.last_mut().unwrap().kind = kind; - Cow::Owned(out) - } - NodeKind::Subscript(index) => { - let index = Node { kind: NodeKind::Literal(index.clone()), span: last.span.clone() }; - let Ok((start, _)) = self.trailing_operand(&members, members.len() - 1, &mut None) else { return members }; - let mut out = members.into_owned(); - let subscript = out.pop().unwrap(); - let inner = out.split_off(start); - let span = crate::syntax::cover(&inner); - let inner = self.selection_members(&inner).into_owned(); - let item = if inner.len() == 1 { inner.into_iter().next().unwrap() } else { Node { kind: NodeKind::Group(inner), span } }; - let span = Span { source: item.span.source.clone(), range: item.span.range.start..subscript.span.range.end }; - out.push(Node { kind: NodeKind::Group(vec![ - index, Node { kind: NodeKind::Function(Builtin::Primitive(Primitive::Index)), span: subscript.span }, item, - ]), span }); - Cow::Owned(out) - } - _ => members, - } - } - /// Rewrite dot reads, logging in `log` what names hold (`tree::Fact`). - pub(super) fn members_with<'a>(&self, nodes: &'a [Node], log: &mut Facts) -> Cow<'a, [Node]> { + /// `nodes` with their dot reads and their joined items grouped, as their categories decide (`category`). + fn members<'a>(&mut self, nodes: &'a [Node]) -> Cow<'a, [Node]> { if !Self::has_members(nodes) { return Cow::Borrowed(nodes); } let mut out: Vec = Vec::with_capacity(nodes.len()); // Whether the last node in `out` is the right operand of a `.` after a function. let (mut i, mut on_function) = (0, false); while i < nodes.len() { if (matches!(nodes[i].kind, NodeKind::Subscript(_)) && on_function - || matches!(nodes[i].kind, NodeKind::Operator(OperatorKind::Product)) && matches!(out.last().map(|n| &n.kind), Some(NodeKind::Subscript(_)))) - && let Ok((start, _)) = self.trailing_operand(&out, out.len(), log) { + || matches!(nodes[i].kind, NodeKind::Operator(OperatorKind::Dot)) && matches!(out.last().map(|n| &n.kind), Some(NodeKind::Subscript(_)))) + && let Some(start) = self.operand_start(&out, 0, out.len()) { let inner = out.split_off(start); let span = crate::syntax::cover(&inner); out.push(Node { kind: NodeKind::Group(inner), span }); on_function = false; } - if let (NodeKind::Operator(OperatorKind::Product), Some(next)) = (&nodes[i].kind, nodes.get(i + 1)) { + if let (NodeKind::Operator(OperatorKind::Dot), Some(next)) = (&nodes[i].kind, nodes.get(i + 1)) { let root = out.last().filter(|root| { !on_function && match &root.kind { - NodeKind::Name(_, _) | NodeKind::Group(_) => self.holds_array(root, log), + NodeKind::Name(_, _) | NodeKind::Group(_) => self.category(root) == Category::Value, NodeKind::Literal(_) | NodeKind::ArrayLiteral { .. } | NodeKind::Scope(..) => true, _ => false } }); // `.name` reads a field and `.[I]` selects, after an array or a function. After a function, a name that starts a dot path to a function is an inner product's operand instead, as in `+.ops.mul`. + let field = match (&next.kind, root.is_some()) { (NodeKind::Name(key, _), true) => Some(key.text()), _ => None }; let read = match &next.kind { - NodeKind::Name(name, _) if root.is_some() || !self.path_function(&nodes[i + 1..], log) => { + NodeKind::Name(name, _) if root.is_some() || !self.session.path_function(&nodes[i + 1..], &mut Some(&mut self.facts)) => { Some((Node { kind: NodeKind::Literal(crate::keyed::text(name.text())), span: next.span.clone() }, Primitive::Mix)) } NodeKind::ArrayLiteral { form: ListForm::Items | ListForm::Cells, .. } => Some((next.clone(), Primitive::Bracket)), @@ -84,13 +67,16 @@ impl Session { }; if let Some(kind) = kind { let root = out.pop().expect("a root was found"); - out.push(Node { kind, span: Span { source: next.span.source.clone(), range: root.span.range.start..next.span.range.end } }); + let range = root.span.range.start..next.span.range.end; + if let Some(key) = field { self.dot_path(&root, key, narrow(&range)); } + out.push(Node { kind, span: Span { source: next.span.source.clone(), range } }); i += 2; continue; } } else if let Some((key, _)) = read { - // After a function, the read stays the operand of `.`, which `Function::new` builds. A path goes on from there: `⊢.x.[0].a`. - out.extend([nodes[i].clone(), key]); + let kind = match &next.kind { NodeKind::Name(name, _) => OperatorKind::Field(*name), _ => OperatorKind::Index }; + out.push(Node { kind: NodeKind::Operator(kind), span: nodes[i].span.clone() }); + if matches!(next.kind, NodeKind::ArrayLiteral { .. }) { out.push(key); } (i, on_function) = (i + 2, true); continue; } @@ -106,13 +92,13 @@ impl Session { while i + 1 < out.len() { let NodeKind::Joiner(joiner) = out[i].kind else { i += 1; continue; }; let mut start = i - 1; - while start > 0 && matches!(out[start].kind, NodeKind::Operator(OperatorKind::Super(_)) | NodeKind::Subscript(_)) && touching(&out[start - 1], &out[start]) { + while start > 0 && matches!(out[start].kind, NodeKind::BoundOperator(OperatorKind::Power, _) | NodeKind::Operator(OperatorKind::Mark(_) | OperatorKind::Inverse) | NodeKind::Subscript(_) | NodeKind::Letter(_)) && touching(&out[start - 1], &out[start]) { start -= 1; } let left = if start + 1 == i { out[start].clone() } else { Node { kind: NodeKind::Group(out[start..i].to_vec()), span: cover(&out[start..i]) } }; if matches!(joiner, Joiner::Exponent) != exponent || !touching(&left, &out[i]) || !touching(&out[i], &out[i + 1]) - || !self.holds_array(&left, log) || !self.holds_array(&out[i + 1], log) { i += 1; continue; } + || self.category(&left) != Category::Value || self.category(&out[i + 1]) != Category::Value { i += 1; continue; } let joined = joined_term(left, joiner, &out[i].span, out[i + 1].clone()); out.splice(start..i + 2, [joined]); i = start + 1; @@ -122,317 +108,217 @@ impl Session { } /// Whether a run ends in a dyadic operator, which then takes the next run as its right operand. - pub(super) fn ends_in_dyadic_operator(&self, nodes: &[Node], log: &mut Facts) -> bool { - self.members_with(nodes, log).last().is_some_and(|n| self.static_category(n, log).valence() == Some(true)) - } + fn ends_in_dyadic_operator(&mut self, nodes: &[Node]) -> bool { self.members(nodes).last().is_some_and(|n| self.category(n).valence() == Some(true)) } /// Whether a run starts with an operator, which then takes the whole run before it as its left operand. - pub(super) fn starts_with_operator(&self, nodes: &[Node], log: &mut Facts) -> bool { - self.members_with(nodes, log).first().is_some_and(|n| self.static_category(n, log).valence().is_some()) - } + fn starts_with_operator(&mut self, nodes: &[Node]) -> bool { self.members(nodes).first().is_some_and(|n| self.category(n).valence().is_some()) } - /// Join each run that starts with an operator to the node before it, as the run's first token: `≢⊢ ⌸x` reads as `(≢⊢)⌸x`. A run before it joins as a group. A joined run that still starts with an operator joins again: `+ / ¨x` reads as `+/¨x`. No join reaches across `←`. `log` keeps what each test read about what names hold (`tree::Fact`). - fn joined<'a>(&self, nodes: &'a [Node], log: &mut Facts) -> Cow<'a, [Node]> { - let starts = |node: &Node, log: &mut Facts| matches!(&node.kind, NodeKind::Run(inner) if self.starts_with_operator(inner, log)); - // Every run but the first is tested, so that `log` keeps what each test read. - if !nodes.iter().skip(1).fold(false, |any, node| starts(node, log) || any) { return Cow::Borrowed(nodes); } + /// Join each run that starts with an operator to the node before it, as the run's first token: `≢⊢ ⌸x` reads as `(≢⊢)⌸x`. A run before it joins as a group. A joined run that still starts with an operator joins again: `+ / ¨x` reads as `+/¨x`. No join reaches across `←`. + fn joined<'a>(&mut self, nodes: &'a [Node]) -> Cow<'a, [Node]> { + // Every run but the first is tested, so that the facts keep what each test read. + if !nodes.iter().skip(1).fold(false, |any, node| self.starts(node) || any) { return Cow::Borrowed(nodes); } let mut out: Vec = Vec::with_capacity(nodes.len()); - let mut unlogged = None; for node in nodes { let mut node = node.clone(); - // The test above read each node as written. A run that takes in the node before it is a new run, whose test logs what it reads. - let mut merged = false; - while out.last().is_some_and(|before| !matches!(before.kind, NodeKind::Assign)) && starts(&node, if merged { &mut *log } else { &mut unlogged }) { + while out.last().is_some_and(|before| !matches!(before.kind, NodeKind::Assign)) && self.starts(&node) { let before = out.pop().expect("a node is before this run"); let span = Span { source: before.span.source.clone(), range: before.span.range.start..node.span.range.end }; let before = match before.kind { NodeKind::Run(run) => Node { kind: NodeKind::Group(run), span: before.span }, _ => before }; let NodeKind::Run(inner) = node.kind else { unreachable!() }; node = Node { kind: NodeKind::Run(std::iter::once(before).chain(inner).collect()), span }; - merged = true; } out.push(node); } - // A run that is the whole expression needs no wrapper, as in the parser. - if let [Node { kind: NodeKind::Run(inner), .. }] = out.as_mut_slice() { return Cow::Owned(std::mem::take(inner)); } - Cow::Owned(out) - } - /// Whether the dot path that starts `nodes`, such as `mul` or `ops.mul`, names a function or operator, read without evaluating anything (`path_category`). - fn path_function(&self, nodes: &[Node], log: &mut Facts) -> bool { - let Some(NodeKind::Name(name, _)) = nodes.first().map(|n| &n.kind) else { return false }; - let keys: Vec> = nodes[1..].chunks_exact(2).map_while(|pair| match pair { - [Node { kind: NodeKind::Operator(OperatorKind::Product), .. }, Node { kind: NodeKind::Name(key, _), .. }] => Some(key.text().into()), _ => None - }).collect(); - self.path_category(*name, &keys, log) != Category::Value - } -} - -// Only the binder has unfinished trains and bound left arguments. Names and groups contain completed values. `Callable` is an array that holds a function, which binds by the item to its right (`Category::before`). -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(super) enum Category { NoResult, Value, Function, Operator, DyadicOperator, Left, Train, Absent, Callable } -#[derive(Clone, Debug)] -pub(super) enum Term { - Binding(Binding), - /// Functions and arrays side by side before a function with no argument. - Train(Vec), - Left(Value, Function), - /// Arrays side by side, one item each. They become a vector when used. - Strand(Vec), - /// A group's train, held as its tines until something needs its function. A compiled call of the group calls the tines in place (`tree.rs`'s `Op::CallTines`). - Tines(Vec), - /// A keyed bracket item's entries, which only its literal reads, in a compiled statement. - Entries(Vec), -} -#[derive(Clone, Debug)] -pub(super) enum Tine { Array(Value), Function(Function) } -/// The last entity of a run, or the call it ends in when that call is in tail position. -enum Reduced { Entity(Entity), Tail(Application) } -impl Reduced { - /// The step that a run gives: its call in tail position, or its last entity's value. - fn step(self, source: &Arc) -> Result { - Ok(match self { Self::Tail(call) => Step::Tail(call), Self::Entity(entity) => Step::Done(entity.bound(source)?) }) + Cow::Owned(crate::syntax::unwrapped(out)) } -} -#[derive(Clone, Debug)] -pub(super) struct Entity { - pub(super) term: Term, - /// The `u32` offsets of the entity's text in the source of the binder's nodes. They keep `Entity` within 64 bytes, which the assertion below checks. A `Span` is rebuilt from them only where one is needed. - pub(super) range: std::ops::Range, - pub(super) shy: bool, - /// The entity's own category, from its term (`Category::bound`). - category: Category, - /// The category it binds with on the binder's stack: its `category`, with a function array resolved against the entity below it (`Binder::push`). - role: Category, - pub(super) expression: Option>, -} -/// The binder's state where a recording's check failed (`tree.rs`): its stack, its pending stack and its cursor, with the entity it had just resolved, whether it had just pushed a call's result, and the `←` of an assignment it was making. -pub(super) struct Resume { - pub(super) stack: Vec, pub(super) pending: Vec, pub(super) cursor: usize, pub(super) next: Option, pub(super) after_call: bool, - pub(super) assignment: Option -} -const _: () = assert!(std::mem::size_of::() <= 64); -/// `range` as `u32` offsets, clamped for a source longer than 4 GiB. -pub(super) fn narrow(range: &std::ops::Range) -> std::ops::Range { - let at = |i: usize| u32::try_from(i).unwrap_or(u32::MAX); - at(range.start)..at(range.end) -} -/// The span of `range` in `source`. -pub(super) fn span_of(source: &Arc, range: &std::ops::Range) -> Span { - Span { source: source.clone(), range: range.start as usize..range.end as usize } -} + /// Whether `node` is a run that starts with an operator, which `joined` joins to the node before it. + fn starts(&mut self, node: &Node) -> bool { matches!(&node.kind, NodeKind::Run(inner) if self.starts_with_operator(inner)) } -impl Entity { - /// An entity with `term`, not yet on the binder's stack. - // Out of line, `new` built each entity and then copied it out, about 265 instructions for each call of `{x←⍵+1 ⋄ x}`. - #[inline(always)] - pub(super) fn new(term: Term, range: std::ops::Range, shy: bool, expression: Option>) -> Self { - let category = match term { - Term::Binding(ref value) => Category::bound(value), Term::Strand(_) | Term::Entries(_) => Category::Value, Term::Train(_) => Category::Train, - Term::Left(..) => Category::Left, Term::Tines(_) => Category::Function + /// Keeps the dot path of the field read at `range`, which reads `key` from `root`, where `root` is a name or a field read of one, so that `group` binds the read with the field's role (`Session::path_role`). + fn dot_path(&mut self, root: &Node, key: &str, range: std::ops::Range) { + let path = match &root.kind { + NodeKind::Name(name, _) => Some((*name, Vec::new())), + _ => { let at = narrow(&root.span.range); self.fields.iter().find(|(r, ..)| *r == at).map(|(_, name, keys)| (*name, keys.clone())) } }; - Self { term, range, shy, category, role: category, expression } - } - /// An entity that holds `value`, with no captured expression. - #[inline(always)] - pub(super) fn of(value: Binding, range: std::ops::Range, shy: bool) -> Self { Self::new(Term::Binding(value), range, shy, None) } - /// The entity's own category. - #[inline(always)] - pub(super) fn category(&self) -> Category { self.category } - /// The entity's value: its binding, or what its unfinished term builds (`Entity::built`). - #[inline(always)] - pub(super) fn value(self, source: &Arc) -> Result { - match self.term { Term::Binding(value) => Ok(value), term => (Self { term, ..self }).built(source) } - } - /// What an unfinished term builds: a train's function, a bound left argument's function or a strand's vector. - // Out of line, so that the conversions that every compiled operand goes through stay small enough to inline. - #[inline(never)] - fn built(self, source: &Arc) -> Result { - let span = || span_of(source, &self.range); - Ok(match self.term { - Term::Binding(value) => value, Term::Train(tines) | Term::Tines(tines) => Binding::Function(self::Function::train(tines, &span())?), - Term::Left(a, f) => Binding::Function(before(a, f, &span())?), - Term::Strand(items) => Binding::Value(Value::new(vec![items.len()], items).map_err(|k| Error::new(k, "invalid strand").at(&span()))?), - Term::Entries(_) => unreachable!("a keyed item's entries go only into its literal") - }) - } - #[inline(always)] - pub(super) fn function(self, source: &Arc) -> Result { - let range = self.range.clone(); - match self.value(source)? { Binding::Function(f) => Ok(f), value => Function::from_value(value, &span_of(source, &range)) } + if let Some((name, mut keys)) = path && !self.fields.iter().any(|(at, ..)| *at == range) { keys.push(key.into()); self.fields.push((range, name, keys)); } } - #[inline(always)] - pub(super) fn array(self, source: &Arc) -> Result { match self.value(source)? { Binding::Value(a) => Ok(a), _ => unreachable!() } } - /// The entity's value, with whether it displays and the expression it captured. - fn bound(mut self, source: &Arc) -> Result { - let (shy, expression) = (self.shy, self.expression.take()); - Ok(Bound { value: self.value(source)?, shy, expression }) + + /// Where the operand that ends at `end` in `nodes` starts, no further left than `begin` (`trailing`), compiling nothing. + fn operand_start(&mut self, nodes: &[Node], begin: usize, end: usize) -> Option { + let mark = self.mark(); + let start = self.trailing(nodes, begin, end).ok().map(|(start, _)| start); + self.truncate(mark); + start } -} -/// `left` and `right` side by side as a strand. A strand adds its items, and anything else adds itself as one array. Gives the items, how many of them came from `left`, and the strand's text range, which is `left`'s. -#[inline] -pub(super) fn strand(left: Entity, right: Entity, source: &Arc) -> Result<(Vec, usize, std::ops::Range), Error> { - let range = left.range.clone(); - let mut joined = match left.term { - Term::Strand(items) => items, - term => { - let mut items = Vec::with_capacity(2); - items.push(Entity { term, ..left }.array(source)?); - items + /// Fails where `node` is a run whose value is an operator, as `operator` says, unless the run is one parenthesised group. A run reduces to one value: an array, a function, or an operator that a dot path such as `m.op` names. + fn one_value(&mut self, node: &Node, operator: bool) -> Result<(), Error> { + let NodeKind::Run(nodes) = &node.kind else { return Ok(()) }; + if operator && !matches!(&self.members(nodes)[..], [Node { kind: NodeKind::Group(_), .. }]) { + return Err(Error::new(ErrorKind::Syntax, "a run must reduce to one value: an array, a function or an operator glyph").at(&node.span)); } - }; - let from_left = joined.len(); - match right.term { Term::Strand(items) => joined.extend(items), term => joined.push(Entity { term, ..right }.array(source)?) } - Ok((joined, from_left, range)) -} - -/// `entity` as an argument: its array, which stays borrowed where a borrowed entity holds one, as a recording's constant does (`tree.rs`). -#[inline(always)] -pub(super) fn array_of<'p>(entity: Cow<'p, Entity>, source: &Arc) -> Result, Error> { - match entity { - Cow::Borrowed(Entity { term: Term::Binding(Binding::Value(v)), .. }) => Ok(Cow::Borrowed(v)), - entity => entity.into_owned().array(source).map(Cow::Owned), + Ok(()) } -} -/// `entity` as a called function, which stays borrowed where a borrowed entity holds one. -#[inline(always)] -pub(super) fn function_of<'p>(entity: Cow<'p, Entity>, source: &Arc) -> Result, Error> { - match entity { - Cow::Borrowed(Entity { term: Term::Binding(Binding::Function(f)), .. }) => Ok(Cow::Borrowed(f)), - entity => entity.into_owned().function(source).map(Cow::Owned), + /// `nodes`, a target of selective assignment, with its dot reads and numeric subscripts made selection calls (`members`), in the group or run that ends it too: `v₁` reads as `1⌷v`. + fn selection<'a>(&mut self, nodes: &'a [Node]) -> Cow<'a, [Node]> { + let members = self.members(nodes); + let Some(last) = members.last() else { return members }; + match &last.kind { + NodeKind::Group(inner) | NodeKind::Run(inner) => { + let inner = self.selection(inner); + if matches!(inner, Cow::Borrowed(_)) { return members; } + let kind = if matches!(last.kind, NodeKind::Run(_)) { NodeKind::Run(inner.into_owned()) } else { NodeKind::Group(inner.into_owned()) }; + let mut out = members.into_owned(); + out.last_mut().unwrap().kind = kind; + Cow::Owned(out) + } + NodeKind::Subscript(index) => { + let index = Node { kind: NodeKind::Literal(index.clone()), span: last.span.clone() }; + let Some(start) = self.operand_start(&members, 0, members.len() - 1) else { return members }; + let mut out = members.into_owned(); + let subscript = out.pop().unwrap(); + let inner = out.split_off(start); + let span = crate::syntax::cover(&inner); + let inner = self.selection(&inner).into_owned(); + let item = if inner.len() == 1 { inner.into_iter().next().unwrap() } else { Node { kind: NodeKind::Group(inner), span } }; + let span = Span { source: item.span.source.clone(), range: item.span.range.start..subscript.span.range.end }; + out.push(Node { kind: NodeKind::Group(vec![ + index, Node { kind: NodeKind::Function(Builtin::Primitive(Primitive::Index)), span: subscript.span }, item, + ]), span }); + Cow::Owned(out) + } + _ => members, + } } -} - -/// `left` bound as `right`'s left argument: `left` as an argument, then `right` as a function. The pair takes `right`'s text range, because a call belongs to its function's text. -#[inline] -pub(super) fn attached<'p>( - left: Cow<'p, Entity>, right: Cow<'p, Entity>, source: &Arc) -> Result<(Cow<'p, Value>, Cow<'p, Function>, std::ops::Range), Error> { - let range = right.range.clone(); - let x = array_of(left, source)?; - Ok((x, function_of(right, source)?, range)) -} -/// A call's left argument and function, from the entity before its right argument: a bound left argument's parts, or no left argument and the entity as a function. -#[inline] -pub(super) fn split_call<'p>(left: Cow<'p, Entity>, source: &Arc) -> Result<(Option>, Cow<'p, Function>), Error> { - Ok(match left { - Cow::Owned(Entity { term: Term::Left(x, f), .. }) => (Some(Cow::Owned(x)), Cow::Owned(f)), - Cow::Borrowed(Entity { term: Term::Left(x, f), .. }) => (Some(Cow::Borrowed(x)), Cow::Borrowed(f)), left => (None, function_of(left, source)?) - }) -} + /// The target `nodes` as its assignment runs it (`Target`), whose last `count` nodes are a modified assignment's function: the nodes before them, with dot reads and numeric subscripts made selection calls (`selection`), compiled as what they assign to. A plain assignment's target is read by `plain`; a modified one's is names where `assignment_names` admits them and otherwise a selection. Its readings add facts to the statement's, but no assigned names. + fn assigned_target(&mut self, nodes: &[Node], count: usize) -> Target { + let assigned = self.assigned.len(); + let selection = self.selection(&nodes[..nodes.len() - count]); + let mut slots = 0; + let to = if count == 0 { self.plain(&selection, &mut slots) } + else if self.session.assignment_names(&selection, &mut Some(&mut self.facts)) { self.names(&selection) } + else { self.selected(&selection, &mut slots) }; + self.assigned.truncate(assigned); + Target { to, slots, span: crate::syntax::cover(nodes), at: nodes[0].span.clone() } + } -/// What the operator `operator` gives on the operand `operand` (`Rule::Derive`): a derived function, a scalar that holds the function for `ᵘ`, or the call that a power, `ᵀ` or a number marker on an array stands for: of `*`, `⍉`, `⌊ₓ` or `•float`. -pub(super) enum Derived { Term(Binding), Call(Function, Option, Value) } - -/// `operand` given to `operator` at `span`, as `Derived` describes. -pub(super) fn derived(operand: Binding, operator: Binding, span: &Span) -> Result { - let operand = Operand::from_value(operand); - let Binding::Operator(operator) = operator else { unreachable!("a derive's right item is an operator") }; - let function = |p| self::Function::primitive(p); - if let (self::OperatorNode::Subscript(index), Operand::Value(a)) = (&operator, &operand) { - return Ok(Derived::Call(function(Primitive::Index), Some(index.clone()), a.clone())); - } - if let self::OperatorNode::Primitive(OperatorKind::Super(s)) = &operator { - match (*s, operand) { - // `ᵘ` makes a scalar that holds a function. `⊂` encloses a subject. - (Superscript::Unit, Operand::Function(f)) => { - return Ok(Derived::Term(Binding::Value(crate::Value::Function(f).enclose().described("invalid scalar").at(span)?))); - } - (Superscript::Unit, _) => return Err(Error::domain("ᵘ makes a scalar that holds a function. Enclose a subject with ⊂").at(span)), - (Superscript::Power(n), Operand::Value(a)) => { - return Ok(Derived::Call(function(Primitive::Math(crate::number::Math::Power)), Some(a), crate::Value::weak_integer(n))); - } - (Superscript::Transpose, Operand::Value(a)) => return Ok(Derived::Call(function(Primitive::Transpose), None, a)), - (Superscript::Mark(crate::number::Mark::Exact), Operand::Value(a)) => { - return Ok(Derived::Call(function(Primitive::Exact(crate::primitive::Exact::Floor)), None, a)); - } - (Superscript::Mark(crate::number::Mark::Fixed(w)), Operand::Value(a)) => { - return Ok(Derived::Term(Binding::Value(crate::derivative::converted(&a, rustymath::buffer::DType::Integer(w), span).at(span)?))); - } - (Superscript::Mark(crate::number::Mark::Width(w)), Operand::Value(a)) => { - let float = crate::primitive::Builtin::find("•float").expect("•float is a system function").function(); - return Ok(Derived::Call(float, Some(crate::Value::integer(w.bits().into())), a)); - } - (_, operand) => return Ok(Derived::Term(operator.derive(operand, span)?)), + /// What a plain assignment's target `nodes` assigns to: a name, whatever it holds, `⎕`, an unknown system name's error, names (`names`), a function's rule (`Session::rule_target`), or a selection (`selected`). A group holds names or a selection. + fn plain(&mut self, nodes: &[Node], slots: &mut usize) -> To { + let session = self.session; + match nodes { + [Node { kind: NodeKind::Name(name, _), span }] => To::Name(*name, span.clone()), + [Node { kind: NodeKind::System(name), span }] => To::Invalid(undefined(name, span)), + [Node { kind: NodeKind::Output, span }] => To::Output(span.clone()), + _ if session.assignment_names(nodes, &mut Some(&mut self.facts)) => self.names(nodes), + [Node { kind: NodeKind::Group(inner), .. }] => self.selected(inner, slots), + _ => match session.rule_target(nodes, &mut Some(&mut self.facts)) { + Some((name, derivative)) => To::Rule(name, derivative, nodes[0].span.clone()), + None => self.selected(nodes, slots), + }, } } - Ok(Derived::Term(operator.derive(operand, span)?)) -} -/// The dyadic operator `operator` with its right operand `operand` bound (`Rule::BindRight`). -pub(super) fn bound_right(operator: Binding, operand: Binding) -> self::OperatorNode { - let Binding::Operator(operator) = operator else { unreachable!("a bind's left item is a dyadic operator") }; - self::OperatorNode::Bound(Arc::new(operator), Operand::from_value(operand)) -} + /// The names that the target `nodes` assigns, which `assignment_names` admits: a name, or brackets of names whose cells each have the key that a pattern reads (`Session::targets`). A pattern whose cells don't fit gives its error. + fn names(&mut self, nodes: &[Node]) -> To { + match nodes { + [Node { kind: NodeKind::Name(name, _), span }] => To::Name(*name, span.clone()), + [Node { kind: NodeKind::Group(inner), .. }] => self.names(inner), + [Node { kind: NodeKind::ArrayLiteral { cells, record, .. }, span }] => match self.session.targets(cells, *record, &mut Some(&mut self.facts)) { + Ok(cells) => To::Names(cells.into_iter().map(|(key, cell)| (key, self.names(cell), cell[0].span.clone())).collect(), span.clone()), + Err(e) => To::Invalid(e), + }, + _ => unreachable!("`assignment_names` admits names, groups and brackets"), + } + } -/// The tines of `left` and then of `right`, which join in a train (`Rule::Train`). The tines go into a train's own list where either side brings one, and otherwise into a new list with room for `TINES`, so a statement that builds a train at each call allocates once for it. -#[inline] -pub(super) fn train(left: Cow<'_, Entity>, right: Cow<'_, Entity>, source: &Arc) -> Result, Error> { - Ok(match (left, right) { - (Cow::Owned(Entity { term: Term::Train(mut tines), .. }), right) => { push_tines(right, &mut tines, source)?; tines } - (left, Cow::Owned(Entity { term: Term::Train(mut tines), .. })) => { - let n = tines.len(); - push_tines(left, &mut tines, source)?; - let pushed = tines.len() - n; - tines.rotate_right(pushed); - tines + /// The selection target `nodes`, as layers (`layer`), or the error of one that assigns on its way to its root array. + fn selected(&mut self, nodes: &[Node], slots: &mut usize) -> To { + match Session::root_assignment(nodes) { + Some(span) => To::Invalid(Error::new(ErrorKind::Syntax, INVALID_SELECTION).at(&span)), + None => To::Selected(self.layer(nodes, slots)), } - (left, right) => { - let mut tines = Vec::with_capacity(TINES); - push_tines(left, &mut tines, source)?; - push_tines(right, &mut tines, source)?; - tines + } + + /// The layer of a selection target that `nodes` hold, inside the groups and runs that hold them alone (`tree::Layer`). The split is at the first selecting primitive where the nodes before it can all be keys, and the extension of missing keys reaches the level through groups but not through a run (`Split::keyed`); otherwise it's after the first node where that's an array and a selecting primitive and more nodes follow (`Split::simple`). A strand of two arrays, or any last node but a name, a group or a run, can't end a selection. + fn layer(&mut self, nodes: &[Node], slots: &mut usize) -> Layer { + let (span, at) = (crate::syntax::cover(nodes), nodes[0].span.clone()); + let (mut nodes, mut keys) = (nodes, true); + loop { + match nodes { + [Node { kind: NodeKind::Group(inner), .. }] => nodes = inner, + [Node { kind: NodeKind::Run(inner), .. }] => (nodes, keys) = (inner, false), + _ => break, + } } - }) -} + let selects = |node: &Node| matches!(node.kind.primitive(), Some(Primitive::Index | Primitive::Bracket | Primitive::Mix)); + let first = nodes.iter().position(selects); + let keyed = keys && first.is_some_and(|i| i > 0 && i + 1 < nodes.len() && nodes[..i].iter().all(crate::syntax::key_node)); + let simple = nodes.len() > 2 && selects(&nodes[1]) && self.is_array(&nodes[0]); + let at_split = if keyed { first } else { simple.then_some(1) }; + let last_at = nodes.len() - 1; + let split = at_split.map(|i| { + let index = self.part(&nodes[..i], None); + let slot = *slots; + *slots += 1; + let unit = self.category(&nodes[i + 1]).valence().is_none(); + let container = Box::new(self.layer(&nodes[i + 1..], slots)); + let p = nodes[i].kind.primitive().expect("a selecting primitive"); + Split { p, index, slot, keyed, simple, unit, container, at: nodes[0].span.clone(), p_at: nodes[i].span.clone() } + }); + let prefix = (last_at > 0 && !split.as_ref().is_some_and(|split| split.simple && split.unit)).then(|| self.part(&nodes[..last_at], Some(Category::Value))); + let node = &nodes[last_at]; + let last = match &node.kind { + NodeKind::Name(name, _) => Last::Name(*name, node.span.clone()), + NodeKind::Group(_) | NodeKind::Run(_) if at_split == Some(last_at - 1) => Last::Container, + NodeKind::Group(_) | NodeKind::Run(_) => Last::Layer(Box::new(self.layer(std::slice::from_ref(node), slots))), + _ if last_at > 0 && self.is_array(&nodes[last_at - 1]) && self.is_array(node) => { + let span = Span { source: node.span.source.clone(), range: nodes[last_at - 1].span.range.start..node.span.range.end }; + Last::Invalid(Error::new(ErrorKind::Syntax, STRAND_TARGET).at(&span)) + } + _ => Last::Invalid(Error::new(ErrorKind::Syntax, "selection must end in an array name").at(&node.span)), + }; + Layer { split, prefix, last, last_at: node.span.clone(), span, at } + } -/// The tines in a train's new list that a statement such as `(g ⍵-1)+g ⍵-2` fills without growing it. -const TINES: usize = 4; + /// Whether this compiler reads `node` as an array: a literal, or a node whose category is an array's. + fn is_array(&mut self, node: &Node) -> bool { matches!(node.kind, NodeKind::Literal(_)) || self.category(node) == Category::Value } -/// Appends `entity`'s parts to `tines` as tines of a train: a train's tines, a bound left argument's array and function, or the entity as one array or function. Where `entity` is borrowed, as a recording's constant is (`tree.rs`), each tine copies only its own array or function. -#[inline] -fn push_tines(entity: Cow<'_, Entity>, tines: &mut Vec, source: &Arc) -> Result<(), Error> { - match entity { - Cow::Owned(Entity { term: Term::Train(owned), .. }) => tines.extend(owned), - Cow::Borrowed(Entity { term: Term::Train(borrowed), .. }) => tines.extend_from_slice(borrowed), - Cow::Owned(Entity { term: Term::Left(a, f), .. }) => tines.extend([Tine::Array(a), Tine::Function(f)]), - Cow::Borrowed(Entity { term: Term::Left(a, f), .. }) => tines.extend([Tine::Array(a.clone()), Tine::Function(f.clone())]), - entity if matches!(entity.category, Category::Value | Category::Callable) => tines.push(Tine::Array(array_of(entity, source)?.into_owned())), - entity => tines.push(Tine::Function(function_of(entity, source)?.into_owned())), - } - Ok(()) + /// `nodes`, a part of an assignment's target, compiled as a statement of its own, with an item of category `seed` to their right where one is given, for the assignment to run, or the error that compiling it gives, which the assignment raises where it runs the part. Its facts join this statement's, which checks them as it starts. + fn part(&mut self, nodes: &[Node], seed: Option) -> Result { + let mut compiler = Compiler::new(self.session); + let seed = seed.map(|category| Item::new(Read::Register(compiler.register()), category, 0..0)); + let result = compiler.level(nodes, 0, seed)?; + let mut code = compiler.finish(nodes, result); + for fact in std::mem::take(&mut code.facts) { note(&mut self.facts, fact); } + Ok(code) + } } -/// Whether a call of `function` can return to the caller's loop as a tail call: the user defined it. -#[inline] -pub(super) fn tails(function: &Function) -> bool { matches!(function.node(), FunctionNode::Defined(_) | FunctionNode::Derived(..)) } +// Only the binder has unfinished trains and bound left arguments. Names and groups contain completed values. `Callable` is an array that holds a function, which binds by the item to its right (`Category::before`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum Category { NoResult, Value, Function, Operator, DyadicOperator, Left, Train, Absent, Callable } impl Category { - /// The category of the field that `keys` reach from the array that a name's binding `found` holds, or of `found` itself where it holds no array, as `named` counts it. A field that the array doesn't have counts as an array. - pub(super) fn field(found: Option, keys: &[Arc]) -> Self { - match found { - Some(Binding::Value(a)) => { - keys.iter().try_fold(a, |a, key| crate::keyed::field(&a, key)).map_or(Self::Value, |item| Self::of(&Binding::from_element(item))) - } - found => Self::named(found.as_ref().map(Self::of)), - } - } + /// The category of what an item of this category gives once it's built (`Entity::value`): a train or a bound left argument gives a function. + pub(super) fn settled(self) -> Self { match self { Self::Train | Self::Left => Self::Function, category => category } } pub(super) fn of(value: &Binding) -> Self { match value { Binding::NoResult => Self::NoResult, Binding::Absent => Self::Absent, Binding::Value(_) | Binding::Released => Self::Value, Binding::Function(_) => Self::Function, Binding::Operator(op) => Self::of_operator(op) } } - pub(super) fn of_operator(op: &self::OperatorNode) -> Self { if op.is_dyadic() { Self::DyadicOperator } else { Self::Operator } } - /// A name's category from its binding's, as `Bindings::category` gives it: an unbound name counts as an array, and so does an absent `⍺`. - pub(super) fn named(found: Option) -> Self { match found { None | Some(Self::Absent) => Self::Value, Some(category) => category } } + pub(super) fn of_operator(op: &OperatorValue) -> Self { + if matches!(op, OperatorValue::Operator(op) if op.dyadic()) { Self::DyadicOperator } else { Self::Operator } + } /// Whether this is an operator's category, as `Some` of whether the operator is dyadic. pub(super) fn valence(self) -> Option { match self { Self::DyadicOperator => Some(true), Self::Operator => Some(false), _ => None } } /// The category of an array: `Callable` where it holds a function as an item, and otherwise `Value`. #[inline] pub(super) fn array(a: &Value) -> Self { if a.holds_functions() { Self::Callable } else { Self::Value } } - /// The category that the binder and its recordings give `value`. Unlike `of`, it tells an array that holds a function from other arrays (`array`). + /// The category that the binder gives `value`. Unlike `of`, it tells an array that holds a function from other arrays (`array`). #[inline] pub(super) fn bound(value: &Binding) -> Self { match value { Binding::Value(a) => Self::array(a), value => Self::of(value) } } /// How an item of this category binds before the item to its right, which binds as `right`, or with nothing to its right where `right` is `None`. An array that holds a function binds as a function before a subject or an absent `⍺`, and as an array otherwise. Every other category binds as itself. @@ -474,341 +360,607 @@ impl Rule { } } -pub(super) struct Binder { stack: Vec } -impl Binder { - pub(super) fn evaluate(nodes: &[Node], session: &Session, tail: bool) -> Result { - Self::reduced(nodes, session, tail, None, None, None)?.step(&nodes[0].span.source) - } - /// `nodes` evaluated with `seed` already on the binder's stack, as the entity to the right of them all. - pub(super) fn evaluate_seeded(nodes: &[Node], session: &Session, seed: Entity) -> Result { Self::seeded_with(nodes, session, seed, None) } - /// `evaluate_seeded`, with `rec` recording the run's reductions. `rec` records the run as a level whose stack starts with the seed (`Recorder::seeded_starts`). - pub(super) fn seeded_with(nodes: &[Node], session: &Session, seed: Entity, rec: Option<&mut Recorder>) -> Result { - let Step::Done(bound) = Self::reduced(nodes, session, false, Some(seed), rec, None)?.step(&nodes[0].span.source)? else { - unreachable!("a run with no tail call gives its value") - }; - Ok(bound) - } - /// `nodes` evaluated as `evaluate` evaluates them, with the recording of the binder's steps when they stay within the subset that recordings cover (`tree.rs`). - pub(super) fn recorded(nodes: &[Node], session: &Session, tail: bool) -> Result<(Step, Option), Error> { - let mut recorder = Recorder::default(); - let reduced = Self::reduced(nodes, session, tail, None, Some(&mut recorder), None)?; - Ok((reduced.step(&nodes[0].span.source)?, recorder.finish())) - } - /// `nodes` evaluated from `resume`, the state where a recording's check failed. - pub(super) fn resumed(nodes: &[Node], session: &Session, tail: bool, resume: Resume) -> Result { - Self::reduced(nodes, session, tail, None, None, Some(resume))?.step(&nodes[0].span.source) - } - /// The run or parenthesised group `node`, as the run around it uses it. A run reduces to one value: an array, a function, or an operator that a dot path such as `m.op` names. - pub(super) fn nested(node: &Node, session: &Session) -> Result { Self::nested_with(node, session, None) } - /// `nested`, with `rec` recording the group's reductions. - fn nested_with(node: &Node, session: &Session, rec: Option<&mut Recorder>) -> Result { - let (NodeKind::Group(nodes) | NodeKind::Run(nodes)) = &node.kind else { unreachable!() }; - let Reduced::Entity(entity) = session.nested(nodes, |session| Self::reduced(nodes, session, false, None, rec, None))? else { unreachable!() }; - Self::group_bound(node, session, entity.bound(&nodes[0].span.source)?) - } - /// `bound`, the value of the group or run `node`, once checked: a run reduces to one value. - pub(super) fn group_bound(node: &Node, session: &Session, bound: Bound) -> Result { - let (NodeKind::Group(nodes) | NodeKind::Run(nodes)) = &node.kind else { unreachable!() }; - if matches!(node.kind, NodeKind::Run(_)) && matches!(bound.value, Binding::Operator(_)) - && !matches!(&session.members_with(nodes, &mut None)[..], [Node { kind: NodeKind::Group(_), .. }]) { - return Err(Error::new(ErrorKind::Syntax, "a run must reduce to one value: an array, a function or an operator glyph").at(&node.span)); +/// Whether `value` is a postfix operator, which binds an array before its neighbours use it, and stands for a call after one (`tree::Postfix::of`): a digit's or a mark's operator kind, or a bound `⍠`, as a subscript is. +#[inline] +pub(super) fn postfix(value: &Binding) -> bool { + matches!(value, Binding::Operator(op) if Postfix::of(op).is_some()) +} + +/// The role that the compiler binds `value` with: the category that the binder gives it (`Category::bound`), and whether it's a postfix operator (`postfix`). +#[inline] +pub(super) fn role(value: &Binding) -> (Category, bool) { (Category::bound(value), postfix(value)) } + +/// The role of a name that nothing binds: an array's. A name that is unbound when its statement starts binds as an array, whatever a write later in the statement gives it, so `x+⍎"x←3"` gives 6. +pub(super) const UNBOUND: (Category, bool) = (Category::Value, false); + +/// The role (`role`) of what a name holds, `found`, or `UNBOUND` where nothing binds it. +pub(super) fn role_of(found: Option<&Binding>) -> (Category, bool) { found.map_or(UNBOUND, role) } + +/// The role (`role`) of the field that `keys` reach from the array that a name's binding `found` holds, or of `found` where `keys` is empty or `found` holds no array. An unbound name, and a field that the array doesn't have, bind as an array. So does a field of an array whose items are numbers or characters at every depth (`numeric`), which isn't read: reading an item of a pending array would compute it, and a trace's input would stop the trace. +pub(super) fn field_role(found: Option, keys: &[Arc]) -> (Category, bool) { + match found { + Some(Binding::Value(a)) => keys.iter().try_fold(a, |a, key| (!numeric(&a)).then(|| crate::keyed::field(&a, key)).flatten()) + .map_or(UNBOUND, |item| role(&Binding::from_element(item))), + found => role_of(found.as_ref()), + } +} + +fn numeric(a: &Value) -> bool { a.kind().is_some() } + +/// An item on the compiler's stacks: where its instructions read it, its own category and the category it binds with (`Category::before`), its text, whether it's a postfix operator (`postfix`), and whether a call or a derive gave it (`call`). A function that a call gives binds as a value in its statement, but an assignment stores it as the function, and an operator other than a subscript or a superscript takes it as its function operand. +#[derive(Clone, Debug)] +struct Item { read: Read, category: Category, role: Category, range: std::ops::Range, postfix: bool, call: bool, literal: bool } + +impl Item { + fn new(read: Read, category: Category, range: std::ops::Range) -> Self { Self { read, category, role: category, range, postfix: false, call: false, literal: false } } +} + +/// Pushes `item`, with the category it binds with against the item below it (`Category::before`). +fn push(stack: &mut Vec, mut item: Item) { + item.role = item.category.before(stack.last().map(|below| below.role)); + stack.push(item); +} + +/// Whether `left` goes onto `stack` now, rather than after the top of `stack` reduces. +fn shifts(left: &Item, stack: &[Item]) -> bool { + let [.., below, top] = stack else { return true }; + match Rule::get(top.role, below.role) { + // An operator still awaiting its operand cannot reduce with its right neighbour. + Rule::Wait(_) => true, + // Postfix operators bind an array before its neighbours use it as an argument or operand. + _ if left.postfix => true, + _ if top.role == Category::Value && below.postfix => false, + old => match Rule::get(left.category.before(Some(top.role)), top.role) { + // Adjacent arrays group from the right: `v w i` is `v (w i)`. + Rule::Adjacent if matches!(old, Rule::Adjacent) => false, + new => new.strength() >= old.strength(), + }, + } +} + +/// Adds `fact` to `facts` unless they hold it already. +fn note(facts: &mut Vec, fact: Fact) { if !facts.contains(&fact) { facts.push(fact) } } + +/// The compiler of a statement: the binder's grammar applied to items that instructions compute as the statement runs, so that how a statement binds depends only on its text and on what the names it reads hold when it starts. A call's result is a value to the binder, whatever it holds. It keeps the instructions, constants and levels so far, the registers in use and free, every level's facts about what names hold, which the statement checks as it starts, each name that an assignment earlier in the statement binds, with the category of what it assigned, and the dot path of each field read that `members` formed, by its text (`dot_path`). +pub(super) struct Compiler<'s> { + session: &'s Session, ops: Vec, constants: Vec, levels: Vec>, registers: u32, free: Vec, + facts: Vec, assigned: Vec<(Name, Category)>, fields: Vec<(std::ops::Range, Name, Vec>)>, +} + +impl<'s> Compiler<'s> { + /// `nodes` compiled as a statement, with an item of category `seed` to the right of them all where one is given, which `StatementCode::run` takes in register 0. + pub(super) fn compile(nodes: &[Node], session: &Session, seed: Option) -> Result { + let mut compiler = Compiler::new(session); + let seed = seed.map(|category| Item::new(Read::Register(compiler.register()), category, 0..0)); + let result = compiler.level(nodes, 0, seed)?; + Ok(compiler.finish(nodes, result)) + } + + /// The statement `nodes` as this compiler compiled it, whose value `result` holds. + fn finish(self, nodes: &[Node], result: Item) -> StatementCode { + let levels = self.levels.into_iter().map(|level| level.expect("a level that the compiler closed")).collect(); + StatementCode { ops: self.ops, constants: self.constants, registers: self.registers as usize, result: result.read, call: result.call, facts: self.facts, span: nodes[0].span.clone(), levels } + } + + /// A compiler with nothing compiled. + fn new(session: &'s Session) -> Self { + Compiler { + session, ops: Vec::new(), constants: Vec::new(), levels: Vec::new(), registers: 0, free: Vec::new(), facts: Vec::new(), assigned: Vec::new(), + fields: Vec::new() + } + } + + /// Where the operand that ends at `end` starts, no further left than `begin`, with its item, read right to left as `level` reads it. An operand is what the strongest rules join, as an operator joins its operand and a dyadic operator its right operand. An operator waits for its operand, so whatever is to its left joins it until one arrives. + fn trailing(&mut self, nodes: &[Node], begin: usize, end: usize) -> Result<(usize, Item), Error> { + if end == begin { return Err(Error::new(ErrorKind::Syntax, "missing assignment operand").at(&nodes[end.min(nodes.len() - 1)].span)); } + let source = &nodes[0].span.source; + let strong = |left: Category, right: Category| matches!(Rule::get(left, right), Rule::Derive | Rule::BindRight); + let (mut stack, mut start) = (Vec::new(), end); + while start > begin { + let mark = self.mark(); + let item = self.leaf(&nodes[start - 1], &mut stack)?; + if let Some(top) = stack.last() && top.category != Category::Operator && !strong(item.category.before(Some(top.role)), top.role) { + self.truncate(mark); + break; } - Ok(bound) - } - /// The bracket literal `node`, at `cursor` among the nodes being walked, evaluated as `Session::resolve` evaluates it, each item as a statement of its own, while `rc` records each of its pieces as a level (`Recorder::piece`). - fn literal(node: &Node, cursor: usize, session: &Session, rc: &mut Recorder) -> Result { - rc.literal_starts(cursor); - let value = session.bracket(node, |nodes, cell, at, within| { - rc.piece_starts(cell, at); - let Step::Done(bound) = - session.nested(nodes, |session| Self::reduced(nodes, session, false, None, Some(&mut *rc), None)?.step(&nodes[0].span.source))? - else { unreachable!("an item isn't in tail position") }; - let span = &nodes[0].span; - let value = match within { super::session::Within::Key => Some(bound.array(span)?), _ => super::session::listed(bound.value, span)? }; - rc.piece(within, value.clone(), narrow(&crate::syntax::cover(nodes).range)); - Ok(value) - })?; - rc.literal(node, &value, session)?; - Ok(Bound::new(value)) - } - /// Pushes `entity`, working out the category it binds with against the entity below it (`Category::before`). Reductions take the top of the stack, so that entity stays below it while it's there. - #[inline] - fn push(&mut self, mut entity: Entity) { - entity.role = entity.category.before(self.stack.last().map(|below| below.role)); - self.stack.push(entity); - } - /// A binder holding the `stack` that a recording resumes it with, with each entity's role worked out as `push` works it out. - fn restacked(stack: Vec) -> Self { - let mut binder = Self { stack: Vec::with_capacity(stack.len()) }; - for entity in stack { binder.push(entity); } - binder - } - /// Whether `left` goes onto `stack` now, rather than after the top of `stack` reduces. - fn shifts(left: &Entity, stack: &[Entity]) -> bool { - let [.., below, top] = stack else { return true }; - match Rule::get(top.role, below.role) { - // An operator still awaiting its operand cannot reduce with its right neighbour. - Rule::Wait(_) => true, - // Postfix operators bind an array before its neighbours use it as an argument or operand. - _ if matches!(&left.term, Term::Binding(Binding::Operator(self::OperatorNode::Primitive(OperatorKind::Super(_)) | self::OperatorNode::Subscript(_)))) => true, - _ if top.role == Category::Value && matches!(&below.term, Term::Binding(Binding::Operator( - self::OperatorNode::Primitive(OperatorKind::Super(_)) | self::OperatorNode::Subscript(_)))) => false, - old => match Rule::get(left.category.before(Some(top.role)), top.role) { - // Adjacent arrays group from the right: `v w i` is `v (w i)`. - Rule::Adjacent if matches!(old, Rule::Adjacent) => false, - new => new.strength() >= old.strength(), - }, + push(&mut stack, item); + start -= 1; + while let [.., right, left] = &stack[..] && strong(left.role, right.role) { self.reduce(&mut stack, false, source)?; } } + Ok((start, stack.pop().expect("an operand ends at `end`"))) } - fn reduced( - nodes: &[Node], session: &Session, tail: bool, seed: Option, mut rec: Option<&mut Recorder>, resume: Option) -> Result { - // A recording resumes the binder on the rewritten nodes it walked, whose positions its cursor counts. - let rewriting = resume.is_none(); - let joined = if rewriting { session.joined(nodes, &mut rec.as_deref_mut().map(Recorder::facts)) } else { Cow::Borrowed(nodes) }; - let members = if rewriting { session.members_with(&joined, &mut rec.as_deref_mut().map(Recorder::facts)) } else { Cow::Borrowed(&joined[..]) }; - // Fast combinations replace their spellings before the binder walks the nodes (`rewrite::expression`). - let combined = if rewriting { super::rewrite::expression(session, &members, &mut rec.as_deref_mut().map(Recorder::facts)) } else { Cow::Borrowed(&members[..]) }; - let nodes = &combined[..]; + + /// Compiles `nodes` as level `level`, with `seed` on the stack. Gives the item that holds the level's value. + fn level(&mut self, nodes: &[Node], level: u32, seed: Option) -> Result { + // Joined runs, dot reads and joined items group the nodes before the walk reads them. The groupings read categories through this compiler, which gives the categories the walk will compile with. Each instruction holds the nodes it reads, so nothing keeps the groups. + let joined = self.joined(nodes); + let members = self.members(&joined); + let nodes = &members[..]; let source = &nodes[0].span.source; - if matches!(joined, Cow::Owned(_)) || matches!(members, Cow::Owned(_)) || matches!(combined, Cow::Owned(_)) { log(&mut rec, |rc| rc.rewritten(nodes.to_vec())); } - let (mut binder, mut pending, mut cursor, mut held, mut after_call, mut assignment) = match resume { - Some(r) => (Self::restacked(r.stack), r.pending, r.cursor, r.next, r.after_call, r.assignment), + let (mut stack, mut pending) = (Vec::new(), Vec::new()); + let (mut cursor, mut assignment) = (nodes.len(), None); + if let Some(seed) = seed { push(&mut stack, seed); } + loop { + let next = if pending.is_empty() && assignment.is_none() && cursor > 0 { + cursor -= 1; + if matches!(nodes[cursor].kind, NodeKind::Assign) { assignment = Some(cursor); None } else { Some(self.leaf(&nodes[cursor], &mut stack)?) } + } else { None }; + if let Some(left) = pending.last() { + if shifts(left, &stack) { + let left = pending.pop().expect("a pending item"); + push(&mut stack, left); + continue; + } + } else if let Some(left) = next { + if shifts(&left, &stack) { push(&mut stack, left); continue; } + pending.push(left); + } else if stack.len() < 2 { + let Some(i) = assignment.take() else { break }; + let (item, start) = self.assignment(nodes, i, stack.pop())?; + cursor = start; + pending.push(item); + continue; + } + loop { + // The statement's last reduction, made on its own level with nothing left to resolve or on either stack and no assignment in progress. + let last = level == 0 && assignment.is_none() && cursor == 0 && pending.is_empty() && stack.len() == 2; + self.reduce(&mut stack, last, source)?; + let m = stack.len(); + // Bound operators still need their left operand before rebinding on the right. + if matches!(stack[m - 1].category, Category::Operator) || shifts(&stack[m - 1], &stack[..m - 1]) { break; } + pending.push(stack.pop().expect("a reduced item")); + } + } + Ok(stack.pop().expect("nonempty expression")) + } + + /// The item for `node`, which builds by a constructor (`tree::constant`) or compiles to instructions: nothing evaluates here. A run that ends in a dyadic operator takes its right operand from the top of `stack`. + fn leaf(&mut self, node: &Node, stack: &mut Vec) -> Result { + let range = narrow(&node.span.range); + Ok(match &node.kind { + NodeKind::Run(inner) if self.ends_in_dyadic_operator(inner) => { + // A run that ends in a dyadic operator takes the next run as its right operand, because each run acts as one token. + let Some(right) = stack.pop() else { + return Err(Error::new(ErrorKind::Syntax, "an operator at the end of a run needs a right operand after it").at(&node.span)); + }; + self.group(node, inner, Some(right))? + } + NodeKind::Group(inner) | NodeKind::Run(inner) => self.group(node, inner, None)?, + NodeKind::ArrayLiteral { .. } => self.literal(node)?, + NodeKind::Name(name, take) => self.name(*name, *take, range), + NodeKind::Subscript(index) => self.postfix_item(Postfix::Index(index.clone()), range), + NodeKind::Letter(letter) => self.postfix_item(lettered(*letter), range), + NodeKind::BoundOperator(OperatorKind::Power, count) => self.postfix_item(Postfix::Power(count.as_number().expect("a superscript count").integer().expect("an integer superscript")), range), + NodeKind::Operator(OperatorKind::Inverse) => self.postfix_item(Postfix::Power(-1), range), + NodeKind::Operator(OperatorKind::Mark(mark)) => self.postfix_item(Postfix::Mark(*mark), range), + NodeKind::Number { text, mark } => { + let to = self.register(); + self.ops.push(Op::Number { text: *text, mark: *mark, empty: false, to, range: range.clone() }); + Item { literal: true, ..Item::new(Read::Register(to), Category::Value, range) } + } + NodeKind::Zilde(mark) => { + let to = self.register(); + self.ops.push(Op::Number { text: Some("0"), mark: *mark, empty: true, to, range: range.clone() }); + Item::new(Read::Register(to), Category::Value, range) + } + NodeKind::Literal(_) | NodeKind::Function(_) | NodeKind::Operator(_) | NodeKind::BoundOperator(..) => self.constant(Term::Binding(constant(node)), range), + // A dfn becomes a function or an operator as it's read, which captures the frame that reads it. + NodeKind::Dfn(definition) => { + let category = match definition.kind { + DefinitionKind::Function => Category::Function, DefinitionKind::MonadicOperator => Category::Operator, + DefinitionKind::DyadicOperator => Category::DyadicOperator, + }; + let to = self.register(); + self.ops.push(Op::Closure { definition: definition.clone(), to, range: range.clone(), check: category }); + Item::new(Read::Register(to), category, range) + } + NodeKind::Scope(root, body) => self.scope(root, body, range)?, + NodeKind::Output => { + let to = self.register(); + self.ops.push(Op::Input { to, range: range.clone() }); + Item::new(Read::Register(to), Category::Value, range) + } + // An unknown system name fails as it's read, where a known one would be a function. + NodeKind::System(name) => self.fail(undefined(name, &node.span), Category::Function, range), + NodeKind::Joiner(_) => self.fail(Error::new(ErrorKind::Syntax, "ⱼ, ₑ and ᵣ join the two arrays they touch, as in xⱼy or 2ₑn").at(&node.span), Category::Value, range), + _ => self.fail(Error::new(ErrorKind::Syntax, "unexpected assignment symbol").at(&node.span), Category::Value, range), + }) + } + + /// An item for a postfix: a constant that binds as a monadic operator and compiles by its operand's role (`derive`). + fn postfix_item(&mut self, postfix: Postfix, range: std::ops::Range) -> Item { Item { postfix: true, ..self.constant(Term::Postfix(postfix), range) } } + + /// An item whose instruction raises `error` where the statement reaches it (`Op::Fail`), binding as `category` until then. + fn fail(&mut self, error: Error, category: Category, range: std::ops::Range) -> Item { + let to = self.register(); + self.ops.push(Op::Fail { error }); + Item::new(Read::Register(to), category, range) + } + + /// The scope `T.(expr)` or `T.[rows]`: `root` compiled as an item, then the body, which compiles as the statement enters it (`Op::Scope`). What it gives is a value to the binder, as a call's result is. + fn scope(&mut self, root: &Node, body: &Node, range: std::ops::Range) -> Result { + let root = self.leaf(root, &mut Vec::new())?; + let body: Arc<[Node]> = match &body.kind { NodeKind::Group(inner) => inner.clone().into(), _ => vec![body.clone()].into() }; + self.release(root.read); + let to = self.register(); + self.ops.push(Op::Scope { root: root.read, body, to, range: range.clone() }); + Ok(Item::new(Read::Register(to), Category::Value, range)) + } + + /// A read of `name`, which takes the binding out of its frame where `take` marks the dfn's last read of it. A name that an assignment earlier in the statement binds binds as what it assigned. Any other binds with the role of what it holds now (`Session::path_role`), which the statement's facts keep, and its read fails if that role changed while the statement ran (`role_changed`); an unbound name binds as an array. An absent `⍺` drops out, so it compiles to a constant. + fn name(&mut self, name: Name, take: bool, range: std::ops::Range) -> Item { + let session = self.session; + let (category, postfix, on) = match self.assigned.iter().rev().find(|(assigned, _)| *assigned == name) { + Some(&(_, category)) => (category, false, Mismatch::Fit), None => { - let mut stacks = session.stacks.borrow_mut(); - (Self { stack: stacks.pop().unwrap_or_default() }, stacks.pop().unwrap_or_default(), nodes.len(), None, false, None) + let (category, postfix) = session.path_role(name, &[], &mut Some(&mut self.facts)); + if category == Category::Absent { return self.constant(Term::Binding(Binding::Absent), range); } + (category, postfix, Mismatch::Fail) } }; - if let Some(seed) = seed { binder.push(seed); } - loop { - if !after_call { - crate::execution::check().at(&nodes[0].span)?; - let next = if let Some(entity) = held.take() { Some(entity) } - else if pending.is_empty() && assignment.is_none() && cursor > 0 { - cursor -= 1; - let i = cursor; - let node = &nodes[i]; - if matches!(node.kind, NodeKind::Assign) { - assignment = Some(i); - log(&mut rec, |rc| rc.assigning(i)); - None - } else { - let (term, expression) = if matches!(&node.kind, NodeKind::Run(inner) if session.ends_in_dyadic_operator(inner, &mut rec.as_deref_mut().map(Recorder::facts))) - { - // A run that ends in a dyadic operator takes the next run as its right operand, because each run acts as one token. - let NodeKind::Run(inner) = &node.kind else { unreachable!() }; - let Some(right) = binder.stack.pop() else { - return Err(Error::new(ErrorKind::Syntax, "an operator at the end of a run needs a right operand after it").at(&node.span)); - }; - // The run counts toward the depth limit, as a group does. - let bound = match rec.as_deref_mut() { - Some(rc) => { - rc.seeded_starts(i); - let bound = session.nested(inner, |session| Self::seeded_with(inner, session, right, Some(&mut *rc)))?; - rc.group(node, &bound); - bound - } - None => session.nested(inner, |session| Self::evaluate_seeded(inner, session, right))?, - }; - (Term::Binding(bound.value), bound.expression) - } else if matches!(node.kind, NodeKind::Group(_) | NodeKind::Run(_)) { - let bound = match rec.as_deref_mut() { - Some(rc) => { - rc.group_starts(i); - let bound = Self::nested_with(node, session, Some(&mut *rc))?; - rc.group(node, &bound); - bound - } - None => Self::nested(node, session)?, - }; - (Term::Binding(bound.value), bound.expression) - } else if let (NodeKind::ArrayLiteral { .. }, Some(rc)) = (&node.kind, rec.as_deref_mut()) { - let bound = Self::literal(node, i, session, rc)?; - (Term::Binding(bound.value), bound.expression) - } else { - let bound = session.resolve(node)?; - log(&mut rec, |rc| rc.leaf(node, i, &bound.value, session)); - (Term::Binding(bound.value), bound.expression) - }; - Some(Entity::new(term, narrow(&node.span.range), false, expression)) - } - } else { None }; - if let Some(left) = pending.last() { - if Self::shifts(left, &binder.stack) { - binder.push(pending.pop().unwrap()); - log(&mut rec, |rc| rc.unpend()); - continue; - } - } else if let Some(left) = next { - if Self::shifts(&left, &binder.stack) { - binder.push(left); - log(&mut rec, |rc| rc.next(true)); - continue; + let to = self.register(); + self.ops.push(Op::Load { name, address: session.bindings.address(name), take, to, range: range.clone(), check: category, on }); + Item { postfix, ..Item::new(Read::Register(to), category, range) } + } + + /// The item for the group or run `node`, whose nodes are `inner`, compiled as a level of its own, with `seed` on its stack where it's a run that ends in a dyadic operator. A group with a constant value compiles to that constant. A field read that `members` formed binds with the field's role (`dot_path`, `Session::path_role`), which the statement's facts keep. Any other group binds as its value does, where a train or a bound left argument becomes a function. + fn group(&mut self, node: &Node, inner: &[Node], seed: Option) -> Result { + let session = self.session; + let range = narrow(&node.span.range); + let start = self.mark(); + self.levels.push(None); + let id = self.levels.len() as u32; + self.ops.push(Op::Enter { level: id }); + let value = session.nested(inner, |_| self.level(inner, id, seed))?; + self.one_value(node, value.category.valence().is_some())?; + let path = self.fields.iter().find(|(at, ..)| *at == range).map(|(_, name, keys)| (*name, keys.clone())); + let (category, postfix) = match path { + Some((name, keys)) => session.path_role(name, &keys, &mut Some(&mut self.facts)), + None => (value.category.settled(), value.postfix), + }; + if let Read::Constant(c) = value.read && self.ops.len() == start.ops + 1 { + let constant = self.constants[c as usize].clone().value(&node.span.source)?; + self.truncate(start); + return Ok(Item { postfix, ..self.constant(Term::Binding(constant), range) }); + } + self.ops.push(Op::Exit { level: id, from: value.read }); + self.release(value.read); + let to = self.register(); + self.levels[id as usize - 1] = Some(Level { to, range: range.clone(), span: inner[0].span.clone(), check: category, kind: Kind::Group }); + Ok(Item { postfix, call: value.call, literal: value.literal, ..Item::new(Read::Register(to), category, range) }) + } + + /// The item for the bracket literal `node`. Its items, and its keys that aren't constant, are levels of their own (`Compiler::item`), which run left to right with an item's value before its key, and the literal's instruction assembles what they give (`tree::assembled`). The level that finishes a keyed item makes its entries before the next item runs (`tree::Keyed`). An absent `⍺` drops out, and so does a keyed item whose value is absent, without its key being evaluated. A key with no value is a name, which gives both: `[a:]` is `["a":a]`. A literal whose pieces are all constant compiles to its value. A literal binds as an array that holds a function where an item binds as a function. + fn literal(&mut self, node: &Node) -> Result { + let NodeKind::ArrayLiteral { cells, record, .. } = &node.kind else { unreachable!("a bracket literal") }; + let source = &node.span.source; + let range = narrow(&node.span.range); + let (start, shared) = (self.mark(), std::cell::OnceCell::new()); + let (mut pieces, mut functions) = (Vec::with_capacity(cells.len()), false); + for (k, nodes) in cells.iter().enumerate() { + let Some(colon) = record.then(|| crate::syntax::key_colon(nodes)).flatten() else { + let (item, _) = self.item(node, &shared, k, 0..nodes.len())?; + functions |= item.category.settled() == Category::Function; + if item.category != Category::Absent { pieces.push(item.read) } + continue; + }; + // The value and the key are items of their own, value first. As one expression, `key:f` would be a train. + let named = match &nodes[..colon] { + _ if colon + 1 < nodes.len() => None, + [Node { kind: NodeKind::Name(name, _), .. }] => Some(crate::keyed::text(name.text())), + _ => return Err(Error::new(ErrorKind::Syntax, "a key with no value is a name, as in [a:]").at(&nodes[colon].span)), + }; + let at = if named.is_some() { 0..colon } else { colon + 1..nodes.len() }; + let text = narrow(&crate::syntax::cover(&nodes[at.clone()]).range); + let (value, held) = self.item(node, &shared, k, at)?; + functions |= value.category.settled() == Category::Function; + // An absent value leaves its item out, key and all, and its key isn't evaluated. + if value.category == Category::Absent { self.release(value.read); continue } + let key = match named { + Some(key) => key, + None => match self.item(node, &shared, k, 0..colon)? { + (Item { read: Read::Constant(c), .. }, _) => { + self.constants[c as usize].clone().value(source)?.into_value(&crate::syntax::cover(&nodes[..colon]))? } - pending.push(left); - log(&mut rec, |rc| rc.next(false)); - } else if binder.stack.len() < 2 { - if let Some(i) = assignment.take() { - let begin = nodes[..i].iter().rposition(|n| matches!(n.kind, NodeKind::Assign)).map_or(0, |j| j + 1); - if begin == i { return Err(Error::new(ErrorKind::Syntax, NO_TARGET).at(&nodes[i].span)); } - let range = narrow(&nodes[i].span.range); - // With nothing after `←`, `a(f)←` sets `a` to `f a` and gives no result. - let value = match binder.stack.pop() { - None => None, - Some(entity) => { - let value = entity.value(source)?; - if matches!(value, Binding::NoResult) { - return Err(Error::new(ErrorKind::Value, "assignment requires a value").at(&nodes[i].span)); - } - if matches!(value, Binding::Absent) { return Err(Error::new(ErrorKind::Value, ABSENT).at(&nodes[i].span)); } - Some(value) - } - }; - let monadic = value.is_none(); - cursor = begin + session.assignment_start(&nodes[begin..i])?; - let value = session.assigned(&nodes[cursor..i], value, None, &nodes[i].span)?; - log(&mut rec, |rc| { - let local = match &nodes[cursor..i] { - [Node { kind: NodeKind::Name(name, _), .. }] if !monadic => match session.bindings.here(*name) { - Place::Slot(_, slot) => Some(slot), _ => None - }, - _ => None, - }; - rc.assigned(i - begin, cursor, monadic, local, range.clone()) - }); - pending.push(Entity::new(Term::Binding(value), range, true, None)); + // A key that isn't constant makes the item's entries as its own level ends, from the value that evaluated before it. + (key, Some(keyed)) => { + let Some(Level { kind: Kind::Item { key: slot, .. }, .. }) = &mut self.levels[keyed as usize - 1] else { unreachable!("an item's level") }; + *slot = Some(Keyed::Value { value: value.read, colon: colon as u32 }); + self.release(value.read); + pieces.push(key.read); continue; } - break; + (_, None) => unreachable!("a computed item has a level"), + }, + }; + match (value.read, held) { + (Read::Constant(c), _) => { + let value = self.constants[c as usize].clone().value(source)?; + pieces.push(self.constant(piece(self.session, node, k, value, Some((&key, colon)), &node.span)?, text).read); } - } - loop { - let call = if std::mem::take(&mut after_call) { None } else { binder.reduce(session.capture.get(), source, rec.as_deref_mut())? }; - if let Some(call) = call { - // The binder's last reduction, with nothing left to resolve or on either stack and no assignment in progress. A dfn's last statement returns such a call of a function that the user defined to the caller's loop. - let last = assignment.is_none() && cursor == 0 && pending.is_empty() && binder.stack.is_empty(); - if tail && last && tails(&call.function) { - log(&mut rec, |rc| rc.called(call.span.clone(), Category::Value, true)); - session.stacks.borrow_mut().extend([binder.stack, pending]); - return Ok(Reduced::Tail(call)); - } - let bound = call.function.call_with(call.left.map(Cow::Owned), Cow::Owned(call.right), &call.span)?; - let entity = Entity::new(Term::Binding(bound.value), narrow(&call.span.range), bound.shy, call.expression); - log(&mut rec, |rc| rc.called(call.span.clone(), entity.category, last)); - binder.push(entity); + (read, Some(held)) => { + let Read::Constant(constant) = self.constant(Term::Binding(Binding::Value(key)), text).read else { unreachable!("a constant") }; + let Some(Level { kind: Kind::Item { key: slot, .. }, .. }) = &mut self.levels[held as usize - 1] else { unreachable!("an item's level") }; + *slot = Some(Keyed::Key { constant, colon: colon as u32 }); + pieces.push(read); } - // Bound operators still need their left operand before rebinding on the right. - let m = binder.stack.len(); - if matches!(binder.stack[m - 1].category, Category::Operator) || Self::shifts(&binder.stack[m - 1], &binder.stack[..m - 1]) { break; } - pending.push(binder.stack.pop().unwrap()); - log(&mut rec, |rc| rc.pend()); - crate::execution::check().at(&nodes[0].span)?; + (Read::Register(_), None) => unreachable!("a computed item has a level"), } } - let entity = binder.stack.pop().expect("nonempty expression"); - session.stacks.borrow_mut().extend([binder.stack, pending]); - Ok(Reduced::Entity(entity)) - } - // A browser worker's small stack limits recursion. In wasm, inlining this into `reduced` would put its locals in the frame that `reduced` keeps across each call, at every level of recursion. V8's Liftoff gives each local a slot, whether or not its path runs. - #[cfg_attr(target_arch = "wasm32", inline(never))] - fn reduce(&mut self, capture: bool, source: &Arc, mut rec: Option<&mut Recorder>) -> Result, Error> { - let mut left = self.stack.pop().unwrap(); - let mut right = self.stack.pop().unwrap(); - let range = left.range.clone(); - let span = || span_of(source, &range); - let rule = Rule::get(left.role, right.role); - if let Some(rc) = rec.as_deref_mut() { - match rule { - Rule::Drop => rc.reduce(Reduction::Drop), - Rule::DropRight => rc.reduce(Reduction::DropRight), - Rule::Adjacent => rc.reduce(Reduction::Strand), - Rule::Attach => rc.reduce(Reduction::Attach), - Rule::Call if !matches!(right.term, Term::Binding(Binding::Absent)) => rc.call = true, - // A derive or a bind logs once its result is known. - Rule::Derive | Rule::BindRight => {} - Rule::Train => rc.reduce(Reduction::Train), - _ => rc.fail(), - } + let category = if functions { Category::Callable } else { Category::Value }; + let constants = &self.constants; + let folded = if matches!(node.kind, NodeKind::ArrayLiteral { form: ListForm::Cells, .. }) { None } else { + pieces.iter().map(|piece| match piece { Read::Constant(c) => Some(Cow::Borrowed(&constants[*c as usize])), Read::Register(_) => None }) + .collect::>>().and_then(|pieces| super::tree::assembled(node, pieces.into_iter()).ok()) + }; + if let Some(value) = folded { + self.truncate(start); + return Ok(self.constant(Term::Binding(value), range)); } - let left_expression = left.expression.take(); - let right_expression = right.expression.take(); - let mut expression = None; - let term = match rule { - Rule::Missing => return Err(Error::new(ErrorKind::Value, "expression produced no value").at(&span())), - Rule::Absent => return Err(Error::new(ErrorKind::Value, ABSENT).at(&span())), - Rule::Drop => { - right.expression = right_expression; - self.push(right); - return Ok(None); - } - Rule::DropRight => { - left.expression = left_expression; - self.push(left); - return Ok(None); + for &piece in &pieces { self.release(piece) } + let to = self.register(); + self.ops.push(Op::Array { literal: shared.get_or_init(|| Arc::new(node.clone())).clone(), pieces: pieces.into_boxed_slice(), to, range: range.clone(), check: category }); + Ok(Item::new(Read::Register(to), category, range)) + } + + /// The item for the nodes in `at` of cell `cell` of the bracket literal `node`, compiled as a level of its own, with that level where the item isn't constant. A constant item compiles to its value as the literal takes it (`session::listed`), or to an absent `⍺`, which drops out. The item keeps its own category, which says whether the literal holds a function. `shared` holds the literal for the instruction and the levels that need it, copied once. + fn item(&mut self, node: &Node, shared: &std::cell::OnceCell>, cell: usize, at: std::ops::Range) -> Result<(Item, Option), Error> { + let NodeKind::ArrayLiteral { cells, .. } = &node.kind else { unreachable!("a bracket literal") }; + let session = self.session; + let nodes = &cells[cell][at.clone()]; + let span = crate::syntax::cover(nodes); + let text = narrow(&span.range); + let start = self.mark(); + self.levels.push(None); + let id = self.levels.len() as u32; + self.ops.push(Op::Enter { level: id }); + let value = session.nested(nodes, |_| self.level(nodes, id, None))?; + if !matches!(node.kind, NodeKind::ArrayLiteral { record: true, .. }) && let Read::Constant(c) = value.read && self.ops.len() == start.ops + 1 { + let constant = self.constants[c as usize].clone().value(&span.source)?; + self.truncate(start); + let item = self.constant(piece(session, node, cell, constant, None, &span)?, text); + return Ok((Item { category: value.category, ..item }, None)); + } + self.ops.push(Op::ExitItem { level: id, from: value.read }); + self.release(value.read); + let to = self.register(); + self.levels[id as usize - 1] = Some(Level { + to, range: text.clone(), span: nodes[0].span.clone(), check: Category::Value, + kind: Kind::Item { literal: shared.get_or_init(|| Arc::new(node.clone())).clone(), cell: cell as u32, key: None } + }); + Ok((Item::new(Read::Register(to), value.category, text), Some(id))) + } + + /// The assignment whose `←` is at `i` among `nodes`, of `value`, or with no value, `a(f)←`'s update. Gives the item that holds what the assignment gives, which is shy, and the position where its target starts, where the walk carries on. A target that is one name gives that name the category of `value` for the rest of the statement. + fn assignment(&mut self, nodes: &[Node], i: usize, value: Option) -> Result<(Item, usize), Error> { + let session = self.session; + let span = &nodes[i].span; + let begin = nodes[..i].iter().rposition(|n| matches!(n.kind, NodeKind::Assign)).map_or(0, |j| j + 1); + if begin == i { return Err(Error::new(ErrorKind::Syntax, NO_TARGET).at(span)); } + match value.as_ref().map(|value| value.category) { + Some(Category::NoResult) => return Err(Error::new(ErrorKind::Value, "assignment requires a value").at(span)), + Some(Category::Absent) => return Err(Error::new(ErrorKind::Value, ABSENT).at(span)), + _ => {} + } + // A one-node target starts where it is, whatever its name holds, so only a longer one reads the roles of what its names hold. + let (start, modifier) = if i - begin == 1 { (begin, None) } else { self.target(nodes, begin, i)? }; + let target = self.assigned_target(&nodes[start..i], modifier.as_ref().map_or(0, |(count, _)| *count as usize)); + let category = value.as_ref().map_or(Category::NoResult, |value| value.category.settled()); + let local = match (&nodes[start..i], &value) { + ([Node { kind: NodeKind::Name(name, _), .. }], Some(_)) => { + self.assigned.push((*name, category)); + match session.bindings.here(*name) { Place::Slot(_, slot) => Some(slot), _ => None } } + _ => None, + }; + let call = value.as_ref().is_some_and(|value| value.call); + let value = value.map(|value| { self.release(value.read); value.read }); + let modifier = modifier.map(|(_, item): (u32, Item)| { self.release(item.read); item.read }); + let to = self.register(); + let range = narrow(&span.range); + self.ops.push(Op::Assign { value, local, target, span: span.clone(), modifier, to, range: range.clone(), call }); + Ok((Item { call, ..Item::new(Read::Register(to), category, range) }, start)) + } + + /// Where the target of the assignment whose `←` is at `i` starts, no further left than `begin`, with a modified assignment's function: its count of nodes, and its item, which compiles here as any function does. The target is the operand before `←` (`trailing`). Where that operand is a function with an array before it, the function modifies, and the target is the operand before the function. In a dfn a name before `←` is the target unless an array comes directly before it. Then the name's value decides, as at top level, and a function makes a modified assignment, as in `a(f)←3` and `(a)f←3`. + fn target(&mut self, nodes: &[Node], begin: usize, i: usize) -> Result<(usize, Option<(u32, Item)>), Error> { + let session = self.session; + if session.rule_target(&nodes[begin..i], &mut Some(&mut self.facts)).is_some() { return Ok((begin, None)); } + let mark = self.mark(); + let (operand, item) = self.trailing(nodes, begin, i)?; + if item.category.valence().is_some() { + return Err(Error::new(ErrorKind::Syntax, "missing assignment operand").at(&span_of(&nodes[0].span.source, &item.range))); + } + let modified = operand > begin && item.category.before(None).settled() == Category::Function + && (self.category(&nodes[operand - 1]) == Category::Value || !session.assignment_names(&nodes[operand..i], &mut Some(&mut self.facts))); + let start = if modified { + let read = self.mark(); + let start = self.trailing(nodes, begin, operand).map(|(start, _)| start); + self.truncate(read); + start? + } else { + self.truncate(mark); + operand + }; + if start > begin && self.category(&nodes[start]) == Category::Value && self.category(&nodes[start - 1]) == Category::Value { + let span = Span { source: nodes[start].span.source.clone(), range: nodes[start - 1].span.range.start..nodes[start].span.range.end }; + return Err(Error::new(ErrorKind::Syntax, STRAND_TARGET).at(&span)); + } + Ok((start, modified.then(|| ((i - operand) as u32, item)))) + } + + /// The category that this compiler gives `node` read alone, compiling nothing: a group's or a run's is its value's, a dot path's is its field's, and a run that ends in a dyadic operator is a dyadic operator, as `leaf` reads it. An absent `⍺` counts as an array, as an unbound name does, and a node that can't compile has no result. + fn category(&mut self, node: &Node) -> Category { + if let NodeKind::Run(inner) = &node.kind && self.ends_in_dyadic_operator(inner) { return Category::DyadicOperator; } + let mark = self.mark(); + let read = self.leaf(node, &mut Vec::new()).map(|item| item.category.before(None).settled()); + self.truncate(mark); + match read { Ok(Category::Absent) => Category::Value, Ok(category) => category, Err(_) => Category::NoResult } + } + + /// Compiles the reduction of the top two items of `stack`, by the rule that their roles give. `last` marks the statement's last reduction on its own level. + fn reduce(&mut self, stack: &mut Vec, last: bool, source: &Arc) -> Result<(), Error> { + let (left, right) = (stack.pop().expect("a left item"), stack.pop().expect("a right item")); + let span = span_of(source, &left.range); + let item = match Rule::get(left.role, right.role) { + Rule::Missing => return Err(Error::new(ErrorKind::Value, "expression produced no value").at(&span)), + Rule::Absent => return Err(Error::new(ErrorKind::Value, ABSENT).at(&span)), + Rule::Drop => { self.release(left.read); right } + Rule::DropRight => { self.release(right.read); left } // Arrays side by side form a strand, one item each, as in `[a b c]`. Rule::Adjacent => { - let (items, from_left, _) = strand(left, right, source)?; - if capture { - let mut parts = Expression::items(left_expression.map(|e| *e), &items[..from_left]); - parts.extend(Expression::items(right_expression.map(|e| *e), &items[from_left..])); - expression = Some(Box::new(Expression::Strand(parts))); - } - Term::Strand(items) + let range = left.range.clone(); + self.folded(left, right, Category::Value, range, source, |left, right, to| Op::Strand { left, right, to }) } - Rule::Derive => match derived(left.value(source)?, right.value(source)?, &span())? { - Derived::Term(binding) => { - log(&mut rec, |rc| { - rc.reduce(Reduction::Derive { span: span(), range: range.clone(), result: Category::bound(&binding), value: binding.clone() }) - }); - Term::Binding(binding) - } - // The call logs the derive once the binder has made it (`Recorder::called`). - Derived::Call(function, x, y) => { - if let Some(rc) = rec.as_deref_mut() { (rc.call, rc.derive) = (true, true); } - let expression = capture.then(|| { - let operand = left_expression.map_or_else(|| Expression::array(x.as_ref().unwrap_or(&y)), |e| *e); - Box::new(if x.is_some() { Expression::call(&function, Some(operand), Expression::array(&y)) } - else { Expression::call(&function, None, operand) }) - }); - return Ok(Some(Application { function, left: x, right: y, span: span(), unshy: false, expression })); - } - }, + Rule::Derive => self.derive(left, right, span, source), Rule::BindRight => { - let bound = bound_right(left.value(source)?, right.value(source)?); - log(&mut rec, |rc| rc.reduce(Reduction::BindRight(bound.clone()))); - Term::Binding(Binding::Operator(bound)) + let (range, call) = (left.range.clone(), right.call); + let literal = right.literal; + self.folded(left, right, Category::Operator, range, source, |operator, operand, to| Op::BindRight { operator, operand, to, call, literal }) } + // A call belongs to its function's text, so a bound left argument takes `right`'s. Rule::Attach => { - let (x, f, range) = attached(Cow::Owned(left), Cow::Owned(right), source)?; - self.push(Entity::new(Term::Left(x.into_owned(), f.into_owned()), range, false, left_expression)); - return Ok(None); + let range = right.range.clone(); + let literal = left.literal; + self.folded(left, right, Category::Left, range, source, |left, right, to| Op::Attach { left, right, to, literal }) } - Rule::Call if matches!(right.term, Term::Binding(Binding::Absent)) => Term::Binding(Binding::Absent), - Rule::Call => { - let (x, f) = split_call(Cow::Owned(left), source)?; - let y = right.array(source)?; - let expression = capture.then(|| { - Box::new(Expression::call( - &f, x.as_ref().map(|a| left_expression.map_or_else(|| Expression::array(a), |e| *e)), - right_expression.map_or_else(|| Expression::array(&y), |e| *e))) - }); - return Ok(Some(Application { function: f.into_owned(), left: x.map(Cow::into_owned), right: y, span: span(), unshy: false, expression })); + Rule::Call if right.category == Category::Absent => { + self.release(left.read); + self.constant(Term::Binding(Binding::Absent), left.range) } - Rule::Train => Term::Train(train(Cow::Owned(left), Cow::Owned(right), source)?), - _ => return Err(Error::new(ErrorKind::Syntax, "these grammatical categories do not bind").at(&span())), + Rule::Call => self.call(left, right, span, last), + Rule::Train => { + let range = left.range.clone(); + let literals = [left.literal, right.literal]; + self.folded(left, right, Category::Train, range, source, |left, right, to| Op::Train { left, right, to, literals }) + } + _ => return Err(Error::new(ErrorKind::Syntax, "these grammatical categories do not bind").at(&span)), }; - self.push(Entity::new(term, range, false, expression)); - Ok(None) + push(stack, item); + Ok(()) } -} -pub(super) fn atop(f: Function, g: Function, span: &Span) -> Result { - Function::new(FunctionNode::Composed(OperatorKind::Atop, [Operand::Function(f), Operand::Function(g)]), span) -} + /// `left` and `right` reduced by the instruction that `op` makes, or, where both are constants, by running that instruction's step on them (`tree::paired`), which gives a constant where it succeeds; where it fails, the instruction fails as the statement runs. The item binds as `category`, with the text `range`. + fn folded(&mut self, left: Item, right: Item, category: Category, range: std::ops::Range, source: &Arc, op: impl Fn(Read, Read, u32) -> Op) -> Item { + if let (Read::Constant(l), Read::Constant(r)) = (left.read, right.read) + && let Ok(entity) = paired(&op(left.read, right.read, 0), Cow::Borrowed(&self.constants[l as usize]), Cow::Borrowed(&self.constants[r as usize]), source, false) { + return self.constant(entity.term, range); + } + self.release(left.read); + self.release(right.read); + let to = self.register(); + self.ops.push(op(left.read, right.read, to)); + Item::new(Read::Register(to), category, range) + } -pub(crate) fn before(a: Value, f: Function, span: &Span) -> Result { - Function::new(FunctionNode::Composed(OperatorKind::Before, [Operand::Value(a), Operand::Function(f)]), span) -} + /// `operator` applied to `operand` at `span`. A postfix compiles by its operand's role (`postfix`). Otherwise a constant, where both are constants and the derive gives a binding (`derived`), or an instruction. What it gives binds as `derived_category` says, with the operand's text. + fn derive(&mut self, operand: Item, operator: Item, span: Span, source: &Arc) -> Item { + if let Read::Constant(p) = operator.read && let Term::Postfix(postfix) = &self.constants[p as usize].term { + let postfix = postfix.clone(); + return self.postfix(operand, operator, postfix, span, source); + } + let category = self.derived_category(&operand, &operator); + let range = operand.range.clone(); + if let (Read::Constant(o), Read::Constant(p)) = (operand.read, operator.read) { + let o = self.constants[o as usize].clone().value(source); + if let (Ok(o), Ok(p)) = (o, self.constants[p as usize].clone().value(source)) && let Ok(binding) = derived(o, p, operand.literal, &span) { + return self.constant(Term::Binding(binding), range); + } + } + self.release(operand.read); + self.release(operator.read); + let to = self.register(); + self.ops.push(Op::Derive { + operand: operand.read, operator: operator.read, to, span, range: range.clone(), result: category, + call: operand.call && !operator.postfix, literal: operand.literal + }); + Item { call: true, ..Item::new(Read::Register(to), category, range) } + } + + /// The category that what `operator` gives on `operand` binds as: an array for the call that a postfix operator that a name holds stands for after an array (`tree::Postfix::of`), and a function otherwise. + fn derived_category(&self, operand: &Item, operator: &Item) -> Category { + if operator.postfix && operand.role == Category::Value { Category::Value } else { Category::Function } + } + + /// The postfix `postfix`, whose item is `operator`, compiled for `operand`'s role (target parts 18, 21): after an array, the call that it stands for; after a function, the operator that it stands for, derived as any other (`Postfix::operator`), or `ᵘ`'s scalar. A constant operand folds once (`tree::postfixed`); a postfix that the operand's role rejects compiles to its error (`Op::Fail`). + fn postfix(&mut self, operand: Item, operator: Item, postfix: Postfix, span: Span, source: &Arc) -> Item { + let range = operand.range.clone(); + let array = operand.role == Category::Value; + if !array && !matches!(postfix, Postfix::Unit) { + return match postfix.operator(&span) { + Ok(op) => { + let op = Item { postfix: true, ..self.constant(Term::Binding(Binding::Operator(op)), operator.range) }; + self.derive(operand, op, span, source) + } + Err(error) => { self.release(operand.read); self.fail(error, Category::Function, range) } + }; + } + if array && matches!(postfix, Postfix::Unit) { + self.release(operand.read); + return self.fail(Error::domain("ᵘ makes a scalar that holds a function. Enclose a subject with ⊂").at(&span), Category::Value, range); + } + let category = if array { Category::Value } else { Category::Callable }; + if !array && let Read::Constant(o) = operand.read && let Ok(o) = self.constants[o as usize].clone().value(source) && let Ok(binding) = postfixed(&postfix, o, &span) { + return self.constant(Term::Binding(binding), range); + } + self.release(operand.read); + let to = self.register(); + self.ops.push(Op::Postfix { postfix, operand: operand.read, to, span, range: range.clone(), result: category }); + Item { call: true, ..Item::new(Read::Register(to), category, range) } + } + + /// The call of `function` on `argument` at `span`, compiled as one instruction with the left argument that the attach before it bound, where it did. A constant train builds its function here, once (`built`); a train whose tines aren't all constants builds its value at each evaluation and is called as any function is. The call's result is a value to the binder, whatever it holds, so that how a statement binds depends only on its text and on the roles of the names it reads, not on what a call returns. `last` marks the statement's last reduction on its own level, which returns a call of a dfn or a derived function to the caller's loop. + fn call(&mut self, function: Item, argument: Item, span: Span, last: bool) -> Item { + let range = narrow(&span.range); + let read = function.read; + let (left, function, left_literal) = match self.ops.last() { + Some(&Op::Attach { left, right, to, literal }) if read == Read::Register(to) => { self.ops.pop(); (Some(left), right, literal) } + _ => (None, read, false), + }; + let function = self.built(function, &span); + let literals = u64::from(left_literal) | (u64::from(argument.literal) << 1); + self.release(read); + self.release(argument.read); + let (argument, to) = (argument.read, self.register()); + self.ops.push(Op::Call { left, function, argument, to, span, range: range.clone(), result: Category::Value, last, literals }); + Item { call: true, ..Item::new(Read::Register(to), Category::Value, range) } + } + + /// `read` as a called function: a constant train, all of whose tines are constants, builds its function here, once (target part 19: 1 `Train` value); any other read stays as it is. + fn built(&mut self, read: Read, span: &Span) -> Read { + let Read::Constant(c) = read else { return read }; + if !matches!(self.constants[c as usize].term, Term::Train(_)) { return read; } + let range = self.constants[c as usize].range.clone(); + match self.constants[c as usize].clone().value(&span.source) { Ok(function) => self.constant(Term::Binding(function), range).read, Err(_) => read } + } + + /// An item that reads `term`, kept as a constant with the text `range`, which binds as the term's category. + fn constant(&mut self, term: Term, range: std::ops::Range) -> Item { + let literal = matches!(&term, Term::Binding(Binding::Value(Value::Number(_) | Value::Character(_)))); + let entity = Entity::new(term, range.clone(), false, None); + let category = entity.category(); + self.constants.push(entity); + Item { literal, ..Item::new(Read::Constant(self.constants.len() as u32 - 1), category, range) } + } + + /// A register that holds nothing the compiler still needs. + fn register(&mut self) -> u32 { + let r = self.free.pop().unwrap_or_else(|| { self.registers += 1; self.registers - 1 }); + r + } + + /// Frees `read`'s register, whose item a reduction used up or dropped. + fn release(&mut self, read: Read) { if let Read::Register(r) = read { self.free.push(r) } } -/// `step` logged on `rec`, while it records. -fn log(rec: &mut Option<&mut Recorder>, step: impl FnOnce(&mut Recorder)) { if let Some(rc) = rec.as_deref_mut() { step(rc) } } + fn mark(&self) -> Mark { + Mark { ops: self.ops.len(), constants: self.constants.len(), levels: self.levels.len(), registers: self.registers, free: self.free.clone() } + } + + /// Drops the instructions, constants, levels and registers that the compiler gained after `mark`: a constant group's, or those of a literal that compiles to its value. + fn truncate(&mut self, mark: Mark) { + self.ops.truncate(mark.ops); + self.constants.truncate(mark.constants); + self.levels.truncate(mark.levels); + (self.registers, self.free) = (mark.registers, mark.free); + } +} diff --git a/src/eval/bindings.rs b/src/eval/bindings.rs index 7e2d42f4..674a0ce4 100644 --- a/src/eval/bindings.rs +++ b/src/eval/bindings.rs @@ -1,459 +1,439 @@ -//! Binding storage: the frames of running calls and scopes, each module's names, and where code is running. Code finds where a name is bound as a `Place`, and reads and writes bindings only through `Bindings`. - -use super::{Binding, Category, MAX_FRAME_DEPTH}; -use crate::{Error, Span, Value, name::Name}; +//! Slot frames, call-time capture bindings and stable module entries. +use super::{Binding, CapturedName, Category, MAX_FRAME_DEPTH}; +use crate::{Error, Span, Value, array::TreeRoot, name::Name}; use rustymath::{error::Kind, hashing::{HashMap, HashMapExt, HashSet, HashSetExt}}; use std::{cell::{Cell, RefCell}, ops::Range, sync::atomic::{AtomicU64, Ordering::Relaxed}}; -/// Where a binding is, or where an assignment puts one: a frame's slot, by the frame's index and the slot's number among the frame's slots; a name that a frame binds without a slot, such as a key of a scope's record; or an entry in a module's names, by the module's number and the entry's. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum Place { Slot(usize, u32), Other(usize, Name), Entry(usize, u32) } - +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub(crate) enum Place { Slot(usize, u32, u64), Other(usize, Name, u64), Entry(usize, u32) } impl Place { - /// The frame that holds the binding, or `None` for a module's names, which outlive every frame. A binding can't hold a value that refers to a later frame. - pub(crate) fn frame(self) -> Option { match self { Self::Slot(i, _) | Self::Other(i, _) => Some(i), Self::Entry(..) => None } } + pub(crate) fn frame(self) -> Option { match self { Self::Slot(i, ..) | Self::Other(i, ..) => Some(i), Self::Entry(..) => None } } +} + +#[derive(Clone, Debug)] +pub(crate) struct FrameInputs { frame: usize, generation: u64 } +impl FrameInputs { + pub(super) fn environment(&self) -> Option { Some(self.frame) } + pub(super) fn with_origin(&self, run: impl FnOnce(crate::program::Origin<&[Value]>) -> crate::error::Result) -> crate::error::Result { + super::session().bindings.with_origin(self, run) + } + pub(super) fn rebuild(&self, template: &super::Function) -> crate::error::Result { + self.with_origin(|origin| { + let first = origin.capture_start(); + let trees = origin.trees.as_ref().map(|trees| trees.project_roots(&trees.roots().skip(origin.capture_root_start()).collect::>())); + template.rebuilt(&origin.list[first..], trees.as_ref(), &origin.bindings) + }) + } +} + +pub(super) type Callable = super::FunctionBinding; +impl Callable { + pub(super) fn function(&self) -> crate::error::Result { + match self { Self::Input(function) => Ok(function.clone()), Self::Rebuild { template, dependencies } => dependencies.rebuild(template) } + } + pub(super) fn definition(&self) -> Option<&crate::syntax::Definition> { + match self { Self::Input(function) | Self::Rebuild { template: function, .. } => function.definition() } + } + pub(super) fn origin(&self, args: &[Value], literals: u64) -> crate::error::Result>> { + match self { + Self::Input(function) => function.origin(args, literals), + Self::Rebuild { template, dependencies } => dependencies.with_origin(|parent| { + let mut origin = template.call_origin(&parent, args, &vec![None; args.len()])?; + origin.literals |= super::literal_leaves(origin.trees.as_deref(), literals << origin.operation.functions()); + Ok(origin) + }), + } + } + fn call_outputs(&self, left: Option<&Value>, right: &Value, span: &Span) -> crate::error::Result> { + let args: Vec<_> = left.into_iter().chain([right]).cloned().collect(); + let mut origin = self.origin(&args, 0)?; origin.site = Some(span.clone()); + crate::primitive::Operation::dispatch(origin.reborrow()).map_err(|error| error.at(span)) + } + pub(super) fn call_array(&self, left: Option<&Value>, right: &Value, span: &Span) -> crate::error::Result { + self.call_outputs(left, right, span)?.assemble().map_err(|error| error.at(span)) + } + pub(super) fn call_with(&self, left: Option, right: R, span: &Span) -> crate::error::Result { + super::Bound::from_outputs(self.call_outputs(left.as_ref().map(super::Argument::value), right.value(), span)?, span) + } } -/// The number of slots before a frame's locals: one for each of the six names that a call binds itself, in the order of `Name::implicit`. const IMPLICIT: usize = 6; +static IDS: AtomicU64 = AtomicU64::new(1); -/// What a frame binds without slots: names that `⍎` binds where the frame's definition has no slot for them, `$e`, a scope's assignments, and a scope's record, whose keys on its first axis name its items or major cells (`Frame::key`). A key's text never becomes a `Name`, so a record's keys never add to the process's names. -#[derive(Clone, Default)] +#[derive(Default)] struct Unslotted { names: HashMap, record: Option } -/// The frame of a running call or scope. Its slots on the slot stack hold the six names that a call binds itself, then the names that its definition gives slots. It also has what it binds without slots, the frame it sees names through after its own (`parent`), and the module whose names it sees after its frames. pub(super) struct Frame { - /// The names that have slots after the six that a call binds itself, in slot order: the definition's locals (`Definition::locals`). Empty for a scope, and for a definition that assigns no names. - locals: &'static [Name], - /// Where the frame's slots start on the slot stack. - base: usize, - /// What the frame binds without slots. Most frames of calls bind nothing there and allocate nothing for it. - unslotted: Option>, - parent: Option, - module: usize, + locals: &'static [Name], base: usize, id: u64, + unslotted: Option>, parent: Option, module: usize, + origin: Option>>, scope: Option, roots: Box<[TreeRoot]>, } - impl Frame { - /// The frame of a call, with nothing bound yet. `locals` are the names that its definition assigns. - #[inline] - pub(super) fn new(parent: Option, module: usize, locals: &'static [Name]) -> Self { Self { locals, base: 0, unslotted: None, parent, module } } - - /// The frame of a scope over `record`, which has keys on its first axis, with nothing bound yet. + pub(super) fn new(parent: Option, module: usize, locals: &'static [Name]) -> Self { + Self { locals, base: 0, id: IDS.fetch_add(1, Relaxed), unslotted: None, parent, module, origin: None, scope: None, roots: Box::default() } + } pub(super) fn scope(parent: Option, module: usize, record: Value) -> Self { Self { unslotted: Some(Box::new(Unslotted { names: HashMap::new(), record: Some(record) })), ..Self::new(parent, module, &[]) } } - - /// Whether the frame binds names without slots: names that `⍎` or an assignment binds where the frame has no slot for them, or a scope's keys. - #[inline] - fn unslotted(&self) -> bool { self.unslotted.is_some() } - - /// Whether the frame can bind a name other than the six that a call binds itself. - #[inline] - fn binds_named(&self) -> bool { !self.locals.is_empty() || self.unslotted() } - - /// The positions of the frame's slots on the slot stack. - #[inline] - fn slots(&self) -> Range { self.base..self.base + IMPLICIT + self.locals.len() } - - /// The number among the frame's slots of `name`'s slot, where the frame has one. - #[inline] + fn slots(&self) -> Range { self.base..self.captured() + self.roots.len() } + fn captured(&self) -> usize { self.base + IMPLICIT + self.locals.len() } + fn inputs(&self, frame: usize) -> FrameInputs { FrameInputs { frame, generation: self.id } } + fn closed(&self) -> Option<&super::CaptureScope> { self.origin.as_ref()?.bindings.scopes.get(self.scope? as usize) } + fn root_slot(&self, position: &TreeRoot) -> Option { + let origin = self.origin.as_ref()?; + let index = origin.capture_root_start() + position.index; + self.roots.get(index).map(|_| self.captured() + index) + } + fn root_value(&self, slot: usize, slots: &[Option]) -> Option { + if let Some(binding) = &slots[slot] { return Some(binding.clone()); } + let origin = self.origin.as_ref()?; + let root = &self.roots[slot - self.captured()]; + Some(Binding::Value(origin.trees.as_ref().map_or_else(|| origin.list[root.leaves.start].clone(), |trees| trees.at_root(root, &origin.list).into_owned()))) + } + fn root_binding(&self, frame: usize, slot: usize, slots: &[Option]) -> Option { + Some(match self.root_value(slot, slots)? { + Binding::Value(Value::Function(function)) if self.origin.as_ref()?.bindings.scope(&function).is_some() => + Binding::Function(super::FunctionBinding::Rebuild { template: function, dependencies: self.inputs(frame) }), + Binding::Value(Value::Function(function)) => function.into(), + Binding::Value(Value::Operator(operator)) => Binding::Operator((*operator).clone()), binding => binding, + }) + } fn slot(&self, name: Name) -> Option { - match name.implicit() { Some(k) => Some(k as u32), None => self.locals.binary_search(&name).ok().map(|k| (IMPLICIT + k) as u32) } + name.implicit().map(|k| k as u32).or_else(|| self.locals.binary_search(&name).ok().map(|k| (IMPLICIT + k) as u32)) } - - /// The binding of `name` among the names that the frame binds without slots. - #[inline] - fn other(&self, name: Name) -> Option<&Binding> { self.unslotted.as_ref()?.names.get(&name) } - - /// The binding of `name`, a name other than the six that a call binds itself, in frame `i`, with its place, besides a key of a scope's record (`key`). A name that has a slot is bound there or nowhere in the frame. It's out of line, so that `Bindings::find` stays small, and `find` calls it only for a frame that `binds_named`. - #[inline(never)] - fn named<'a>(&'a self, i: usize, slots: &'a [Option], name: Name) -> Option<(Place, &'a Binding)> { - match self.slot(name) { - Some(s) => Some((Place::Slot(i, s), slots[self.base + s as usize].as_ref()?)), None => Some((Place::Other(i, name), self.other(name)?)) - } + fn place(&self, i: usize, name: Name) -> Place { + self.slot(name).map_or(Place::Other(i, name, self.id), |slot| Place::Slot(i, slot, self.id)) } - - /// The item or major cell that `name` names as a key on the first axis of the frame's record, where the frame is a scope's. Only a scope's frame reads a name's text. + fn other(&self, name: Name) -> Option<&Binding> { self.unslotted.as_ref()?.names.get(&name) } fn key(&self, name: Name) -> Option { let record = self.unslotted.as_ref()?.record.as_ref()?; - let at = record.keys(0)?.position(name.text())?; - let item = if record.shape().len() == 1 { record.at(at) } else { record.cells(record.shape().len() - 1).ok()?.get(at).ok()? }; - Some(Binding::from_element(item)) + record.keys(0)?.position(name.text())?; + let operation = crate::primitive::Operation::Primitive { primitive: crate::primitive::Primitive::Mix, pairing: crate::primitive::Pairing::Axes }; + let value = operation.call(&[crate::keyed::text(name.text()), record.clone()], 1).expect("a known record field is a valid structural read"); + Some(Binding::from_element(value)) } - - /// The text of each name that the frame binds, besides the six that a call binds itself. fn names<'a>(&'a self, slots: &'a [Option]) -> impl Iterator { - let locals = self.locals.iter().zip(&slots[self.base + IMPLICIT..]).filter(|(_, slot)| !matches!(slot, None | Some(Binding::Released))) - .map(|(name, _)| name.text()); + let locals = self.locals.iter().zip(&slots[self.base + IMPLICIT..]).filter(|(_, slot)| !matches!(slot, None | Some(Binding::Released))).map(|(name, _)| name.text()); + let captured = self.closed().into_iter().flat_map(|scope| scope.names.iter()).filter_map(move |name| { + self.root_slot(name.position.as_ref()?).and_then(|slot| slots[slot].as_ref()).map(|_| name.name.text()) + }); let keys = |record: &'a Value| record.keys(0).into_iter().flat_map(|keys| keys.names().iter().flatten().map(|key| &**key)); - locals.chain(self.unslotted.iter().flat_map(move |u| u.names.keys().map(|name| name.text()).chain(u.record.iter().flat_map(keys)))) - } -} - -/// A copy of a frame's bindings, which a guard case reads at its cells and then swaps in (`Session::restricted`). -pub(super) struct FrameBindings { slots: Vec>, unslotted: Option> } - -impl FrameBindings { - /// Each array that the frame binds, and a scope's record, for a guard case to read at its cells. - pub(super) fn values_mut(&mut self) -> impl Iterator { - let (names, record) = match self.unslotted.as_deref_mut() { Some(Unslotted { names, record }) => (Some(names), record.as_mut()), None => (None, None) }; - let bindings = self.slots.iter_mut().flatten().chain(names.into_iter().flat_map(|names| names.values_mut())); - bindings.filter_map(|binding| match binding { Binding::Value(v) => Some(v), _ => None }).chain(record) + locals.chain(captured).chain(self.unslotted.iter().flat_map(move |u| u.names.keys().map(|name| name.text()).chain(u.record.iter().flat_map(keys)))) } } -/// The frames of running calls and scopes, innermost last, and the slot stack, which holds each frame's slots after those of the frames below it. A call pushes its slots there and allocates nothing for its names. #[derive(Default)] struct Stack { frames: Vec, slots: Vec> } - -/// The frames that code sees names through from frame `scope`, with their indices: that frame, then each frame's parent in turn (`Bindings::chain`). -struct Chain<'f> { frames: &'f [Frame], scope: Option } - -impl<'f> Iterator for Chain<'f> { - type Item = (usize, &'f Frame); - #[inline(always)] +struct Chain<'a> { frames: &'a [Frame], scope: Option } +impl<'a> Iterator for Chain<'a> { + type Item = (usize, &'a Frame); fn next(&mut self) -> Option { let i = self.scope?; - let frame = &self.frames[i]; + let frame = self.frames.get(i)?; self.scope = frame.parent; Some((i, frame)) } } -/// A module's names. `index` gives each name's entry, which holds its binding. Deleting a name empties its entry, which keeps its place, so the entry number in a compiled load (`Free`) stays the name's. Every reader of a module's names skips empty entries. -struct Module { - /// A number that no other module in the process has. A recording that holds a module's entry numbers can run in another session, where the module of the same number is a different one. - id: u64, - entries: Vec>, - index: HashMap, -} - -/// The `id` that the next module takes. -static MODULE_IDS: AtomicU64 = AtomicU64::new(0); - +struct Module { id: u64, entries: Vec>, index: HashMap } impl Module { - fn new() -> Self { Self { id: MODULE_IDS.fetch_add(1, Relaxed), entries: Vec::new(), index: HashMap::new() } } - /// The binding of `name`, with its entry number. - #[inline] + fn new() -> Self { Self { id: IDS.fetch_add(1, Relaxed), entries: Vec::new(), index: HashMap::new() } } fn get(&self, name: Name) -> Option<(u32, &Binding)> { let at = *self.index.get(&name)?; Some((at, self.entries[at as usize].as_ref()?)) } - /// `name`'s entry number, which a new empty entry takes where the module has none. fn entry(&mut self, name: Name) -> u32 { let Self { entries, index, .. } = self; *index.entry(name).or_insert_with(|| { entries.push(None); entries.len() as u32 - 1 }) } - /// Each name that the module binds, with its binding. fn bound(&self) -> impl Iterator { self.index.iter().filter_map(|(&name, &at)| Some((name, self.entries[at as usize].as_ref()?))) } } -/// Where a compiled load reads a name (`Bindings::load`), worked out when the binder records the read (`Bindings::address`). #[derive(Clone, Debug, PartialEq)] -pub(super) enum Address { - /// One of the six names that a call binds itself, by its slot (`Name::implicit`). - Implicit(usize), - Free(Box), -} - -/// Where a load finds any other name: the slots on the lexical chain that can hold it, nearest first, then the running module's entry for it. +pub(super) enum Address { Implicit(usize), Free(Box) } #[derive(Clone, Debug, PartialEq)] pub(super) struct Free { name: Name, - /// Each frame on the chain whose definition gives `name` a slot, as its depth on the chain and the slot's number among its slots. - slots: Box<[(u32, u32)]>, - /// The running module's number, and its `id`. - module: usize, - id: u64, - /// `name`'s entry in the module, where the module had one when the read was recorded. - entry: Option, + // A captured slot stops lookup even when its call-time binding was absent. + slots: Box<[(u32, Option, bool)]>, module: usize, id: u64, entry: Option, } -/// Every binding, and where code is running: the frames of running calls and scopes with their slots, each module's names, the current frame, and the module whose top level is running. pub(super) struct Bindings { - stack: RefCell, - /// The names of each module, by module number. Module 0 holds the session's own names, and each loaded file adds a module. - modules: RefCell>, - /// The frame that code is running in, or `None` at a module's top level. - current: Cell>, - /// The module whose top level is running. - module: Cell, + stack: RefCell, modules: RefCell>, current: Cell>, module: Cell, } - impl Default for Bindings { - fn default() -> Self { - Self { stack: RefCell::default(), modules: RefCell::new(vec![Module::new()]), current: Cell::default(), module: Cell::default() } - } + fn default() -> Self { Self { stack: RefCell::default(), modules: RefCell::new(vec![Module::new()]), current: Cell::default(), module: Cell::default() } } } - impl Bindings { - /// Takes `other`'s bindings in place of these, for `Session::clear`. - pub(super) fn replace(&self, other: Bindings) { - let Bindings { stack, modules, current, module } = other; - self.stack.replace(stack.into_inner()); - self.modules.replace(modules.into_inner()); - self.current.set(current.get()); - self.module.set(module.get()); + fn with_origin(&self, inputs: &FrameInputs, run: impl FnOnce(crate::program::Origin<&[Value]>) -> crate::error::Result) -> crate::error::Result { + let stack = self.stack.borrow(); + let frame = stack.frames.get(inputs.frame).filter(|frame| frame.id == inputs.generation) + .ok_or_else(|| Error::domain("function's call frame has ended"))?; + if stack.slots[frame.captured()..frame.slots().end].iter().all(Option::is_none) { + return run(frame.origin.as_ref().expect("a captured binding has call context").reborrow()); + } + drop(stack); + let origin = self.origin_at(inputs, None)?; + run(origin.reborrow()) + } + fn origin_at(&self, inputs: &FrameInputs, source: Option>) -> crate::error::Result>> { + let stack = self.stack.borrow(); + let frame = stack.frames.get(inputs.frame).filter(|frame| frame.id == inputs.generation) + .ok_or_else(|| Error::domain("function's call frame has ended"))?; + let source = source.unwrap_or_else(|| frame.origin.as_ref().expect("a captured binding has call context").reborrow()); + let overrides = &stack.slots[frame.captured()..frame.slots().end]; + if overrides.iter().all(Option::is_none) { return Ok(source.with_list(source.list.to_vec())); } + let mut result = super::map_roots(&source, |root| { + let root = root.index; + match &overrides[root] { + Some(Binding::Value(value)) => Ok(Some((value.clone(), None))), + None | Some(Binding::Absent | Binding::Released) => Ok(None), + _ => Err(Error::domain("a captured override must be a produced value")), + } + })?; + for (old, scope) in source.bindings.scopes.iter().zip(&mut std::sync::Arc::make_mut(&mut result.bindings).scopes) { + for (old, name) in old.names.iter().zip(&mut scope.names) { + if old.position.as_ref().and_then(|position| frame.root_slot(position)).is_some_and(|slot| matches!(stack.slots[slot], Some(Binding::Absent | Binding::Released))) { + name.position = None; + } + } + } + Ok(result) } - /// The frame that code is running in, or `None` at a module's top level. - #[inline] - pub(super) fn current(&self) -> Option { self.current.get() } - - /// The module whose names code sees after its frames: the module that defined the running function, or the module whose top level is running. - #[inline] - pub(super) fn module(&self) -> usize { self.running_module(&self.stack.borrow().frames) } - - /// `module`, read from `frames`, which the caller holds borrowed. - #[inline] - fn running_module(&self, frames: &[Frame]) -> usize { self.current.get().map_or(self.module.get(), |i| frames[i].module) } - - /// The frames that code here sees names through, with their indices: the current frame, then each frame's parent in turn. - #[inline(always)] - fn chain<'f>(&self, frames: &'f [Frame]) -> Chain<'f> { Chain { frames, scope: self.current.get() } } + pub(super) fn active_origin(&self, source: &crate::program::Origin<&[Value]>) -> crate::error::Result>>> { + let inputs = { + let stack = self.stack.borrow(); + stack.frames.iter().enumerate().rev().find_map(|(i, frame)| { + let original = frame.origin.as_ref()?; + (std::sync::Arc::ptr_eq(&original.bindings, &source.bindings) && stack.slots[frame.captured()..frame.slots().end].iter().any(Option::is_some)).then(|| frame.inputs(i)) + }) + }; + inputs.map(|inputs| self.origin_at(&inputs, Some(source.reborrow()))).transpose() + } - /// Where a plain assignment binds `name` here: the current frame's slot for it, or its other names where it has none, or the running module's entry for it at a module's top level. - #[inline] + pub(super) fn replace(&self, other: Bindings) { + self.stack.replace(other.stack.into_inner()); self.modules.replace(other.modules.into_inner()); + self.current.set(other.current.get()); self.module.set(other.module.get()); + } + pub(super) fn current(&self) -> Option { self.current.get() } + pub(super) fn inputs(&self) -> Option { + let current = self.current.get()?; + let stack = self.stack.borrow(); + let frame = &stack.frames[current]; + frame.origin.as_ref().map(|_| frame.inputs(current)) + } + pub(super) fn module(&self) -> usize { self.current.get().map_or(self.module.get(), |i| self.stack.borrow().frames[i].module) } + pub(super) fn capture_context(&self, parent: Option) -> Option { + let stack = self.stack.borrow(); + Chain { frames: &stack.frames, scope: parent }.find_map(|(_, frame)| frame.closed().cloned()) + } + fn chain<'a>(&self, frames: &'a [Frame]) -> Chain<'a> { Chain { frames, scope: self.current.get() } } pub(super) fn here(&self, name: Name) -> Place { match self.current.get() { - Some(i) => self.stack.borrow().frames[i].slot(name).map_or(Place::Other(i, name), |s| Place::Slot(i, s)), + Some(i) => self.stack.borrow().frames[i].place(i, name), None => { let m = self.module.get(); Place::Entry(m, self.modules.borrow_mut()[m].entry(name)) } } } - - /// The binding of `name` that code sees here, copied, with its place. `None` where nothing binds `name`. - #[inline] + pub(super) fn local(&self, slot: u32) -> Place { + let i = self.current.get().expect("a local has a running frame"); + Place::Slot(i, slot, self.stack.borrow().frames[i].id) + } pub(super) fn lookup(&self, name: Name) -> Option<(Place, Binding)> { self.find(name, |place, value| (place, value.clone())) } - - /// The category of the binding of `name` that code sees here, for callers that only test what kind of binding a name has. It copies no binding, so it updates no reference count. - #[inline] pub(super) fn category(&self, name: Name) -> Option { self.find(name, |_, value| Category::of(value)) } - - /// The binding that code sees here of the name that a call binds itself at slot `k`, with the frame that holds it. - #[inline(always)] - fn implicit<'s>(&self, frames: &'s [Frame], slots: &'s [Option], k: usize) -> Option<(usize, &'s Binding)> { - for (i, frame) in self.chain(frames) { if let Some(value) = &slots[frame.base + k] { return Some((i, value)); } } - None + pub(super) fn role(&self, name: Name) -> Option<(Category, bool)> { self.find(name, |_, value| super::binder::role(value)) } + fn find(&self, name: Name, read: impl FnOnce(Place, &Binding) -> T) -> Option { + self.find_from(self.current.get(), self.module(), name, read) } - - /// `f` on the binding of `name` that code sees here, and its place. Lookup tries the current frame, then each frame that it sees names through, then the running module's names. Every call's frame binds `⍺`, to an absent argument where the call has no left argument, so lookup finds the current call's `⍺`. No module holds the six names that a call binds itself. In each frame, a name that has a slot is in its slot or unbound there, and an empty slot sends the lookup on to the next frame, as an unbound name does. In a scope's frame, a key of its record names its item or major cell, after any name that an assignment in the scope binds. The binding stays borrowed while `f` reads it, so `f` evaluates nothing. - fn find(&self, name: Name, f: impl FnOnce(Place, &Binding) -> T) -> Option { + fn find_from(&self, parent: Option, module: usize, name: Name, read: impl FnOnce(Place, &Binding) -> T) -> Option { let stack = self.stack.borrow(); - let Stack { frames, slots } = &*stack; - if let Some(k) = name.implicit() { return self.implicit(frames, slots, k).map(|(i, value)| f(Place::Slot(i, k as u32), value)); } - for (i, frame) in self.chain(frames) { - if !frame.binds_named() { continue; } - if let Some((place, value)) = frame.named(i, slots, name) { return Some(f(place, value)); } - if let Some(value) = frame.key(name) { return Some(f(Place::Other(i, name), &value)); } + for (i, frame) in (Chain { frames: &stack.frames, scope: parent }) { + if let Some(slot) = frame.slot(name) && let Some(value) = &stack.slots[frame.base + slot as usize] { + return Some(read(Place::Slot(i, slot, frame.id), value)); + } + if let Some(value) = frame.other(name) { return Some(read(Place::Other(i, name, frame.id), value)); } + if let Some(value) = frame.key(name) { return Some(read(Place::Other(i, name, frame.id), &value)); } + if let Some(captured) = frame.closed().and_then(|scope| scope.names.iter().find(|binding| binding.name == name)) { + let slot = frame.root_slot(captured.position.as_ref()?)?; + let binding = frame.root_binding(i, slot, &stack.slots)?; + return Some(read(captured.place.unwrap_or(Place::Slot(i, (slot - frame.base) as u32, frame.id)), &binding)); + } } - let module = self.running_module(frames); + if name.implicit().is_some() { return None; } drop(stack); - self.modules.borrow()[module].get(name).map(|(at, value)| f(Place::Entry(module, at), value)) + self.modules.borrow()[module].get(name).map(|(at, value)| read(Place::Entry(module, at), value)) + } + pub(super) fn capture(&self, parent: Option, module: usize, name: Name) -> Option<(Place, Binding)> { + let stack = self.stack.borrow(); + if let Some(i) = parent && let Some(frame) = stack.frames.get(i) { + if let Some(slot) = frame.slot(name) && let Some(value) = &stack.slots[frame.base + slot as usize] { + return Some((Place::Slot(i, slot, frame.id), value.clone())); + } + if let Some(value) = frame.other(name) { return Some((Place::Other(i, name, frame.id), value.clone())); } + if let Some(value) = frame.key(name) { return Some((Place::Other(i, name, frame.id), value)); } + } + for (i, frame) in stack.frames.iter().enumerate().rev() { + let Some(origin) = &frame.origin else { continue }; + for scope in &origin.bindings.scopes { + if scope.parent != parent || scope.module != module { continue; } + if let Some(captured) = scope.names.iter().find(|binding| binding.name == name) { + let slot = frame.root_slot(captured.position.as_ref()?)?; + return Some((captured.place.unwrap_or(Place::Slot(i, (slot - frame.base) as u32, frame.id)), frame.root_binding(i, slot, &stack.slots)?)); + } + } + } + drop(stack); + self.find_from(parent, module, name, |place, value| (place, value.clone())) } - - /// The binding at `place`, copied. `None` where nothing is bound there, or `place` is in a frame that has ended. - #[inline] pub(super) fn get(&self, place: Place) -> Option { + let stack = self.stack.borrow(); + for (i, frame) in stack.frames.iter().enumerate().rev() { + if let Some(origin) = &frame.origin { + for captured in origin.bindings.scopes.iter().flat_map(|scope| scope.names.iter()) { + if captured.place == Some(place) { return frame.root_binding(i, frame.root_slot(captured.position.as_ref()?)?, &stack.slots); } + } + } + } match place { - Place::Slot(i, s) => { let stack = self.stack.borrow(); stack.slots[stack.frames.get(i)?.base + s as usize].clone() } - Place::Other(i, name) => { - let stack = self.stack.borrow(); - let frame = stack.frames.get(i)?; - frame.other(name).cloned().or_else(|| frame.key(name)) + Place::Slot(i, slot, id) => { let frame = stack.frames.get(i)?; (frame.id == id).then(|| stack.slots[frame.base + slot as usize].clone()).flatten() } + Place::Other(i, name, id) => { let frame = stack.frames.get(i)?; if frame.id != id { return None; } frame.other(name).cloned().or_else(|| frame.key(name)) } + Place::Entry(module, entry) => self.modules.borrow().get(module)?.entries.get(entry as usize)?.clone(), + } + } + fn aliases(stack: &mut Stack, place: Place, value: Option<&Binding>) { + for frame in &stack.frames { + let Some(origin) = &frame.origin else { continue }; + for captured in origin.bindings.scopes.iter().flat_map(|scope| scope.names.iter()) { + if captured.place == Some(place) && let Some(slot) = captured.position.as_ref().and_then(|position| frame.root_slot(position)) { + stack.slots[slot] = value.map(|binding| match binding { + Binding::Function(super::FunctionBinding::Input(function)) => Binding::Value(Value::Function(function.clone())), + Binding::Operator(operator) => Binding::Value(Value::Operator(std::sync::Arc::new(operator.clone()))), binding => binding.clone(), + }).or(Some(Binding::Absent)); + } } - Place::Entry(m, at) => self.modules.borrow()[m].entries[at as usize].clone(), } } - - /// Binds `place` to `value`. - #[inline] pub(super) fn insert(&self, place: Place, value: Binding) { + let mut stack = self.stack.borrow_mut(); match place { - Place::Slot(i, s) => { - let mut stack = self.stack.borrow_mut(); - let at = stack.frames[i].base + s as usize; - stack.slots[at] = Some(value); - } - Place::Other(i, name) => { self.stack.borrow_mut().frames[i].unslotted.get_or_insert_default().names.insert(name, value); } - Place::Entry(m, at) => self.modules.borrow_mut()[m].entries[at as usize] = Some(value), + Place::Slot(i, slot, id) => { if let Some(frame) = stack.frames.get(i).filter(|frame| frame.id == id) { let at = frame.base + slot as usize; stack.slots[at] = Some(value.clone()); } } + Place::Other(i, name, id) => { if let Some(frame) = stack.frames.get_mut(i).filter(|frame| frame.id == id) { frame.unslotted.get_or_insert_default().names.insert(name, value.clone()); } } + Place::Entry(module, entry) => self.modules.borrow_mut()[module].entries[entry as usize] = Some(value.clone()), } + Self::aliases(&mut stack, place, Some(&value)); } - - /// Removes the binding at `place`, and gives it back. A slot or entry stays, empty. pub(super) fn remove(&self, place: Place) -> Option { - match place { - Place::Slot(i, s) => { - let mut stack = self.stack.borrow_mut(); - let at = stack.frames[i].base + s as usize; - stack.slots[at].take() - } - Place::Other(i, name) => self.stack.borrow_mut().frames[i].unslotted.as_mut()?.names.remove(&name), - Place::Entry(m, at) => self.modules.borrow_mut()[m].entries[at as usize].take(), - } + let mut stack = self.stack.borrow_mut(); + let old = match place { + Place::Slot(i, slot, id) => { let frame = stack.frames.get(i)?; if frame.id != id { return None; } let at = frame.base + slot as usize; stack.slots[at].take() } + Place::Other(i, name, id) => { let frame = stack.frames.get_mut(i)?; if frame.id != id { return None; } frame.unslotted.as_mut()?.names.remove(&name) } + Place::Entry(module, entry) => self.modules.borrow_mut()[module].entries[entry as usize].take(), + }; + Self::aliases(&mut stack, place, None); + old } - - /// The address of `name` for a compiled load, read from the frames and module that code runs in now. A dfn's lexical chain is the same on every call, because a closure's environment is a frame of the definition around its literal, so the address holds while no frame on the chain binds names without slots (`load`). pub(super) fn address(&self, name: Name) -> Address { if let Some(k) = name.implicit() { return Address::Implicit(k); } let stack = self.stack.borrow(); - let slots = self.chain(&stack.frames).enumerate().filter_map(|(depth, (_, frame))| Some((depth as u32, frame.slot(name)?))).collect(); - let module = self.running_module(&stack.frames); + let mut slots = Vec::new(); + for (depth, (_, frame)) in self.chain(&stack.frames).enumerate() { + if let Some(slot) = frame.slot(name) { slots.push((depth as u32, Some(slot), false)); } + if let Some(captured) = frame.closed().and_then(|scope| scope.names.iter().find(|binding| binding.name == name)) { + let slot = captured.position.as_ref().and_then(|position| frame.root_slot(position)).map(|slot| (slot - frame.base) as u32); + slots.push((depth as u32, slot, true)); + break; + } + } + let module = self.module(); let modules = self.modules.borrow(); - Address::Free(Box::new(Free { name, slots, module, id: modules[module].id, entry: modules[module].index.get(&name).copied() })) - } - - /// The binding at `address`, copied, as `lookup` finds it. `None` where nothing binds the name. - #[inline(always)] - pub(super) fn load(&self, address: &Address) -> Option { - self.read_at(address, Binding::clone).unwrap_or_else(|| self.unaddressed(address, Binding::clone)) + Address::Free(Box::new(Free { name, slots: slots.into(), module, id: modules[module].id, entry: modules[module].index.get(&name).copied() })) } - - /// The binding of `name` in the current frame's slot, taken out, with its place, for a dfn's last read of a local, `⍵` or `⍺` (`syntax::release`). `None` where that slot holds nothing. - pub(super) fn take(&self, name: Name) -> Option<(Place, Binding)> { - let s = { let stack = self.stack.borrow(); stack.frames[self.current.get()?].slot(name)? }; - self.take_slot(s) + pub(super) fn load(&self, address: &Address) -> Option { self.read_at(address, Binding::clone).unwrap_or_else(|| self.unaddressed(address, Binding::clone)) } + fn own_slot(address: &Address) -> Option { + match address { Address::Implicit(k) => Some(*k as u32), Address::Free(free) => match free.slots.first() { Some(&(0, Some(slot), false)) => Some(slot), _ => None } } } - - /// `take` for a compiled read at `address`. `None` where the address's first slot isn't in the current frame, or that slot holds nothing. - #[inline] pub(super) fn take_at(&self, address: &Address) -> Option { - let s = match address { Address::Implicit(k) => *k as u32, Address::Free(free) => match free.slots.first() { Some(&(0, s)) => s, _ => return None } }; - self.take_slot(s).map(|(_, binding)| binding) - } - - /// The binding in slot `s` of the current frame, taken out, leaving `Binding::Released`. Lookup then stops at the slot, so a later read by text fails there and never finds an outer binding of the name. - fn take_slot(&self, s: u32) -> Option<(Place, Binding)> { + let slot = Self::own_slot(address)?; let i = self.current.get()?; let mut stack = self.stack.borrow_mut(); - let base = stack.frames[i].base; - let slot = &mut stack.slots[base + s as usize]; - if matches!(slot, None | Some(Binding::Released)) { return None; } - slot.replace(Binding::Released).map(|binding| (Place::Slot(i, s), binding)) + let at = stack.frames[i].base + slot as usize; + stack.slots[at].as_ref()?; + stack.slots[at].replace(Binding::Released) } - - /// Where the name at `address` is bound, found by name, as a trace finds a free name's place (`Session::read_binding`). `None` for a name that a call binds itself, and for a name that nothing binds. pub(super) fn place_of(&self, address: &Address) -> Option { match address { Address::Implicit(_) => None, Address::Free(free) => self.find(free.name, |place, _| place) } } - - /// The category of the binding at `address`, as `category` gives it, for a recording's check of a fact. - pub(super) fn category_at(&self, address: &Address) -> Option { - self.read_at(address, Category::of).unwrap_or_else(|| self.unaddressed(address, Category::of)) - } - - /// `read` on the binding at `address`, as `find` finds it, or `Some(None)` where nothing binds the name. `None` where the address can't tell: a frame on the chain binds names without slots, as `⍎` and scopes bind them, the running module isn't the recorded one, or the module had no entry for the name when the address was worked out. - #[inline(always)] + pub(super) fn role_at(&self, address: &Address) -> Option<(Category, bool)> { self.read_at(address, super::binder::role).unwrap_or_else(|| self.unaddressed(address, super::binder::role)) } fn read_at(&self, address: &Address, read: impl Fn(&Binding) -> R) -> Option> { let stack = self.stack.borrow(); - let Stack { frames, slots } = &*stack; - let free = match address { - Address::Implicit(k) => return Some(self.implicit(frames, slots, *k).map(|(_, value)| read(value))), Address::Free(free) => free + let Address::Free(free) = address else { + let Address::Implicit(k) = address else { unreachable!() }; + return Some(self.chain(&stack.frames).find_map(|(_, frame)| stack.slots[frame.base + k].as_ref().map(&read))); }; - let mut shadows = free.slots.iter().peekable(); - for (depth, (_, frame)) in self.chain(frames).enumerate() { - if frame.unslotted() { return None; } - if let Some(&&(at, s)) = shadows.peek() && at as usize == depth { - shadows.next(); - if let Some(value) = &slots[frame.base + s as usize] { return Some(Some(read(value))); } + let mut candidates = free.slots.iter().peekable(); + for (depth, (frame_index, frame)) in self.chain(&stack.frames).enumerate() { + if frame.unslotted.is_some() { return None; } + while let Some(&&(at_depth, slot, stop)) = candidates.peek() { + if at_depth as usize != depth { break; } + candidates.next(); + let value = slot.and_then(|slot| if stop { frame.root_binding(frame_index, frame.base + slot as usize, &stack.slots) } + else { stack.slots[frame.base + slot as usize].clone() }); + if value.is_some() || stop { return Some(value.as_ref().map(&read)); } } } - if self.running_module(frames) != free.module { return None; } + drop(stack); let modules = self.modules.borrow(); - let module = &modules[free.module]; + let module = modules.get(free.module)?; if module.id != free.id { return None; } Some(module.entries[free.entry? as usize].as_ref().map(read)) } - - /// `read` on the binding of the name at `address`, looked up by `find`, where the address can't tell (`read_at`). Out of line, so that a compiled load stays small. - #[cold] - #[inline(never)] fn unaddressed(&self, address: &Address, read: impl Fn(&Binding) -> R) -> Option { - let Address::Free(free) = address else { unreachable!("the address of a name that a call binds itself always tells") }; + let Address::Free(free) = address else { unreachable!() }; self.find(free.name, |_, value| read(value)) } - - /// The text of each name that code sees here, once each, sorted: the names of the current frame and of each frame that it sees names through, then the running module's. The six names that a call binds itself, and empty slots, aren't listed. pub(super) fn names(&self) -> Vec { let stack = self.stack.borrow(); let mut names = HashSet::new(); - for (_, frame) in self.chain(&stack.frames) { names.extend(frame.names(&stack.slots)); } - names.extend(self.modules.borrow()[self.running_module(&stack.frames)].bound().map(|(name, _)| name.text())); - let mut names: Vec<_> = names.into_iter().map(str::to_owned).collect(); - names.sort_unstable(); - names - } - - /// Adds a module with no names, and gives its number. - pub(super) fn new_module(&self) -> usize { - let mut modules = self.modules.borrow_mut(); - modules.push(Module::new()); - modules.len() - 1 + for (_, frame) in self.chain(&stack.frames) { names.extend(frame.names(&stack.slots).map(str::to_owned)); } + names.extend(self.modules.borrow()[self.module()].bound().map(|(name, _)| name.text().to_owned())); + let mut names: Vec<_> = names.into_iter().collect(); names.sort(); names } - - /// Each name in module `module`, with its binding, sorted by the names' text. + pub(super) fn new_module(&self) -> usize { let mut modules = self.modules.borrow_mut(); modules.push(Module::new()); modules.len() - 1 } pub(super) fn module_names(&self, module: usize) -> Vec<(Name, Binding)> { - let mut names: Vec<_> = self.modules.borrow()[module].bound().map(|(name, value)| (name, value.clone())).collect(); - names.sort_by_cached_key(|(name, _)| name.text()); - names + let mut names: Vec<_> = self.modules.borrow()[module].bound().map(|(name, value)| (name, value.clone())).collect(); names.sort_by_key(|&(name, _)| name); names } - - /// Runs `f` with no current frame, at the top level of module `module`, or of the running top-level module where `module` is `None`. Then puts back the frame and module that were running. - pub(super) fn at_top_level(&self, module: Option, f: impl FnOnce() -> T) -> T { + pub(super) fn at_top_level(&self, module: Option, run: impl FnOnce() -> T) -> T { let (current, running) = (self.current.replace(None), self.module.get()); if let Some(module) = module { self.module.set(module); } - let result = f(); - self.current.set(current); - self.module.set(running); - result + let result = run(); self.current.set(current); self.module.set(running); result } - - /// The number of frames, which is the index of the next frame that a call or scope pushes. - #[inline] pub(super) fn depth(&self) -> usize { self.stack.borrow().frames.len() } - - /// Pushes `frame` and makes it current. Its first slots hold `implicit`, the bindings of the six names that a call binds itself, in slot order (`Name::implicit`), and its locals' slots are empty. Fails at the depth limit, before pushing. - // Out of line in the browser, to keep its locals out of the frame that `Session::call_defined` keeps on the stack at every level of recursion, as `Binder::reduce`'s comment explains. - #[cfg_attr(target_arch = "wasm32", inline(never))] - #[cfg_attr(not(target_arch = "wasm32"), inline(always))] - pub(super) fn enter(&self, mut frame: Frame, mut implicit: [Option; IMPLICIT], span: &Span) -> Result<(), Error> { + pub(super) fn enter(&self, mut frame: Frame, mut implicit: [Option; IMPLICIT], closed: Option<(Option, crate::program::Origin<&[Value]>)>, span: &Span) -> Result<(), Error> { + if let Some((scope, origin)) = closed { + frame.scope = scope; + frame.roots = origin.trees.as_ref().map_or_else(|| (0..origin.list.len()).map(|index| TreeRoot { index, leaves: index..index + 1 }).collect(), |trees| trees.roots().collect()); + frame.origin = Some(origin.with_list(origin.list.to_vec())); + } let mut stack = self.stack.borrow_mut(); - if stack.frames.len() == MAX_FRAME_DEPTH { return Err(Error::new(Kind::Limit, format!("lexical frame depth exceeds {MAX_FRAME_DEPTH}")).at(span)); } + if stack.frames.len() == MAX_FRAME_DEPTH { return Err(Error::new(Kind::Limit, "too many active frames").at(span)); } frame.base = stack.slots.len(); + let inputs = frame.inputs(stack.frames.len()); + for binding in implicit.iter_mut().flatten() { + if let Binding::Function(super::FunctionBinding::Input(function)) = binding && frame.origin.as_ref().is_some_and(|origin| origin.bindings.scope(function).is_some()) { + *binding = Binding::Function(super::FunctionBinding::Rebuild { template: function.clone(), dependencies: inputs.clone() }); + } + } stack.slots.resize_with(frame.slots().end, || None); - stack.slots[frame.base..frame.base + IMPLICIT].swap_with_slice(&mut implicit); - self.current.set(Some(stack.frames.len())); - stack.frames.push(frame); + for (slot, value) in stack.slots[frame.base..frame.base + IMPLICIT].iter_mut().zip(&mut implicit) { std::mem::swap(slot, value); } + self.current.set(Some(stack.frames.len())); stack.frames.push(frame); Ok(()) + } + pub(super) fn enter_context(&self, origin: crate::program::Origin<&[Value]>, span: &Span) -> Result<(), Error> { + let current = self.current.get(); + self.enter(Frame::new(None, self.module(), &[]), Default::default(), Some((None, origin)), span)?; + self.current.set(current); Ok(()) } - - /// Drops the frames from `depth` on, with their slots, as a tail call does before it pushes the frame of the call it makes. - #[inline] pub(super) fn truncate(&self, depth: usize) { let mut stack = self.stack.borrow_mut(); if let Some(base) = stack.frames.get(depth).map(|frame| frame.base) { stack.slots.truncate(base); } stack.frames.truncate(depth); } - - /// Drops the frames from `depth` on, and makes `current` the current frame. - #[inline] - pub(super) fn leave(&self, depth: usize, current: Option) { - self.truncate(depth); - self.current.set(current); - } - - /// A copy of frame `i`'s bindings, for a guard case to read at its cells and then `swap` in. - pub(super) fn frame_bindings(&self, i: usize) -> FrameBindings { - let stack = self.stack.borrow(); - let frame = &stack.frames[i]; - FrameBindings { slots: stack.slots[frame.slots()].to_vec(), unslotted: frame.unslotted.clone() } - } - - /// Puts each frame's bindings in, and gives back the bindings they replace. - pub(super) fn swap(&self, bindings: Vec<(usize, FrameBindings)>) -> Vec<(usize, FrameBindings)> { - let mut stack = self.stack.borrow_mut(); - let Stack { frames, slots } = &mut *stack; - bindings.into_iter().map(|(i, mut copy)| { - let frame = &mut frames[i]; - slots[frame.slots()].swap_with_slice(&mut copy.slots); - std::mem::swap(&mut frame.unslotted, &mut copy.unslotted); - (i, copy) - }).collect() - } + pub(super) fn leave(&self, depth: usize, current: Option) { self.truncate(depth); self.current.set(current); } } diff --git a/src/eval/dissect.rs b/src/eval/dissect.rs index f7eef9c4..11a80f02 100644 --- a/src/eval/dissect.rs +++ b/src/eval/dissect.rs @@ -1,16 +1,15 @@ use super::*; #[derive(Clone, Debug)] -pub(super) enum Expression { Atom(Operand), Operator(OperatorNode), Call { function: Function, left: Option>, right: Box }, Strand(Vec) } - +pub(super) enum Expression { Atom(Value), Call { function: Function, left: Option>, right: Box }, Strand(Vec) } impl Expression { pub(super) fn binding(binding: &Binding) -> Option { - match binding { - Binding::Value(a) => Some(Self::array(a)), Binding::Function(f) => Some(Self::Atom(Operand::Function(f.clone()))), - Binding::Operator(op) => Some(Self::Operator(op.clone())), _ => None - } + Some(Self::Atom(match binding { + Binding::Value(value) => value.clone(), Binding::Function(function) => Value::Function(function.function().ok()?), + Binding::Operator(operator) => Value::Operator(operator.clone()), _ => return None, + })) } - pub(super) fn array(value: &Value) -> Self { Self::Atom(Operand::Value(value.clone())) } + pub(super) fn array(value: &Value) -> Self { Self::Atom(value.clone()) } pub(super) fn call(function: &Function, left: Option, right: Self) -> Self { Self::Call { function: function.clone(), left: left.map(Box::new), right: Box::new(right) } } @@ -19,26 +18,23 @@ impl Expression { } fn tree(&self, el: Elide, budget: &mut usize) -> crate::display::Tree { use crate::display::Tree; - if *budget == 0 { return Tree::leaf("…"); } - *budget -= 1; + if *budget == 0 { return Tree::leaf("…"); } *budget -= 1; match self { - Self::Atom(a) => a.tree(el, budget), - Self::Operator(op) => Tree::leaf(op.text(el)), + Self::Atom(Value::Function(function)) => function.tree(el, budget), + Self::Atom(value) => Tree::leaf(value.operand(el)), Self::Call { function, left, right } => { let mut children = vec![function.tree(el, budget)]; - children.extend(left.iter().map(|a| a.tree(el, budget))); - children.push(right.tree(el, budget)); + children.extend(left.iter().map(|a| a.tree(el, budget))); children.push(right.tree(el, budget)); Tree { label: "call".into(), children } } Self::Strand(items) => Tree { label: "[]".into(), children: items.iter().map(|a| a.tree(el, budget)).collect() }, } } fn text(&self, el: Elide, budget: &mut usize) -> String { - if *budget == 0 { return "…".into(); } - *budget -= 1; + if *budget == 0 { return "…".into(); } *budget -= 1; match self { - Self::Atom(a) => a.text(el, budget), - Self::Operator(op) => op.text(el), + Self::Atom(Value::Function(function)) => function.text(el, budget), + Self::Atom(value) => value.operand(el), Self::Call { function, left, right } => { let left = left.as_ref().map_or(String::new(), |a| a.text(el, budget)); format!("({left}({}){})", function.text(el, budget), right.text(el, budget)) diff --git a/src/eval/operators.rs b/src/eval/operators.rs index 2cfb2888..1e955db0 100644 --- a/src/eval/operators.rs +++ b/src/eval/operators.rs @@ -1,2023 +1,587 @@ -//! The operators: composition, power, inverse, key, outer and inner products, rank, each, reduce, scan, stencil, `@` and agenda. - +//! Operator semantics build ordinary calls, maps, selects and loops; execution stays in the graph runner. use super::*; -use crate::transform::Pairing; -use rustymath::error::Kind; +use crate::{array::Layout, error::Result, primitive::{Exit, Order, Pairing, ReadOp, ViewAxis}, program::{Origin, Outputs}}; -/// `f`'s results converted to the marker's numeric type. Plain `ₓ` uses a primitive's `Exact` kernel where one exists. -pub(super) fn marked(f: &Function, mark: crate::number::Mark, left: Option<&Value>, right: &Value, span: &Span) -> Result { - use {crate::{number::Mark, primitive::Exact}, rustymath::buffer::DType}; - match mark { - Mark::Width(w) => crate::derivative::converted(&f.call(left, right, span)?.array(span)?, DType::Float(w), span), - Mark::Fixed(w) => crate::derivative::converted(&f.call(left, right, span)?.array(span)?, DType::Integer(w), span).at(span), - Mark::Exact => match f.as_primitive().and_then(|p| Exact::of(p, left.is_some())) { - Some(e) => Primitive::Exact(e).call(left, right, span), - None => { - let result = f.call(left, right, span)?.array(span)?; - if result.fixed_width().is_some() { Ok(result.ordinary()) } else { Primitive::Exact(Exact::Floor).call(None, &result, span) } - } - }, - } +const fn entry(python: &'static str, glyph: &'static str, dyadic: bool) -> OperatorInfo { + OperatorInfo { call: operator_call, spelling, inverse, discards, python, glyph, dyadic, operands: operand_kind } } -/// `gᵇ`, which is `f`, called on `left` and `right`, where `operand` is `g`. A dyadic call takes `f`'s primitive where `g`'s Boolean kernel gives its truth table (`Function::dyadic_primitive`), as folds and scans do. Otherwise the call builds the table by calling `g` on Booleans: for a monadic call, whose table comes from `g`'s monadic form, and for a `g` that isn't a primitive. -pub(super) fn bitwise(f: &Function, operand: &Function, left: Option<&Value>, right: &Value, span: &Span) -> Result { - if left.is_some() && let Some(p) = f.dyadic_primitive() { return p.call(left, right, span); } - let mut table = 0; - for i in 0..if left.is_some() { 4 } else { 2 } { - let boolean = |b| Value::Number(crate::Number::from_bool(b)); - let (x, y) = (boolean(i & 2 != 0), boolean(i & 1 != 0)); - let result = operand.call_array(left.map(|_| &x), &y, span)?.boolean().described("a bitwise operand must return a Boolean")?; - table |= u8::from(result) << i; - } - Primitive::Bitwise(table).call(left, right, span) -} +static ENTRIES: [OperatorInfo; 30] = [ + entry("reduce", "/", false), entry("reduce_first", "⌿", false), entry("scan", "\\", false), entry("scan_first", "⍀", false), + entry("each", "¨", false), entry("commute", "⍨", false), entry("inverse", "⁻¹", false), entry("outer", "⊗", false), + entry("key", "⌸", false), entry("derivative", "∂", false), entry("optimize", "⇄", false), entry("bitwise", "ᵇ", false), + entry("mark", "", false), entry("under", "@", true), entry("atop", "∘", true), entry("over", "⍥", true), + entry("rank", "⍤", true), entry("axis", "⍠", true), entry("stencil", "⌺", true), entry("dot", ".", true), + entry("power", "⍣", true), entry("before", "↣", true), entry("after", "↢", true), entry("valences", "⊘", true), + entry("agenda", "⍚", true), entry("field", ".", false), entry("index", ".", true), entry("framed", "framed", false), + entry("jvp_step", "jvp", false), entry("vjp_step", "vjp", false), +]; -// Inlined into `dispatch`, this would add its locals to the stack frame of every BPL call, limiting recursion in the browser. -#[inline(never)] -pub(super) fn composition(f: &Function, left: Option, right: R, span: &Span) -> Result { +pub(super) fn info(kind: &OperatorKind) -> &'static OperatorInfo { use OperatorKind::*; - let &FunctionNode::Composed(op, ref operands) = f.node() else { unreachable!("composition takes a composed function") }; - // An operator that reads its arguments after calling its operand borrows them. The others give them to their operand's last call. - let (x, y) = (left.as_ref().map(L::value), right.value()); - if matches!(op, Under) { return under(operands, x, y, || f.bpl(), span); } - if matches!(op, Agenda) { return agenda(operands, left, right, || f.bpl(), span); } - if matches!(op, Stencil) { - if left.is_some() { return Err(Error::new(ErrorKind::Syntax, "stencil is monadic")); } - let [Operand::Function(f), spec] = operands else { unreachable!() }; - return stencil(f, &computed(spec, None, y, span)?, y, span); - } - if matches!(op, Power) { return power(operands, x, right, span); } - match (&operands[0], &operands[1]) { - (Operand::Value(a), Operand::Function(f)) if matches!(op, Before) => bound_call(a, f, left.is_some(), right, span), - (Operand::Function(f), Operand::Value(a)) if matches!(op, After) => { - if left.is_some() { return Err(Error::new(ErrorKind::Syntax, ONE_ARGUMENT)); } - f.call_with(Some(right), a, span) - } - (Operand::Function(f), ranks) if matches!(op, Rank) => rank(f, &computed(ranks, x, y, span)?, x, y, span), - (Operand::Function(f), spec) if matches!(op, Axis) => along_axes(f, &computed(spec, x, y, span)?, x, y, span), - (Operand::Function(f), Operand::Function(g)) => match op { - Valences => { if left.is_some() { g.call_with(left, right, span) } else { f.call_with(None::<&Value>, right, span) } } Product => inner(f, g, x, y, span), - Atop => { let gy = g.call_with(left, right, span)?.array(span)?; f.call_with(None::<&Value>, gy, span) } Over => over(f, g, x, y, span), - // `X f↢g Y` is `X f(g Y)`. Monadic, it's `Y f(g Y)`, so `g` borrows `Y` and `f` takes it. - After => match left { - Some(l) => { let gy = g.call_with(None::<&Value>, right, span)?.array(span)?; f.call_with(Some(l), gy, span) } - None => { let gy = g.call_array(None, y, span)?; f.call_with(Some(right), gy, span) } - }, - // `X f↣g Y` is `(f X)g Y`. Monadic, it's `(f Y)g Y`, so `f` borrows `Y` and `g` takes it. - Before => match left { - Some(l) => { let fx = f.call_with(None::<&Value>, l, span)?.array(span)?; g.call_with(Some(fx), right, span) } - None => { let fx = f.call_array(None, y, span)?; g.call_with(Some(fx), right, span) } - }, - _ => unreachable!() - }, - _ => Err(Error::domain("invalid operator operands")), - } -} - -/// The error for a left argument given to a function whose argument is bound, as `2↣×`'s is. -const ONE_ARGUMENT: &str = "a bound function takes one argument"; - -/// `a↣f`'s call: `f` on `right`, with `a` as its left argument. It takes one argument. -// Composition calls this for each bound function. Out of line, even with `#[inline]`, it cost `{(⍵)+⊢ ⍵}` about 50 instructions a call. -#[inline(always)] -fn bound_call(a: &Value, f: &Function, left: bool, right: impl Argument, span: &Span) -> Result { - if left { return Err(Error::new(ErrorKind::Syntax, ONE_ARGUMENT)); } - f.call_with(Some(a), right, span) -} - -/// A fork's call: its right tine reads the arguments first, then its left tine takes them, then its middle tine takes both results. -// Out of line, as `composition` is, so that `dispatch` adds nothing to the stack frame of every BPL call. -#[inline(never)] -pub(super) fn fork_call(tines: &[Function; 3], left: Option, right: R, span: &Span) -> Result { - let y = tines[2].call_array(left.as_ref().map(L::value), right.value(), span)?; - let x = tines[0].call_with(left, right, span)?.array(span)?; - tines[1].call_with(Some(x), y, span) -} - -/// A part of a train, as `train_call` calls it: a function tine, or an array tine bound as the left argument of the function after it (`a↣f`). -enum TrainPart<'t> { Function(&'t Function), Bound(&'t Value, &'t Function) } - -impl TrainPart<'_> { - fn call(&self, left: Option, right: impl Argument, span: &Span) -> Result { - match self { Self::Function(f) => f.call_with(left, right, span), Self::Bound(a, f) => bound_call(a, f, left.is_some(), right, span) } - } -} - -/// Whether `train_call` can call the train with tines `tines` by its tines. It can't in three cases, where the caller builds the train and calls it. The first is while a transformation or `•explain` runs, because each must see the train's own call. The second is an array tine that doesn't bind a function. The third is a train that may hold a fast combination (`rewrite::may_combine`), which only the train's function computes. -pub(super) fn calls_tines(tines: &[Tine]) -> bool { - !session().transforming() && !crate::explain::reporting() - && matches!(tines.last(), Some(Tine::Function(_))) && !super::rewrite::may_combine(tines) - && !tines.windows(2).any(|w| matches!(w, [Tine::Array(_), Tine::Array(_)])) -} - -/// How many tines of the train with tines `tines` read its arguments: its last part, and the left tine of each fork (`Function::train_shape`). -pub(super) fn train_readers(tines: &[Tine]) -> usize { - Function::train_shape(Function::train_parts(tines, |_| (), |_, ()| ()), &mut (), |_, ()| Ok(1), |_, (), (), n| Ok(n + 1), |_, (), n| Ok(n)).unwrap_or(1) + &ENTRIES[match kind { Reduce => 0, ReduceFirst => 1, Scan => 2, ScanFirst => 3, Each => 4, Commute => 5, Inverse => 6, + Outer => 7, Key => 8, Derivative => 9, Optimize => 10, Bitwise => 11, Mark(_) => 12, Under => 13, Atop => 14, + Over => 15, Rank => 16, Axis => 17, Stencil => 18, Dot => 19, Power => 20, Before => 21, After => 22, + Valences => 23, Agenda => 24, Field(_) => 25, Index => 26, Framed { .. } => 27, JvpStep { .. } => 28, VjpStep { .. } => 29 }] } -/// The call of the train with tines `tines`, which `calls_tines` accepts, on `left` and `right`, made as the train's function would make it, without building the function (`Function::train_shape`). A compiled statement calls a group's train this way (`tree.rs`). The train reads its arguments in its last part, then in the left tine of each fork: `readers` reads in all, as `train_readers` counts them. Each read borrows the arguments, apart from the last, which takes them. -pub(super) fn train_call(tines: &[Tine], readers: usize, left: Option, right: R, span: &Span) -> Result { - let parts = Function::train_parts(tines, TrainPart::Function, |a, part| match part { - TrainPart::Function(f) => TrainPart::Bound(a, f), TrainPart::Bound(..) => unreachable!("calls_tines declines array tines side by side") - }); - // The call counts toward the depth limit, as the train's function's own call would. - session().descend(span)?; - let result = if readers == 1 { - // The last part is the only reader. - Function::train_shape(parts, &mut (), |_, part| part.call(left, right, span), |_, _, _, _| unreachable!("a train with one reader has no fork"), - |_, f, inner| f.call(None::<&Value>, inner.array(span)?, span)) - } else { - // The reads still to come, and the arguments, which the last read takes. - let mut context = (readers, Some((left, right))); - let read = |(readers, arguments): &mut (usize, Option<(Option, R)>), part: TrainPart| { - *readers -= 1; - if *readers == 0 { let (x, y) = arguments.take().expect("the last read takes the arguments once"); return part.call(x, y, span); } - let (x, y) = arguments.as_ref().expect("the last read comes last"); - part.call(x.as_ref().map(L::value), y.value(), span) - }; - Function::train_shape(parts, &mut context, read, - |context, g, f, inner| { let y = inner.array(span)?; let x = read(context, g)?.array(span)?; f.call(Some(x), y, span) }, - |_, f, inner| f.call(None::<&Value>, inner.array(span)?, span)) - }; - session().ascend(); - result -} - -/// The case that the unit `index` picks from `len` cases: a whole number, which counts back from the end where it's negative. -pub(super) fn agenda_index(index: &Value, len: usize) -> Result { - let Plain::Number(n) = index.observed()? else { return Err(Error::domain("agenda index must be numeric")) }; - crate::primitive::position(n, len) -} - -/// `selector⍚cases`. A unit selector picks one case, which takes the whole arguments. Any other selector picks a case for each cell (`Cells::chosen`), or, where a select can run both cases on every item, selects their results item by item (`selected`). -fn agenda(operands: &[Operand; 2], left: Option, right: R, text: impl FnOnce() -> String, span: &Span) -> Result { - let [selector, Operand::Value(fs)] = operands else { unreachable!() }; - let (x, y) = (left.as_ref().map(L::value), right.value()); - let index = computed(selector, x, y, span)?; - if !index.is_unit() { - let cases: Vec<_> = fs.elements().map(|f| match f { Value::Function(f) => f, _ => unreachable!() }).collect(); - if let [no, yes, ..] = &cases[..] { - if let Some(result) = selected(&index, [Some(no), Some(yes)], x, y, text, span)? { return Ok(Bound::from(result)); } - } - return Ok(Bound::from(Cells::new(&index, x, y)?.chosen(&index, &cases, x, y, span)?)); +impl OperatorKind { + pub(crate) fn all() -> impl Iterator { + use OperatorKind::*; + [Reduce, ReduceFirst, Scan, ScanFirst, Each, Commute, Inverse, Outer, Key, Derivative, Optimize, Bitwise, Under, + Atop, Over, Rank, Axis, Stencil, Dot, Power, Before, After, Valences, Agenda].into_iter() } - let Value::Function(f) = fs.at(agenda_index(&index, fs.len())?) else { unreachable!() }; - f.call_with(left, right, span) + pub(crate) fn from_glyph(glyph: char) -> Option { Self::all().find(|kind| kind.info().glyph.chars().eq(std::iter::once(glyph))) } } -/// An operand that gives an array: the array itself, or a function's result on the arguments, as `⍤`, `⍠`, `⌺` and `⍚` take them. -fn computed(operand: &Operand, left: Option<&Value>, right: &Value, span: &Span) -> Result { - match operand { Operand::Value(a) => Ok(a.clone()), Operand::Function(g) => g.call_array(left, right, span) } +fn spelling(kind: &OperatorKind) -> String { match kind { OperatorKind::Mark(mark) => mark.marker().into(), _ => kind.info().glyph.into() } } +fn operand_kind(kind: &OperatorKind, position: usize, operand: &FunctionOperand) -> OperandKind { + if position != 1 || !matches!(kind, OperatorKind::Rank | OperatorKind::Axis | OperatorKind::Stencil) { return OperandKind::Data; } + let Some(value) = operand.stored() else { return OperandKind::Data; }; + if value.keys(0).is_some_and(|keys| keys.position("fill").is_some()) { OperandKind::Data } else { OperandKind::Structure } } -/// `f@g`: `f` applied to the selection that `g` makes from the argument, then written back in its place. An array `f` replaces the selection. An array `g` lists positions along the leading axis, as `[g]⌷` reads them, and a selection of no items leaves the argument as it is, without calling `f`. `[p]` runs `f` on the cells where `p` of the argument is 1 (`masked`). With positions or `[p]`, a left argument is laid over the right argument (`Laid`), and a unit goes whole to `f`. -fn under(operands: &[Operand; 2], left: Option<&Value>, right: &Value, text: impl FnOnce() -> String, span: &Span) -> Result { - let [Operand::Function(f), g] = operands else { unreachable!() }; - let positions = match g { - Operand::Function(g) => return under_function(f, g, left, right, span), - Operand::Value(a) => match predicate(a) { - Some(p) => return Ok(Bound::from(masked(f, &p.call_array(None, right, span)?, left, right, text, span)?)), None => a - }, - }; - let selection = crate::primitive::at_indices(right, positions, span)?; - if selection.targets.len() == 0 { return Ok(Bound::from(right.clone())); } - let x = match left { - Some(x) if !x.is_unit() => { - let x = Laid::new(x, &right.layout().axes(0..1))?.aligned(span)?; - Some(crate::primitive::at_indices(&x, positions, span)?.read(&x, None, span)?) - } - _ => None, - }; - let values = f.call_with(x.map(Cow::Owned).or(left.map(Cow::Borrowed)), selection.read(right, None, span)?, span)?.array(span)?; - Ok(Bound::from(selection.write(right, &values, span)?)) +fn function(value: &Value) -> Result<&Function> { match value { Value::Function(f) => Ok(f), _ => Err(Error::domain("operator needs a function operand")) } } +fn applied(f: &Function) -> (&OperatorKind, &[FunctionOperand]) { + match f.node() { FunctionNode::Applied { op: Operator::Kind(kind), operands } => (kind, operands), _ => unreachable!("an operator entry runs its own application") } } - -/// The function of `[p]`, a one-item vector that holds a function. -fn predicate(a: &Value) -> Option { if a.shape() != [1] { return None; } match a.at(0) { Value::Function(p) => Some(p), _ => None } } - -/// `f@[p]`, where `p` gives the mask `selector`: `f` runs once on the cells that the mask marks 1 (`Cells::marked`), and its results go back in their places under assignment's rule. Every other cell keeps its value, including one marked NaN, or whose key the mask lacks. A unit mask marks the whole argument or none of it. Where a select can run `f` on every item, it does (`selected`), and `•explain` names the call with `text`. -fn masked(f: &Function, selector: &Value, left: Option<&Value>, right: &Value, text: impl FnOnce() -> String, span: &Span) -> Result { - if selector.shape().is_empty() { - let [k] = crate::primitive::positions(selector, 2)?[..] else { unreachable!("a unit has one position") }; - if k != 1 { return Ok(right.clone()); } - let every = Selection { frame: ResultFrame::Array(right.layout().clone()), targets: Targets::Offsets((0..right.len()).collect()) }; - return every.write(right, &f.call_array(left, right, span)?, span); - } - if let Some(result) = selected(selector, [None, Some(f)], left, right, text, span)? { return Ok(result); } - let cells = Cells::new(selector, left, right)?; - let rows = cells.marked(selector)?; - if rows.is_empty() { return Ok(right.clone()); } - let selection = cells.framed.selection(&rows)?; - let values = cells.case(f, &rows, left, right, span)?; - selection.write(right, &values, span) -} - -/// The costs, in copies per item, that decide between a select and a gather of each case's cells (`Cells`). A gather pays `GATHERED` for every item, and `PICKED` plus the case's cost for each item that picks a case that computes. A select pays `CHOSEN` for every item, or `FUSED` where fusion runs it with the comparison that gives its mask, plus the cost of every case that computes. `GATHERED`, `PICKED` and `CHOSEN` come from a fit on 19 October 2026 on this Mac, an Apple M5 Max, to `-@[p] s` and `*@[p] s` on 65,536 floats with 10% to 90% of items picked. They put the crossover at about 22% picked for `-` and 55% for `*`. `FUSED` comes from `-@[p<]x` and `*@[p<]x` on a million floats on 20 October 2026, where a fused select cost 2.0 copies plus its kernel for `-`, and 2.3 for `*`. It puts the crossover at about 10% picked for `-` and 47% for `*`. CI's timings can refine all four. -const GATHERED: f64 = 2.3; -const PICKED: f64 = 7.3; -const CHOSEN: f64 = 3.1; -const FUSED: f64 = 2.1; -/// The items of a pending mask that `selected` reads to estimate the share of items that pick each case. Fusion computes only those items (`fusion::walk_through`). The rest of the mask stays pending for the select. -const SAMPLE: usize = 256; - -/// `cases[1]` on the items where the Boolean `mask` is 1 and `cases[0]` where it's 0, each run on every item of `right` and then chosen item by item (`fusion::select`), in place of gathering each case's cells (`Cells`). A case of `None` keeps `right`'s items, as `@[p]` keeps the items it doesn't select. A case then also runs on items that didn't pick it, which can't show where it never fails and gives the argument's kind (`select_cost`). `None` leaves the call to `Cells`: for a left argument, an empty argument, a mask and cases that `plan::select` refuses, such as a mask that isn't Booleans with the argument's layout, an argument that isn't floats of 32 or 64 bits, an argument that `∂` tracks, whose gradient a select could make NaN where a case's slope is infinite on an item that didn't pick it, or a case that a select can't run. A select also has to cost less than the gather, by its cases' costs and the share of items that pick each (`GATHERED`). The share is counted from a stored mask, and estimated from a sample of a comparison that fusion runs inside the select (`share`). `•explain` reports a select as a fast combination, named by `text`. -fn selected( - mask: &Value, cases: [Option<&Function>; 2], left: Option<&Value>, right: &Value, text: impl FnOnce() -> String, span: &Span -) -> Result, Error> { - use rustymath::buffer::{DType, FloatWidth}; - let Some(DType::Float(width @ (FloatWidth::F32 | FloatWidth::F64))) = right.dtype() else { return Ok(None) }; - if left.is_some() || right.is_empty() || **mask.layout() != **right.layout() || crate::transform::carried(left, right) { return Ok(None); } - let [Some(no), Some(yes)] = cases.map(|f| f.map_or(Some(0), |f| select_cost(f, width))) else { return Ok(None) }; - // Each case holds a constant's kind and shape, or else the argument's, which every other case keeps (`select_cost`). - let [no_case, yes_case] = cases.map(|f| crate::plan::Operand::Held(f.and_then(Function::as_constant).unwrap_or(right))); - let Ok(plan) = crate::plan::select(&crate::plan::Operand::Held(mask), &yes_case, &no_case) else { return Ok(None) }; - let keeps = |f: Option<&Function>| f.is_none_or(|f| matches!(f.as_primitive(), Some(Primitive::Identity(_)))); - let fused = crate::fusion::defers_select(mask, right.layout()); - let ones = share(mask, fused); - let picked = |f: Option<&Function>, share: f64, copies: u32| if keeps(f) { 0.0 } else { share * (PICKED + f64::from(copies)) }; - // `share` counts, and so computes, a mask of at most `SAMPLE` items, which can then no longer run inside the select. - let chosen = if fused && mask.len() > SAMPLE { FUSED } else { CHOSEN }; - if chosen + f64::from(no + yes) >= GATHERED + picked(cases[0], 1.0 - ones, no) + picked(cases[1], ones, yes) { return Ok(None); } - let items = |f: Option<&Function>| -> Result { - let Some(f) = f.filter(|_| !keeps(f)) else { return Ok(right.clone()) }; - if let Some(a) = f.as_constant() { return Ok(a.clone()); } - f.call_array(None, right, span) - }; - let [no, yes] = cases; - let (y, x) = (items(no)?, items(yes)?); - if rustymath::events::running() { crate::explain::fast(rustymath::events::Route::MaskedSelect, text()); } - Ok(Some(crate::fusion::select(mask, &x, &y, right.layout(), &plan))) +fn argument_sources(origin: &Origin<&[Value]>) -> Result>> { + Ok(Function::argument_ranges(origin)?.into_iter().flatten().map(|range| Some(range.start)).collect()) } - -/// The share of `mask`'s items that are 1, counted, or estimated from `SAMPLE` items where `sampled`. An odd stride keeps the sample from lining up with a mask that repeats with a period of a power of two. -fn share(mask: &Value, sampled: bool) -> f64 { - let n = mask.len(); - let k = if sampled { SAMPLE.min(n) } else { n }; - let sample = (k < n).then(|| { - crate::explain::uncounted(|| { - crate::primitive::walked(mask, crate::array::Layout::from(vec![k]), 0, &[rustymath::gather::Steps::along(k, (n / k - 1) | 1)], None) - }).expect("a sample fits its layout") - }); - let crate::array::Items::Compact(rustymath::buffer::View::Booleans(bits)) = sample.as_ref().unwrap_or(mask).as_items() else { - unreachable!("a Boolean mask is compact") - }; - bits.iter().filter(|&&b| b).count() as f64 / k as f64 -} - -/// What a select pays to run `f` on every item of an array of floats of `width`, in copies per item (`Facts::cost`), or `None` where it can't run `f`. A select can run `f` where it never fails, and gives one float of that width for each item, from that item alone. That holds for `⊢`, for a constant float of exactly `width`, and for trains built from those and from primitives whose float kernel never fails and gives floats (`pervasive::kernel`). A constant beside such a primitive can be narrower, because the primitive's result takes the argument's width. Any other constant would make the result's kind depend on which items pick the case. -fn select_cost(f: &Function, width: rustymath::buffer::FloatWidth) -> Option { - use rustymath::buffer::DType; - let real = |a: &Value| match a { Value::Number(n) if !n.is_exact() && n.as_bool().is_none() && n.as_float().is_some() => a.float_width(), _ => None }; - let numeric = Numeric::current(); - let kernel = |p: Primitive, dyadic: bool| { - let copies = u32::from(p.facts(dyadic).cost); - crate::pervasive::kernel(p, dyadic, [None; 2], DType::Float(width), numeric).is_some_and(|k| k.total && matches!(k.kind, DType::Float(_))) - .then_some(copies) - }; - let dyad = |g: &Function| g.as_primitive().and_then(|p| kernel(p, true)); - if let Some(a) = f.as_constant() { return (real(a) == Some(width)).then_some(0); } - if let Some((parts, bound)) = f.called_parts(false) { - if bound.is_some_and(|a| !real(a).is_some_and(|w| w <= width)) { return None; } - return parts.iter().try_fold(0, |cost, &(g, dyadic)| Some(cost + if dyadic { dyad(g)? } else { select_cost(g, width)? })); - } - match f.node() { FunctionNode::Primitive(Primitive::Identity(_)) => Some(0), FunctionNode::Primitive(p) => kernel(*p, false), _ => None } +fn value_call(value: &Value, origin: &Origin<&[Value]>, args: &[Value], from: &[Option]) -> Result { + match value { Value::Function(f) => f.call_bound(origin, args, from), value => Ok(value.clone()) } } -/// A case's result, classified once: one cell for each cell that picks the case, or one item that fills them all, as `@` fills a selection. -enum Made { Cells(Value), Fill(Value) } - -impl Made { - /// `values` from a case whose cells have the list frame `rows`. A singleton fills; any other result aligns by key and needs one result per row. - fn new(values: Value, rows: &crate::array::Layout) -> Result { - if values.is_singleton() { return Ok(Self::Fill(values)); } - let values = crate::keyed::reorder(&values, &rows.all_keys(), true).described("case result does not supply selected keys")?; - if values.shape().first() == rows.shape().first() { return Ok(Self::Cells(values)); } - Err(Error::new(Kind::Length, "a case needs one result for each cell that picks it")) - } - fn value(&self) -> &Value { match self { Self::Cells(v) | Self::Fill(v) => v } } +fn dispatch_in(parent: &Origin<&[Value]>, operation: Operation, args: &[Value], from: &[Option]) -> Result { + let origin = projected_call(parent, operation, args, from)?; + Operation::dispatch(origin.reborrow())?.assemble() } -/// The cells of `right` along the axes that a selector's axes pair with, for `⍚` with a selector for each cell and for `@[p]`. The selector's axes pair with the argument's by name and then from the front. The selector, and a left argument that isn't a unit, are each laid over the frame as agreement lays them (`lookup`, `Laid`). Cells are read, and written, where they lie (`Framed`): no axis of either argument moves. -struct Cells { framed: Framed, axes: Vec, lookup: Mapping, x: Option, len: usize } - -impl Cells { - fn new(selector: &Value, left: Option<&Value>, right: &Value) -> Result { - let axes = rustymath::agreement::Agreement::paired_axes(right.layout(), selector.layout(), true); - if axes.iter().any(|&a| a >= right.shape().len()) { return Err(Error::new(Kind::Rank, "the selector has an axis that the argument lacks")); } - let framed = Framed::new(right.layout(), &axes)?; - let frame = framed.frame(); - let len = frame.count(); - let lookup = lookup(selector.layout(), &frame, len, "the selector doesn't agree with the argument")?; - let x = left.filter(|x| !x.is_unit()).map(|x| Laid::new(x, &frame)).transpose()?; - Ok(Self { framed, axes, lookup, x, len }) - } - - /// The frame positions that `selector` marks 1, in order. A Boolean mask that lies in frame order goes straight to `⍸`'s kernel. Any other mask decodes as `picked` decodes it. - fn marked(&self, selector: &Value) -> Result, Error> { - if let (Mapping::Linear(1), crate::array::Items::Compact(rustymath::buffer::View::Booleans(mask))) = (&self.lookup, selector.as_items()) { - let mut rows = Vec::new(); - rustymath::gather::positions(&mut rows, mask, mask.iter().map(|&b| usize::from(b)).sum(), |j| j as u32); - return Ok(rows); - } - Ok(self.picked(selector, 2)?.each().nth(1).unwrap_or_default().to_vec()) - } - - /// The frame positions grouped by the case of `cases` that each picks (`picked`). - fn picked(&self, selector: &Value, cases: usize) -> Result { - picked(selector, (!matches!(self.lookup, Mapping::Linear(1))).then_some(&self.lookup), self.len, cases) - } - - /// The left argument's cells at frame positions `rows`, or `None` where the left argument is a unit or absent. - fn left_cells(&self, rows: &[u32], span: &Span) -> Result, Error> { self.x.as_ref().map(|x| x.cells(rows, span)).transpose() } +/// A lower-level operation inherits the enclosing call's metadata, with explicit positional projections. - /// The result of case `f` on the cells of `right` at frame positions `rows`. `f` gets the left argument's cells at the same positions, or `left` whole where it's a unit. A constant case doesn't look at its cells. - fn case(&self, f: &Function, rows: &[u32], left: Option<&Value>, right: &Value, span: &Span) -> Result { - if let Some(a) = f.as_constant() { return Ok(a.clone()); } - let xs = self.left_cells(rows, span)?; - f.call_with(xs.map(Cow::Owned).or(left.map(Cow::Borrowed)), self.framed.read(right, rows, span)?, span)?.array(span) - } - /// `⍚` with a selector for each cell. Each case runs once, on the list of all the cells that pick it (`case`), and gives one result for each of them or one item for all of them (`Made`). A case that no cell picks doesn't run. A cell that picks no case gets the fill. Numbers keep their kinds. A case of `⊢` doesn't run: when every other result has the argument's cells, the result starts from the argument, so `⊢`'s cells stay where they are. Otherwise `⊢`'s cells join the other results. - fn chosen(&self, selector: &Value, cases: &[Function], left: Option<&Value>, right: &Value, span: &Span) -> Result { - if self.len == 0 { return self.no_cells(selector, cases, left, right, span); } - let groups = self.picked(selector, cases.len())?; - let picked: Vec<&[u32]> = groups.each().collect(); - let keeps = |k: usize| cases[k].as_primitive() == Some(Primitive::Identity(false)); - let mut made = Vec::new(); - for (k, f) in cases.iter().enumerate() { - let rows = picked[k]; - if rows.is_empty() || keeps(k) { continue; } - let values = self.case(f, rows, left, right, span)?; - let frame = listed(&self.framed.frame(), rows, &Default::default())?; - made.push((k, Made::new(values, &frame)?)); - } - let kept: Vec = (0..cases.len()).filter(|&k| keeps(k) && !picked[k].is_empty()).collect(); - // The first case that some cell picks gives the fill. A kept case's cells are the argument's. - let fill = match (kept.first(), made.first()) { - (Some(&k), first) if first.is_none_or(|(j, _)| k < *j) => right.prototype(), (_, Some((_, m))) => m.value().prototype(), _ => right.prototype() - }; - let cell = self.framed.cells(); - let start = (!kept.is_empty() && made.iter().all(|(_, m)| matches!(m, Made::Fill(_)) || m.value().shape()[1..] == *cell.shape())).then_some(right); - if start.is_none() { - for &k in &kept { made.push((k, Made::Cells(self.framed.read(right, picked[k], span)?))); } - made.sort_by_key(|&(k, _)| k); - } - self.assembled(&picked, picked.get(cases.len()).copied().unwrap_or_default(), &made, fill, start) - } - - /// The results of the cases as one array, laid out as `placed` gives it. `none` lists the cells that pick no case, which get `fill`. A fill result fills each cell of its case. Results whose cells differ in shape are padded, as Rank pads them, and a fill result spreads across the shape that the others pad to. `start`, where a case of `⊢` keeps its cells and every result has the argument's cells, is the argument, which the result starts from. Numbers keep their kinds. - fn assembled(&self, picked: &[&[u32]], none: &[u32], made: &[(usize, Made)], fill: Value, start: Option<&Value>) -> Result { - let cells = || { - made.iter().filter_map(|(k, m)| match m { Made::Cells(v) => Some((*k, v)), Made::Fill(_) => None }) - }; - let mut layouts: Vec<_> = cells().map(|(_, v)| v.layout().axes(1..v.shape().len())).collect(); - if start.is_some() || layouts.is_empty() { layouts.push(self.framed.cells()); } - let cell = crate::array::Layout::assembled(layouts.iter()).described("invalid result cells")?; - let shape = cell.shape(); - if cells().any(|(_, v)| v.shape()[1..] != *shape) { - let mut items = vec![fill.clone(); self.len]; - for (k, m) in made { - let rows = picked[*k]; - match m { - Made::Cells(v) => { - let cells = v.cells(v.shape().len() - 1).and_then(|c| c.framed()).described("invalid result")?; - for (j, &i) in rows.iter().enumerate() { items[i as usize] = cells.at(j); } - } - Made::Fill(v) => { - let n = cell.count(); - let mut spread = Gather::items(n); - spread.fill(&v.at(0), n); - let item = spread.finish(cell.clone(), fill.clone()).described("invalid result")?; - for &i in rows { items[i as usize] = item.clone(); } - } +fn operator_call(f: &Function, origin: Origin<&[Value]>) -> Result { + use OperatorKind::*; + let (kind, _) = applied(f); + let first = f.operand(&origin, 0); + match kind { + Framed { axes, atoms } => return crate::batch::framed(function(&first)?, *axes, atoms, origin.reborrow()), + JvpStep { packing } => return crate::derivative::jvp_step(function(&first)?, packing, origin.reborrow()), + VjpStep { direction } => return crate::derivative::vjp_step(function(&first)?, *direction, origin.reborrow()), + Derivative => return crate::derivative::differentiate(function(&first)?, origin.reborrow()), + _ => {} + } + let args = Function::body_arguments(&origin)?; + let from = argument_sources(&origin)?; + let right = args.last().ok_or_else(|| Error::domain("function needs an argument"))?; + let left = (args.len() == 2).then(|| &args[0]); + let second = || f.operand(&origin, 1); + match kind { + Commute => match first.as_ref() { Value::Function(g) => { + let values = [right.clone(), left.unwrap_or(right).clone()]; + let positions = [from.last().copied().flatten(), from.first().copied().flatten()]; + g.call_bound(&origin, &values, &positions) + }, value => Ok(value.clone()) }, + Before => { + let g = second(); + let gx = match first.as_ref() { Value::Function(_) => value_call(&first, &origin, &[left.unwrap_or(right).clone()], &[from[0]])?, + value if left.is_none() => value.clone(), _ => return Err(Error::domain("a bound function takes one argument")) }; + value_call(&g, &origin, &[gx, right.clone()], &[None, *from.last().unwrap()]) + } + After => { + let h = second(); + if left.is_some() && !matches!(h.as_ref(), Value::Function(_)) { return Err(Error::domain("a bound function takes one argument")); } + let hy = value_call(&h, &origin, &[right.clone()], &[*from.last().unwrap()])?; + value_call(&first, &origin, &[left.unwrap_or(right).clone(), hy], &[from[0], None]) + } + Atop => { + let hy = value_call(&second(), &origin, &args, &from)?; + value_call(&first, &origin, &[hy], &[None]) + } + Over => { + let h = second(); + let values = args.iter().zip(&from).map(|(arg, source)| value_call(&h, &origin, &[arg.clone()], &[*source])).collect::>>()?; + value_call(&first, &origin, &values, &vec![None; values.len()]) + } + Valences => { let selected = if left.is_some() { second() } else { first }; value_call(&selected, &origin, &args, &from) } + Power => loop_call(&origin, function(&first)?, &second(), &args, &from, None), + Reduce | ReduceFirst | Scan | ScanFirst => fold_call(&origin, function(&first)?, kind, &args, &from), + Rank => rank_call(&origin, function(&first)?, &second(), &args, &from), + Each => each_call(&origin, function(&first)?, &args, &from), + Outer => outer_call(&origin, function(&first)?, &args, &from), + Key => key_call(&origin, function(&first)?, &args, &from), + Stencil => stencil_call(&origin, function(&first)?, &second(), &args, &from), + Dot => product_call(&origin, function(&first)?, function(&second())?, &args, &from), + Axis => axis_call(&origin, function(&first)?, &second(), &args, &from), + Under => under_call(&origin, &first, &second(), &args, &from), + Agenda => agenda_call(&origin, &first, &second(), &args, &from), + Inverse => inverse_call(&origin, function(&first)?, &args, &from), + Mark(mark) => { + if let Value::Function(g) = first.as_ref() { + let primitive = g.as_primitive().or_else(|| match g.node() { + FunctionNode::Applied { op: Operator::Kind(Inverse), .. } => match g.operand(&origin, 0).as_ref() { + Value::Function(inner) => inner.as_primitive().and_then(Primitive::inverted), _ => None, + }, + _ => None, + }); + if let Some(primitive) = primitive.and_then(|primitive| primitive.marked(*mark, left.is_some())) { + return Function::primitive(primitive).call_bound(&origin, &args, &from); } } - // Assembly pads the data; result metadata comes from the case cells, not the scalar no-case placeholders. - let result = - self.framed.frame().assemble(&items, &fill).and_then(|v| v.with_layout(self.framed.frame().concat(&cell))).described("invalid result")?; - let (_, positions) = placed(&self.framed, &self.axes, &cell)?; - if rustymath::gather::counts_up(&positions) { return Ok(result); } - return crate::primitive::transpose_axes(fronted(&self.axes, self.framed.layout.shape().len()), &result); - } - let (layout, positions) = placed(&self.framed, &self.axes, &cell)?; - let target = Framed::new(&layout, &positions)?; - let width = cell.count(); - let mut data = Gather::items(self.len * width); - // Where every cell picks a case, the case that fills the most cells with one item fills the whole array first. - let base = if none.is_empty() && start.is_none() { - made.iter().filter(|(_, m)| matches!(m, Made::Fill(_))).max_by_key(|(k, _)| picked[*k].len()).map(|(k, _)| *k) - } else { None }; - match start { - Some(argument) => data.walk(argument, 0, &[rustymath::gather::Steps::Stride { start: 0, len: argument.len(), step: 1 }], None), - None => data.fill(&made.iter().find(|(k, _)| Some(*k) == base).map_or_else(|| fill.clone(), |(_, m)| m.value().at(0)), self.len * width), + let value = value_call(&first, &origin, &args, &from)?; + dispatch_in(&origin, Operation::Convert(*mark), &[value], &[None]) } - let mut write = |rows: &[u32], values: &Value| data.scatter(target.targets(rows).writes().expect("cells' targets are offsets or a walk"), values); - for (k, m) in made.iter().filter(|(k, _)| Some(*k) != base) { write(picked[*k], m.value()) } - if start.is_some() && !none.is_empty() { write(none, &fill) } - data.finish(layout, fill).described("invalid result") - } - - /// `chosen` for a frame with no cells. As Each calls its operand on prototypes, the case that the selector's fill picks runs on the empty list of cells (`on_prototypes`). The result is empty, with that case's cells, laid out as `placed` gives it. - fn no_cells(&self, selector: &Value, cases: &[Function], left: Option<&Value>, right: &Value, span: &Span) -> Result { - let k = match crate::primitive::positions(&selector.prototype(), cases.len())?[..] { - [k] if k != rustymath::gather::Steps::FILL => k, _ => return Err(Error::domain("the selector's fill picks no case")) - }; - let xs = self.left_cells(&[], span)?; - let result = on_prototypes(&cases[k], xs.as_ref().or(left), &self.framed.read(right, &[], span)?, span)?.array(span)?; - let cell = if result.is_singleton() { self.framed.cells() } - else if result.shape().first() == Some(&0) { result.layout().axes(1..result.shape().len()) } - else { Default::default() }; - placed(&self.framed, &self.axes, &cell)?.0.collect(Vec::new(), result.prototype()).described("invalid result") - } -} - -/// What a guard on a batched predicate gives (`guard`): the side that every cell takes, or the batch that joins the two sides' results. -pub(super) enum Guarded { Unanimous(bool), Joined(Value) } - -/// A guard whose predicate gives each cell of batched run `level` a Boolean of its own, run as `⍚` runs a selector for each cell (`Cells`). `truths` holds the Booleans, laid out as the run's frame (`batch::truths`). Where the cells take both sides, `side` runs each side once, on the cells that take it, given whether the predicate holds there, the cells' positions in the frame and their layout as a list. It gives a batch over those cells or a constant (`batch::whole_over`), and the two results join into one batch over the frame. A side whose cells differ in layout from the other side's stops the run, because a batch's cells share one layout. -pub(super) fn guard(truths: &Value, level: u64, mut side: impl FnMut(bool, &[u32], &crate::array::Layout) -> Result) -> Result { - use crate::transform::{Cause, Stop}; - let cells = Cells::new(truths, None, truths)?; - let groups = cells.picked(truths, 2)?; - let picked: Vec<&[u32]> = groups.each().collect(); - if let Some(k) = (0..2).find(|&k| picked[k].is_empty()) { return Ok(Guarded::Unanimous(k == 0)); } - let mut made = Vec::new(); - for k in [0, 1] { - let frame = listed(&cells.framed.frame(), picked[k], &Default::default())?; - let result = side(k == 1, picked[k], &frame)?; - // A constant atom fills its cells, as a constant case of `⍚` does, so a weak number takes the other side's width (`Gather`). - if result.is_atom() && crate::batch::constant(&result, level) { made.push((k, Made::Fill(result))); continue; } - let whole = crate::batch::whole_over(result, level, &frame).ok_or_else(|| Error::from(Stop::new(level, Cause::Rule)))?; - made.push((k, Made::Cells(whole))); - } - let cell = |m: &Made| m.value().layout().axes(1..m.value().shape().len()); - if cell(&made[0].1) != cell(&made[1].1) { return Err(Stop::new(level, Cause::Rule).into()); } - let fill = made[0].1.value().prototype(); - let whole = cells.assembled(&picked, &[], &made, fill, None)?; - crate::batch::result(level, whole, truths.shape().len(), || true, Stop::new(level, Cause::Rule)).map(Guarded::Joined) -} - -/// The cells at frame positions `rows` of `array`, whose first `frame` axes are its frame, as a list, as a guard case reads a batch's whole (`batch::read_cells`). -pub(super) fn cells_at(array: &Value, frame: usize, rows: &[u32], span: &Span) -> Result { - Framed::new(array.layout(), &(0..frame).collect::>())?.read(array, rows, span) -} - -/// The cells of an array along its frame axes. `layout` and `steps` put the frame's axes first and then a cell's, as `⍉` would, but no item moves. The first `rank` steps reach each cell's start; the rest read its items. -struct Framed { layout: crate::array::Layout, rank: usize, steps: Vec } - -impl Framed { - fn new(layout: &crate::array::Layout, frame: &[usize]) -> Result { - let plan = rustymath::structure::transpose(layout, &rustymath::search::inverse_permutation(fronted(frame, layout.shape().len()).into_iter()))?; - Ok(Self { layout: plan.layout.into(), rank: frame.len(), steps: plan.steps }) - } - /// The frame's axes, with their names and keys. - fn frame(&self) -> crate::array::Layout { self.layout.axes(0..self.rank) } - /// The cells' axes, with their names and keys. - fn cells(&self) -> crate::array::Layout { self.layout.axes(self.rank..self.layout.shape().len()) } - /// The starts of `rows`' cells through `lookup`, or fills where it finds none. Interpret the frame's steps once, outside the loop. In frame order, the row positions themselves are the starts; another single stride scales them without coordinate division. - #[inline] - fn starts(&self, rows: &[u32], lookup: impl Fn(usize) -> Option) -> Vec { - use rustymath::gather::Steps; - match self.steps[..self.rank] { - [Steps::Stride { start: 0, step: 1, .. }] => rows.iter().map(|&j| lookup(j as usize).unwrap_or(Steps::FILL)).collect(), - [Steps::Stride { start, step, .. }] => { - rows.iter().map(|&j| lookup(j as usize).map(|j| start.wrapping_add_signed(step * j as isize)).unwrap_or(Steps::FILL)).collect() - } - ref steps => rows.iter().map(|&j| lookup(j as usize).and_then(|j| Steps::reach(0, steps, j)).unwrap_or(Steps::FILL)).collect(), + Field(name) => { + let value = value_call(&first, &origin, &args, &from)?; + dispatch_in(&origin, Operation::Primitive { primitive: Primitive::Mix, pairing: Pairing::Axes }, &[crate::keyed::text(name.text()), value], &[None, None]) } - } - /// The targets of the cells at frame positions `rows`, cell by cell. Single items are their offsets, which are the positions themselves where the cells lie in frame order. Wider cells are a walk, a table of the cells' starts followed by the cells' own steps, which reads and writes cells whose items lie a stride apart, as columns' do, in tiles. - fn targets(&self, rows: &[u32]) -> Targets { self.at_starts(self.starts(rows, Some)) } - /// The cells at `starts`, with a fill start reading a whole cell of fills. Transpose's cell steps start at 0, so singleton cells need only their starts. - fn at_starts(&self, starts: Vec) -> Targets { - let steps = &self.steps[self.rank..]; - if steps.iter().all(|s| s.len() == 1) { return Targets::Offsets(starts); } - let starts = rustymath::gather::Steps::table(starts); - Targets::Walk { base: 0, steps: std::iter::once(starts).chain(steps.iter().cloned()).collect() } - } - /// The selection of the cells at frame positions `rows`, as a list (`listed`). - fn selection(&self, rows: &[u32]) -> Result { - Ok(Selection { frame: ResultFrame::Array(listed(&self.frame(), rows, &self.cells())?), targets: self.targets(rows) }) - } - /// The cells at frame positions `rows` of `array`, as a list (`listed`). - fn read(&self, array: &Value, rows: &[u32], span: &Span) -> Result { self.selection(rows)?.read(array, None, span) } -} - -/// The layout of the cells at positions `rows` of `frame`, as a list of cells laid out by `cell`. A frame of one axis keeps its name and the keys at `rows`. -fn listed(frame: &crate::array::Layout, rows: &[u32], cell: &crate::array::Layout) -> Result { - let list = if frame.shape().len() == 1 { frame.select(0, rows.iter().map(|&j| Some(j as usize))).described("invalid selection")? } - else { crate::array::Layout::from(vec![rows.len()]) }; - Ok(list.concat(cell)) -} - -/// The `len` cells of the frame grouped by the case of `cases` that each picks. `mapping` reads each cell's value from `selector`, and `None` reads them in place, where a Boolean is its own case number. A NaN, or a key that the selector lacks, picks no case, and those cells form a group after the cases. -fn picked(selector: &Value, mapping: Option<&Mapping>, len: usize, cases: usize) -> Result { - use {crate::search::grouped_by, rustymath::gather::Steps}; - match (mapping, selector.as_items()) { - (None, crate::array::Items::Compact(rustymath::buffer::View::Booleans(mask))) if cases > 1 => grouped_by(mask, cases, usize::from), - _ => { - let values = crate::primitive::positions(selector, cases)?; - let keys = match mapping { None => values, Some(m) => (0..len).map(|j| m.get(j).map_or(Steps::FILL, |i| values[i])).collect() }; - grouped_by(&keys, cases + usize::from(keys.contains(&Steps::FILL)), |p| p.min(cases)) + Index => { + let value = value_call(&first, &origin, &args, &from)?; + dispatch_in(&origin, Operation::Primitive { primitive: Primitive::Bracket, pairing: Pairing::Axes }, &[second().into_owned(), value], &[None, None]) } + Bitwise => bitwise_call(&origin, function(&first)?, &args, &from), + Optimize => crate::optimize::optimize(function(&first)?, left, right, &Span::whole(Source::new("", f.bpl()))), + Framed { .. } | JvpStep { .. } | VjpStep { .. } | Derivative => unreachable!(), } } -/// The map from each of the `len` positions of `frame` to a position of `input`, whose axes lie along the frame's in order, as agreement pairs them. An axis of length 1 stretches across the frame. Where both axes have keys, each position looks its key up (`Mapping::new`): a key that `input` lacks gives no position, and a key that only `input` has is never read. Any other axis needs the frame's length, or the call is a LENGTH ERROR with the message `mismatch`. -fn lookup(input: &crate::array::Layout, frame: &crate::array::Layout, len: usize, mismatch: &str) -> Result { - let rank = frame.shape().len(); - if (0..rank).any(|a| !(input.keys(a).is_some() && frame.keys(a).is_some()) && ![frame.shape()[a], 1].contains(&input.shape()[a])) { - return Err(Error::new(Kind::Length, mismatch)); - } - Ok(Mapping::new(input, &(0..rank).collect::>(), frame, len)) +pub(super) fn train_call(f: &Function, origin: Origin<&[Value]>) -> Result { + let FunctionNode::Train(tines) = f.node() else { unreachable!() }; + let values: Vec<_> = (0..tines.len()).map(|i| f.operand(&origin, i).into_owned()).collect(); + let args = Function::body_arguments(&origin)?; + let from = argument_sources(&origin)?; + train(&values, &origin, &args, &from) } -/// A left argument laid over a frame. Its axes pair with the frame's by name and then from the front, and each frame position reads the cell that `lookup` maps it to. A position whose key the argument lacks reads a cell of fills. -struct Laid { value: Value, framed: Framed, lookup: Mapping, frame: crate::array::Layout } - -impl Laid { - fn new(x: &Value, frame: &crate::array::Layout) -> Result { - const MISMATCH: &str = "the left argument doesn't line up with the right argument"; - let axes = rustymath::agreement::Agreement::paired_axes(x.layout(), frame, true); - if axes.iter().any(|&a| a >= x.shape().len()) { return Err(Error::new(Kind::Length, MISMATCH)); } - let framed = Framed::new(x.layout(), &axes)?; - let lookup = lookup(&framed.frame(), frame, frame.count(), MISMATCH)?; - Ok(Self { value: x.clone(), framed, lookup, frame: frame.clone() }) +fn train(tines: &[Value], origin: &Origin<&[Value]>, args: &[Value], from: &[Option]) -> Result { + let mut end = tines.len(); + let mut parts = Vec::new(); + while end > 0 { + let mut start = end - 1; + if !matches!(tines[start], Value::Function(_)) { return Err(Error::domain("a train must end in a function")); } + while start > 0 && !matches!(tines[start - 1], Value::Function(_)) { start -= 1; } + parts.push(&tines[start..end]); end = start; } - - /// The cells for frame positions `rows`, as a list that keeps the frame's keys at `rows` (`listed`). - fn cells(&self, rows: &[u32], span: &Span) -> Result { - let starts = self.framed.starts(rows, |j| self.lookup.get(j)); - let layout = listed(&self.frame, rows, &self.framed.cells())?; - Selection { frame: ResultFrame::Array(layout), targets: self.framed.at_starts(starts) }.read(&self.value, None, span) - } - - /// The argument laid over the whole frame: its cells in frame order, with the frame's axes first. - fn aligned(&self, span: &Span) -> Result { let len = self.frame.count(); self.cells(&(0..len as u32).collect::>(), span) } + let call = |part: &[Value], args: &[Value], from: &[Option]| -> Result { + let function = function(part.last().unwrap())?; + if part.len() == 1 { return function.call_bound(origin, args, from); } + if args.len() != 1 || part.len() != 2 { return Err(Error::domain("a bound train part needs one argument")); } + function.call_bound(origin, &[part[0].clone(), args[0].clone()], &[None, from[0]]) + }; + let mut parts = parts.into_iter(); + let mut result = call(parts.next().ok_or_else(|| Error::domain("empty train"))?, args, from)?; + while let Some(middle) = parts.next() { + if middle.len() > 1 { result = call(middle, &[result], &[None])?; } + else if let Some(first) = parts.next() { + let first = call(first, args, from)?; + result = call(middle, &[first, result], &[None, None])?; + } else { result = call(middle, &[result], &[None])?; } + } + Ok(result) +} + +pub(super) fn train_discards(f: &Function, dyadic: bool) -> bool { + let FunctionNode::Train(tines) = f.node() else { return false; }; + tines.iter().any(|tine| matches!(tine, FunctionOperand::Value { value: Value::Function(f), .. } if f.discards(dyadic))) +} + +pub(super) fn function_array(f: &Function, origin: Origin<&[Value]>) -> Result { + let items = f.operand(&origin, 0); + let args = Function::body_arguments(&origin)?; + let from = argument_sources(&origin)?; + let results = items.elements().map(|item| value_call(&item, &origin, &args, &from)).collect::>>()?; + dispatch_in(&origin, Operation::Join(crate::primitive::Notation::Strand), &results, &vec![None; results.len()])?.with_shape(items.shape()) +} + +fn loop_call(origin: &Origin<&[Value]>, body: &Function, exit: &Value, args: &[Value], from: &[Option], items: Option<(bool, bool)>) -> Result { + let (exit_kind, order, mut values, mut positions) = if let Some((scan, first)) = items { + (Exit::Items { scan, first }, if scan { Order::Forward } else { Order::Backward }, vec![Value::Function(body.clone())], vec![None]) + } else if let Value::Function(predicate) = exit { + (Exit::Predicate, Order::Forward, vec![Value::Function(body.clone()), Value::Function(predicate.clone())], vec![None, None]) + } else { (Exit::Power, Order::Forward, vec![Value::Function(body.clone()), exit.clone()], vec![None, None]) }; + values.extend_from_slice(args); positions.extend_from_slice(from); + dispatch_in(origin, Operation::Loop { exit: exit_kind, order, body_result: crate::primitive::LoopResult::Carry }, &values, &positions) +} + +fn fold_call(origin: &Origin<&[Value]>, f: &Function, kind: &OperatorKind, args: &[Value], from: &[Option]) -> Result { + let scan = matches!(kind, OperatorKind::Scan | OperatorKind::ScanFirst); + let first = matches!(kind, OperatorKind::ReduceFirst | OperatorKind::ScanFirst); + if let Some(primitive) = f.dyadic_primitive() { return dispatch_in(origin, Operation::Fold { primitive, first, scan, pairing: Pairing::Axes }, args, from); } + loop_call(origin, f, &Value::integer(0), args, from, Some((scan, first))) +} + +fn cell_ranks(value: &Value) -> Result<[isize; 3]> { + let value = value.read(crate::ReadKind::Structure)?; + if value.rank() > 1 { return Err(Error::domain("rank operand must be a scalar or vector")); } + let ranks = value.elements().map(|value| value.as_real().filter(|n| n.is_infinite() || n.fract() == 0.0).map(|n| n as isize).ok_or_else(|| Error::domain("invalid cell rank"))).collect::>>()?; + match ranks.as_slice() { [m] => Ok([*m; 3]), [l, r] => Ok([*r, *l, *r]), [m, l, r] => Ok([*m, *l, *r]), _ => Err(Error::domain("rank needs one, two or three numbers")) } +} +fn within(rank: usize, requested: isize) -> usize { if requested < 0 { rank.saturating_sub(requested.unsigned_abs()) } else { rank.min(requested as usize) } } + +fn rank_call(origin: &Origin<&[Value]>, f: &Function, ranks: &Value, args: &[Value], from: &[Option]) -> Result { + let ranks = value_call(ranks, origin, args, from)?; + let [m, l, r] = cell_ranks(&ranks)?; + let selected: Vec<_> = args.iter().enumerate().map(|(i, arg)| arg.rank() - within(arg.rank(), if args.len() == 1 { m } else if i == 0 { l } else { r })).collect(); + let frame = selected.iter().copied().max().unwrap_or(0); + let mut values = Vec::with_capacity(args.len()); + let mut atoms = Vec::new(); + for (arg, &axes) in args.iter().zip(&selected) { + let mut shape = arg.shape()[..axes].to_vec(); shape.extend(std::iter::repeat_n(1, frame - axes)); shape.extend_from_slice(&arg.shape()[axes..]); + values.push(arg.with_shape(shape)?); atoms.push(false); + } + crate::batch::map(f, origin, &values, frame, &atoms, &vec![None; values.len()]) +} + +fn each_call(origin: &Origin<&[Value]>, f: &Function, args: &[Value], from: &[Option]) -> Result { + let frame = args.iter().map(Value::rank).max().unwrap_or(0); + let atoms: Vec<_> = args.iter().map(|_| true).collect(); + crate::batch::map(f, origin, args, frame, &atoms, from) +} + +fn outer_call(origin: &Origin<&[Value]>, f: &Function, args: &[Value], _: &[Option]) -> Result { + let [left, right] = args else { return Err(Error::domain("outer needs two arguments")); }; + let frame = left.rank() + right.rank(); + let mut xshape = left.shape().to_vec(); xshape.extend(std::iter::repeat_n(1, right.rank())); + let mut yshape = vec![1; left.rank()]; yshape.extend_from_slice(right.shape()); + crate::batch::map(f, origin, &[left.with_shape(xshape)?, right.with_shape(yshape)?], frame, &[true, true], &[None, None]) +} + +fn product_call(origin: &Origin<&[Value]>, f: &Function, g: &Function, args: &[Value], from: &[Option]) -> Result { + let [left, right] = args else { return expansion(origin, f, g, &args[0]); }; + if let (Some(fold), Some(map)) = (f.dyadic_primitive(), g.dyadic_primitive()) { + return dispatch_in(origin, Operation::Product { fold, map, contracted: 1 }, args, from); + } + let left_rank = left.rank(); let right_rank = right.rank(); + let n = left.shape().last().copied().unwrap_or(1); + if n != right.shape().first().copied().unwrap_or(1) { return Err(Error::domain("product axes do not agree")); } + let mut xshape = left.shape().to_vec(); xshape.extend(std::iter::repeat_n(1, right_rank.saturating_sub(1))); + let mut yshape = vec![1; left_rank.saturating_sub(1)]; yshape.extend_from_slice(right.shape()); + let pairs = g.call_bound(origin, &[left.with_shape(xshape)?, right.with_shape(yshape)?], &[None, None])?; + let reduce = Function::applied_kind(OperatorKind::Reduce, vec![Value::Function(f.clone())])?; + axis_call(origin, &reduce, &Value::integer(left_rank.saturating_sub(1) as i64), &[pairs], &[None]) +} + +fn agenda_call(origin: &Origin<&[Value]>, selector: &Value, cases: &Value, args: &[Value], from: &[Option]) -> Result { + let selector = value_call(selector, origin, args, from)?; + let cases = cases.read(crate::ReadKind::Structure)?; + let mut values: Vec<_> = cases.elements().map(|case| function(&case).cloned().map(Value::Function)).collect::>()?; + let count = values.len(); values.push(selector); values.extend_from_slice(args); + let mut sources = vec![None; count + 1]; sources.extend_from_slice(from); + dispatch_in(origin, Operation::Select { cases: count }, &values, &sources) +} + +fn under_call(origin: &Origin<&[Value]>, f: &Value, g: &Value, args: &[Value], from: &[Option]) -> Result { + let right = crate::selection::rooted(args.last().unwrap())?; + let selected = match g { + Value::Function(g) => g.call_bound(origin, &[right.clone()], &[None])?, + index => dispatch_in(origin, Operation::Primitive { primitive: Primitive::Index, pairing: Pairing::Axes }, &[index.clone(), right.clone()], &[None, None])?, + }; + let mut inputs = args.to_vec(); *inputs.last_mut().unwrap() = selected.clone(); + let mut sources = from.to_vec(); *sources.last_mut().unwrap() = None; + let result = value_call(f, origin, &inputs, &sources)?; + if let Some(walk) = crate::selection::selecting(&selected, &right)? { return walk.write(result, None); } + let g = function(g)?; + if g.discards(false) { return Err(Error::domain("Under can't invert a function that drops items")); } + let inverse = g.inverse(&Span::whole(Source::new("", g.bpl())))?; + inverse.call_bound(origin, &[result], &[None]) +} + +fn axis_call(origin: &Origin<&[Value]>, f: &Function, spec: &Value, args: &[Value], from: &[Option]) -> Result { + let spec = value_call(spec, origin, args, from)?; + if let Some(primitive) = f.as_primitive() { + let values: Vec<_> = std::iter::once(spec.clone()).chain(args.iter().cloned()).collect(); + let sources: Vec<_> = std::iter::once(None).chain(from.iter().copied()).collect(); + return dispatch_in(origin, Operation::Primitive { primitive, pairing: Pairing::Explicit }, &values, &sources); + } + if let FunctionNode::Applied { op: Operator::Kind(kind), .. } = f.node() + && matches!(kind, OperatorKind::Reduce | OperatorKind::ReduceFirst | OperatorKind::Scan | OperatorKind::ScanFirst) { + let g = f.operand(origin, 0); + return axis_fold(origin, function(&g)?, kind, &spec, args, from); + } + cell_axes(origin, f, &spec, args, from) +} + +fn key_call(origin: &Origin<&[Value]>, f: &Function, args: &[Value], _: &[Option]) -> Result { + let right = args.last().unwrap(); + let keys = &args[0]; + if keys.is_unit() || right.is_unit() { return Err(Error::new(Kind::Rank, "key arguments must have major cells")); } + if keys.shape()[0] != right.shape()[0] { return Err(Error::new(Kind::Length, "key arguments must have equal tallies")); } + let primitive = |primitive, args: &[Value]| dispatch_in(origin, Operation::Primitive { primitive, pairing: Pairing::Axes }, args, &vec![None; args.len()]); + let unique = primitive(Primitive::Union, &[keys.clone()])?; + let groups = primitive(Primitive::Find, &[if args.len() == 2 { keys.unkeyed()? } else { keys.clone() }])?; + let groups = if args.len() == 2 { + let select = Function::applied_kind(OperatorKind::After, vec![Value::Function(Function::primitive(Primitive::Index)), right.clone()])?; + each_call(origin, &select, &[groups], &[None])? + } else { groups }; + crate::batch::map(f, origin, &[unique, groups], 1, &[keys.rank() == 1, true], &[None, None]) +} +fn stencil_call(origin: &Origin<&[Value]>, f: &Function, spec: &Value, args: &[Value], from: &[Option]) -> Result { + let spec = value_call(spec, origin, args, from)?; + let spec = crate::primitive::WindowSpec::new(&spec)?; + if spec.sizes.is_empty() || spec.sizes.iter().chain(&spec.moves).any(|&n| n <= 0) { + return Err(Error::domain("Stencil needs positive sizes and movements")); + } + let count = spec.sizes.len(); + let mut names = vec![Arc::::from("size")]; + let mut values = vec![Value::integers([count], spec.sizes.iter().map(|&n| -n).collect())?]; + if !spec.moves.is_empty() { names.push("step".into()); values.push(Value::integers([spec.moves.len()], spec.moves)?); } + if let Some(edge) = spec.edge { names.push("edge".into()); values.push(crate::keyed::text(edge.name())); } + if let Some(fill) = spec.fill { names.push("fill".into()); values.push(fill); } + let options = crate::keyed::vector(names, values)?; + let windows = dispatch_in(origin, Operation::Primitive { primitive: Primitive::Windows, pairing: Pairing::Axes }, + &[options, args.last().unwrap().clone()], &[None, *from.last().unwrap()])?; + rank_call(origin, f, &Value::integer(-(count as i64)), &[windows], &[None]) +} +fn axis_fold(origin: &Origin<&[Value]>, f: &Function, kind: &OperatorKind, axes: &Value, args: &[Value], from: &[Option]) -> Result { + if let Some(primitive) = f.dyadic_primitive() { + let values: Vec<_> = std::iter::once(axes.clone()).chain(args.iter().cloned()).collect(); + let sources: Vec<_> = std::iter::once(None).chain(from.iter().copied()).collect(); + return dispatch_in(origin, Operation::Fold { primitive, first: matches!(kind, OperatorKind::ReduceFirst | OperatorKind::ScanFirst), + scan: matches!(kind, OperatorKind::Scan | OperatorKind::ScanFirst), pairing: Pairing::Explicit }, &values, &sources); + } + let fold = Function::applied_kind(kind.clone(), vec![Value::Function(f.clone())])?; + let ravelled = Function::applied_kind(OperatorKind::Atop, vec![Value::Function(fold), Value::Function(Function::primitive(Primitive::Ravel))])?; + cell_axes(origin, &ravelled, axes, args, from) +} + +fn reorder(origin: &Origin<&[Value]>, value: &Value, sources: &[usize]) -> Result { + let mut destinations = vec![0; sources.len()]; + for (destination, &source) in sources.iter().enumerate() { destinations[source] = destination as i64; } + let order = Value::integers([destinations.len()], destinations)?; + dispatch_in(origin, Operation::Primitive { primitive: Primitive::Transpose, pairing: Pairing::Axes }, &[order, value.clone()], &[None, None]) +} + +fn cell_axes(origin: &Origin<&[Value]>, f: &Function, spec: &Value, args: &[Value], _: &[Option]) -> Result { + let spec = spec.read(crate::ReadKind::Structure)?; + let target = args.iter().max_by_key(|arg| arg.rank()).unwrap(); + let order = target.rank(); + let selected = crate::primitive::axes(&crate::primitive::resolve_axes(&spec, &target.layout())?, order)?; + let named = crate::primitive::names_axes(&spec); + let mut values = Vec::with_capacity(args.len()); + let mut ranks = Vec::with_capacity(args.len()); + for arg in args { + let own = if named { + crate::primitive::resolve_axes(&spec, &arg.layout()).and_then(|spec| crate::primitive::axes(&spec, arg.rank())).ok() + } else { selected.iter().all(|&axis| axis < arg.rank()).then(|| selected.clone()) }; + if let Some(own) = own { + let axes: Vec<_> = (0..arg.rank()).filter(|axis| !own.contains(axis)).chain(own.iter().copied()).collect(); + values.push(reorder(origin, arg, &axes)?); ranks.push(own.len()); + } else { values.push(arg.clone()); ranks.push(arg.rank()); } + } + let frame = rustymath::agreement::Frames::new(&values.iter().zip(&ranks).map(|(arg, &rank)| (&arg.layout().core, rank)).collect::>())?.layout.shape().len(); + let rank_value = Value::integers([ranks.len()], ranks.iter().map(|&rank| rank as i64).collect())?; + let result = rank_call(origin, f, &rank_value, &values, &vec![None; values.len()])?; + if frame != order - selected.len() { return Ok(result); } + let cell = result.rank().saturating_sub(frame); + let sources: Vec<_> = if cell == selected.len() { + let previous: Vec<_> = (0..order).filter(|axis| !selected.contains(axis)).chain(selected.iter().copied()).collect(); + (0..order).map(|axis| previous.iter().position(|&old| old == axis).unwrap()).collect() + } else { + let start = selected.first().map_or(frame, |&first| (0..first).filter(|axis| !selected.contains(axis)).count()); + (0..start).chain(frame..frame + cell).chain(start..frame).collect() + }; + reorder(origin, &result, &sources) } -/// The layout of `Cells::chosen`'s result: the frame of `framed`, whose axes are `axes` of the argument, with cells laid out by `cell`. Where the cells have the rank of the argument's cells, the frame's axes keep their places in the argument. Otherwise they come first, as a frame does in Rank. Also gives the positions of the frame's axes in the result. -fn placed(framed: &Framed, axes: &[usize], cell: &crate::array::Layout) -> Result<(crate::array::Layout, Vec), Error> { - let first = framed.frame().concat(cell); - let n = framed.layout.shape().len(); - if cell.shape().len() != n - axes.len() { return Ok((first, (0..axes.len()).collect())); } - let plan = rustymath::structure::transpose(&first, &fronted(axes, n))?; - Ok((plan.layout.into(), axes.to_vec())) +fn inverse_call(origin: &Origin<&[Value]>, f: &Function, args: &[Value], from: &[Option]) -> Result { + invert(origin, f, args, from, true) } -/// `operand` called on prototypes, as Each calls it for a frame with no cells. A call that fails gives 0, the prototype of the empty result, as in J. Inside a batched run, a call that fails stops the run, as a guard does (`session::Catch`). -fn on_prototypes(operand: &Function, left: Option<&Value>, right: &Value, span: &Span) -> Result { - match operand.call_prototype(left, right, span) { - Err(e) if super::session::Catch::All.catches(&e.kind) => { - session().effect(crate::transform::Effect::Catches)?; - Ok(Bound::from(Value::Number(Numeric::current().float(0.0)))) - } - result => result, +fn invert(origin: &Origin<&[Value]>, f: &Function, args: &[Value], from: &[Option], first: bool) -> Result { + if let Some(g) = f.rules().and_then(|rules| rules.inverse.as_ref()) { + if !first { return Err(Error::domain("this function has no known inverse")); } + return g.call_bound(origin, args, from); } -} - -/// The order of the axes of an array of rank `n` with the axes `first` moved to the front, in their order, and the others after them, in theirs. -fn fronted(first: &[usize], n: usize) -> Vec { first.iter().copied().chain((0..n).filter(|a| !first.contains(a))).collect() } - -/// `f@g` for a function `g`: `f⍥g`, with the result written back. A `g` that selects writes it back where it took it from. Any other `g` is undone by its inverse. -fn under_function(f: &Function, g: &Function, left: Option<&Value>, right: &Value, span: &Span) -> Result { - let level = session().begin(crate::transform::Kind::Label); - let selected = crate::selection::labelled(right, level, span).and_then(|(labels, y)| Ok((labels, g.call_with(None::<&Value>, y, span)?.array(span)?))); - session().end(); - let (mut labels, selected) = selected?; - let read = labels.read(&selected, level); - let x = left.map(|x| g.call_array(None, x, span)).transpose()?; - let result = f.call_with(x, crate::selection::strip(&selected, level), span)?.array(span)?; - match read { - Some(read) => { - let (selection, values) = labels.replacements(read, &result, span)?; - Ok(Bound::from(selection.write(right, &values, span)?)) + let bound = f.bound(origin, args, from)?; + match f.node() { + FunctionNode::Primitive(primitive) => (primitive.info().inverse)(f, bound.reborrow(), first), + FunctionNode::System { function, .. } => (function.row(None).inverse)(f, bound.reborrow(), first), + FunctionNode::Applied { op: Operator::Kind(kind), .. } if first => (kind.info().inverse)(f, bound.reborrow()), + FunctionNode::Applied { op: Operator::Kind(_), .. } => inverse_derived(f, bound.reborrow(), false), + FunctionNode::Train(tines) if tines.len() == 2 => { + let view = bound.reborrow(); + let a = f.operand(&view, 0); let b = f.operand(&view, 1); + let from = argument_sources(&view)?; + let value = invert(&view, function(&a)?, &[args.last().unwrap().clone()], &[*from.last().unwrap()], true)?; + let mut values = args.to_vec(); *values.last_mut().unwrap() = value; + let mut sources = from; *sources.last_mut().unwrap() = None; + invert(&view, function(&b)?, &values, &sources, first) } - None if g.discards(false) => Err(Error::domain("Under can't use the inverse of a function that drops items")), - None => g.inverse(span)?.call_with(None::<&Value>, result, span), + _ => Err(Error::domain("this function has no known inverse")), } } -/// `f⍥g`: `f` applied to `g` of each argument. -fn over(f: &Function, g: &Function, left: Option<&Value>, right: &Value, span: &Span) -> Result { - let y = g.call_array(None, right, span)?; - let x = left.map(|x| g.call_array(None, x, span)).transpose()?; - f.call_with(x, y, span) -} - -/// `f⌺spec` is `f⍤(-n) spec↕right`, for a spec of `n` sizes: `f` on each window of `right`, centred on a position, as Rank calls it. The spec's edge mode says what a window reads past the ends, fill by default. -fn stencil(f: &Function, spec: &Value, right: &Value, span: &Span) -> Result { - let mut spec = crate::primitive::WindowSpec::new(spec)?; - if spec.sizes.is_empty() { return Err(Error::domain("stencil needs at least one axis")); } - if spec.sizes.iter().chain(&spec.moves).any(|&n| n <= 0) { return Err(Error::domain("stencil sizes and movements must be positive")); } - spec.edge.get_or_insert(rustymath::structure::Edge::Fill); - let windows = crate::primitive::windowed(&spec, right, span)?; - let ranks = Value::integers(vec![1], vec![-(spec.sizes.len() as i64)]).expect("one rank fills its vector"); - rank(f, &ranks, None, &windows, span) -} +fn inverse(f: &Function, origin: Origin<&[Value]>) -> Result { inverse_derived(f, origin, true) } -fn power(operands: &[Operand; 2], left: Option<&Value>, right: impl Argument, span: &Span) -> Result { - let [Operand::Function(f), operand] = operands else { unreachable!() }; - // A one-item list holding a predicate keeps every state of the until form. - let listed = match operand { - Operand::Value(a) if a.shape() == [1] && matches!(a.at(0), Value::Function(_)) => Some(Operand::from_value(Binding::from_element(a.at(0)))), _ => None - }; - let history = listed.is_some(); - let operand = listed.as_ref().unwrap_or(operand); - // `∞` runs until the state stops changing: until a state matches the one before within the convergence tolerance of each float width, whatever the pref. An iteration that settles into a cycle between adjacent floats then stops. - let converge = |step: &Function, value: &mut Value, span: &Span| -> Result<(), Error> { - loop { - let next = step.call_array(left, value, span)?; - let same = next.matches_within(value, Numeric::current().convergence(), span)?; - *value = next; - if same { return Ok(()); } - } +fn inverse_derived(f: &Function, origin: Origin<&[Value]>, first: bool) -> Result { + use OperatorKind::*; + let (kind, _) = applied(f); + let g = f.operand(&origin, 0); + let second = || f.operand(&origin, 1); + let args = Function::body_arguments(&origin)?; + let from = argument_sources(&origin)?; + let left = (args.len() == 2).then(|| &args[0]); + let right = args.last().unwrap(); + let inverse = |g: &Function, fixed: Option<(&Value, bool)>, value: &Value| { + let values: Vec<_> = fixed.into_iter().map(|(value, _)| value.clone()).chain([value.clone()]).collect(); + invert(&origin, g, &values, &vec![None; values.len()], fixed.is_none_or(|(_, first)| first)) }; - match operand { - Operand::Value(count) if count.is_atom() => { - let (negative, n) = power_count(count.observed()?)?; - let inverse; - let f = if negative { inverse = f.inverse(span)?; &inverse } else { f }; - if n == u64::MAX { - let mut value = right.owned(); - converge(f, &mut value, span)?; - return Ok(Bound::from(value)); - } - if n == 0 { return Ok(Bound::from(right.owned())); } - // The first step takes the argument as the caller gave it, and each later step takes the state that the step before made. - let mut result = f.call_with(left, right, span)?; - for _ in 1..n { result = f.call_with(left, result.array(span)?, span)?; } - Ok(result) - } - // An array of counts gives one item for each count, holding the state after that many steps. - Operand::Value(count) => { - let right = right.value(); - let mut counts = Vec::new(); - power_counts(count, span, &mut counts)?; - counts.sort_unstable(); - counts.dedup(); - let mut states = HashMap::new(); - let (mut value, mut negative, mut steps, mut inverse) = (right.clone(), false, 0, None); - for c in counts { - if c.0 && !negative { - (value, negative, steps) = (right.clone(), true, 0); - inverse = Some(f.inverse(span)?); + let operand_inverse = |g: &Function| if first { g.inverse(&Span::whole(Source::new("", ""))) } + else { g.commuted(&Span::whole(Source::new("", "")))?.inverse(&Span::whole(Source::new("", ""))) }; + let fixed = left.map(|left| (left, first)); + match kind { + Inverse if first => function(&g)?.call_bound(&origin, &args, &from), + Atop => { let value = inverse(function(&g)?, None, right)?; inverse(function(&second())?, fixed, &value) } + Over => { + let h = second(); + let bound = left.map(|left| value_call(&h, &origin, &[left.clone()], &[from[0]])).transpose()?; + let value = inverse(function(&g)?, bound.as_ref().map(|value| (value, first)), right)?; + inverse(function(&h)?, None, &value) + } + Before => { + let h = second(); + match (g.as_ref(), left) { + (value, None) if !matches!(value, Value::Function(_)) => inverse(function(&h)?, Some((value, true)), right), + (Value::Function(g), Some(left)) if first => { + let fixed = g.call_bound(&origin, &[left.clone()], &[from[0]])?; + inverse(function(&h)?, Some((&fixed, true)), right) } - let step = inverse.as_ref().unwrap_or(f); - if c.1 == u64::MAX { converge(step, &mut value, span)?; } - else { for _ in steps..c.1 { value = step.call_with(left, value, span)?.array(span)?; } } - steps = c.1; - states.insert(c, value.clone()); + (Value::Function(g), Some(_)) => { let value = inverse(function(&h)?, fixed, right)?; inverse(g, None, &value) } + _ => Err(Error::domain("this hook has no known inverse")), } - Ok(Bound::from(power_states(count, &states, right, span)?)) } - Operand::Function(test) => { - let mut value = right.owned(); - let mut states = if history { vec![value.clone()] } else { Vec::new() }; - loop { - let next = f.call_array(left, &value, span)?; - let done = test.call_array(Some(&next), &value, span)?; - let done = done.boolean().described("power predicate must return a Boolean singleton")?; - value = next; - if history { - generated_len(&[states.len() + 1])?; - states.push(value.clone()); + After => { + let h = second(); + match (h.as_ref(), left) { + (value, None) if !matches!(value, Value::Function(_)) => inverse(function(&g)?, Some((value, false)), right), + (Value::Function(h), Some(left)) if !first => { + let fixed = h.call_bound(&origin, &[left.clone()], &[from[0]])?; + inverse(function(&g)?, Some((&fixed, false)), right) } - if done { break; } - } - if history { - let items = states.iter().map(|s| s.enclose()).collect::, _>>().described("history result is too large")?; - value = Value::assemble(&[items.len()], &items, &states[0].enclose().described("history result is too large")?) - .described("history result is too large")?; + (Value::Function(h), Some(_)) => { let value = inverse(function(&g)?, fixed, right)?; inverse(h, None, &value) } + _ => Err(Error::domain("this hook has no known inverse")), } - Ok(Bound::from(value)) - } - } -} - -/// The count that the observed value `e` gives: its direction and its number of steps, with `u64::MAX` for `∞`. Any other value gives the message of its DOMAIN error. -pub(super) fn power_count(e: Plain<'_>) -> Result<(bool, u64), &'static str> { - if let Some(negative) = e.infinity() { return Ok((negative, u64::MAX)); } - let Plain::Number(n) = e else { return Err("power counts must be numeric") }; - n.integer().map(|n| (n < 0, n.unsigned_abs())).map_err(|_| "power counts must be whole or infinite") -} - -/// The counts in a count array, through any nesting. -fn power_counts(count: &Value, span: &Span, counts: &mut Vec<(bool, u64)>) -> Result<(), Error> { - for e in count.elements() { match &e { Value::Array(_) => power_counts(&e, span, counts)?, _ => counts.push(power_count(e.observed()?)?) } } - Ok(()) -} - -/// `count` with each count replaced by the state after that many steps, as one item. A nested count array gives a nested item. -fn power_states(count: &Value, states: &HashMap<(bool, u64), Value>, right: &Value, span: &Span) -> Result { - let items = count.elements().map(|e| { - let state = match &e { Value::Array(_) => power_states(&e, states, right, span)?, _ => states[&power_count(e.observed()?)?].clone() }; - state.enclose().described("power result is too large") - }).collect::, _>>()?; - count.layout().assemble(&items, &right.enclose().described("power result is too large")?).described("power result is too large") -} - -pub(super) fn inverse(f: &Function, bound: Option<(&Value, bool)>, right: &Value, span: &Span) -> Result { - use FunctionNode::*; - use OperatorKind::*; - let left = bound.map(|(a, _)| a); - let first = bound.is_none_or(|(_, first)| first); - let operand_inverse = |g: &Function| { - let g = if first { g.clone() } else { Function::new(Modified(Commute, Operand::Function(g.clone())), span)? }; - g.inverse(span) - }; - let array = match f.node() { - Primitive(p) => crate::primitive::inverse(*p, bound, right, None, span), - Ruled(_, Rules { inverse: Some(g), .. }) if first => return g.call(left, right, span), - Ruled(g, Rules { inverse: None, .. }) => return inverse(g, bound, right, span), - Composed(Under, [Operand::Function(g), h]) => { - return composition(&Function::new(Composed(Under, [Operand::Function(operand_inverse(g)?), h.clone()]), span)?, left, right, span); - } - Inverse(g) if first => return g.call(left, right, span), - Modified(Each, Operand::Function(g)) => return each(&operand_inverse(g)?, left, right, span), - Modified(Outer, Operand::Function(g)) => { - let bound = left.ok_or_else(|| Error::domain("outer-product inverse needs a bound argument"))?; - inverse_outer(g, bound, first, right, span) - } - Modified(Commute, Operand::Value(k)) => { - if !k.matches(right, span)? { return Err(Error::domain("a constant function gives only its own value")); } - Ok(right.clone()) } - Modified(Commute, Operand::Function(g)) => { - if let Some((a, first)) = bound { return inverse(g, Some((a, !first)), right, span); } - use crate::{number::{Arithmetic, Math}, primitive::Primitive as P}; - // With one argument, `(g↢h)⍨` is the hook `g↢h`. - if matches!(g.node(), Composed(After, [Operand::Function(_), Operand::Function(_)])) { return inverse(g, None, right, span); } - let Primitive(p) = g.node() else { return Err(Error::domain("this commute has no known inverse")); }; - match p { - // With one argument, `+⍨` is monadic `⊽`, which doubles, and `×⍨` is monadic `⊼`, which squares. - P::Arithmetic(Arithmetic::Plus) => crate::primitive::inverse(P::Math(Math::Nor), None, right, None, span), - P::Arithmetic(Arithmetic::Times) => crate::primitive::inverse(P::Math(Math::Nand), None, right, None, span), - _ if p.info().dyad.picks => Ok(right.clone()), - P::Math(Math::Lcm | Math::Gcd) => { - if !p.call(Some(right), right, span)?.matches(right, span)? { return Err(Error::domain("no argument gives this result")); } - Ok(right.clone()) - } + Valences => { let selected = if left.is_some() { second() } else { g }; inverse(function(&selected)?, fixed, right) } + Commute => match g.as_ref() { + Value::Function(g) if left.is_some() => inverse(g, left.map(|left| (left, !first)), right), + Value::Function(g) => match g.as_primitive() { + Some(Primitive::Arithmetic(crate::number::Arithmetic::Plus)) => inverse(&Function::primitive(Primitive::Math(crate::number::Math::Nor)), None, right), + Some(Primitive::Arithmetic(crate::number::Arithmetic::Times)) => inverse(&Function::primitive(Primitive::Math(crate::number::Math::Nand)), None, right), + Some(Primitive::Identity(_)) => Ok(right.clone()), _ => Err(Error::domain("this commute has no known inverse")), + }, + value => { + if !value.matches_within(right, crate::settings::captured(origin.call_view(&origin.list)))? { return Err(Error::domain("a constant function gives only its own value")); } + Ok(right.clone()) } } - Composed(Before, [Operand::Value(a), Operand::Function(g)]) if bound.is_none() => { return inverse(g, Some((a, true)), right, span); } - Composed(After, [Operand::Function(g), Operand::Value(a)]) if bound.is_none() => { return inverse(g, Some((a, false)), right, span); } - Composed(After, [Operand::Function(g), Operand::Function(h)]) => { - let Some((a, first)) = bound else { - // One argument makes a hook, `⍵ g h ⍵`. Only `×↢*` has a known inverse. - use crate::{number::{Arithmetic, Math}, primitive::Primitive as P}; - if matches!(g.node(), Primitive(P::Arithmetic(Arithmetic::Times))) && matches!(h.node(), Primitive(P::Math(Math::Power))) { - return crate::primitive::lambert_w(right, span).map(Bound::from); - } - return Err(Error::domain("this hook has no known inverse")); - }; - if !first { let fixed = h.call_array(None, a, span)?; return inverse(g, Some((&fixed, false)), right, span); } - let y = inverse(g, bound, right, span)?.array(span)?; - return h.inverse(span)?.call_with(None::<&Value>, y, span); - } - Composed(Atop, [Operand::Function(g), Operand::Function(h)]) => { - let y = g.inverse(span)?.call_array(None, right, span)?; - return inverse(h, bound, &y, span); - } - Composed(Over, [Operand::Function(g), Operand::Function(h)]) => { - let fixed = left.map(|a| h.call_array(None, a, span)).transpose()?; - let y = inverse(g, fixed.as_ref().map(|a| (a, first)), right, span)?.array(span)?; - return h.inverse(span)?.call_with(None::<&Value>, y, span); - } - Composed(Before, [Operand::Function(g), Operand::Function(h)]) if bound.is_some() => { - if first { let fixed = g.call_array(None, left.unwrap(), span)?; return inverse(h, Some((&fixed, true)), right, span); } - let y = inverse(h, bound, right, span)?.array(span)?; - return g.inverse(span)?.call_with(None::<&Value>, y, span); - } - Composed(Valences, [Operand::Function(g), Operand::Function(h)]) => { - return if bound.is_some() { inverse(h, bound, right, span) } else { inverse(g, None, right, span) }; - } - Composed(Power, [Operand::Function(g), Operand::Value(count)]) if first && count.is_atom() => { - let count = crate::primitive::Primitive::Arithmetic(crate::number::Arithmetic::Minus).call(None, count, span)?; - return power(&[Operand::Function(g.clone()), Operand::Value(count)], left, right, span); - } - Composed(Rank, [Operand::Function(g), Operand::Value(ranks)]) => { - let mut ranks = ranks.clone(); + Each => each_call(&origin, &operand_inverse(function(&g)?)?, &args, &from), + Under => under_call(&origin, &Value::Function(operand_inverse(function(&g)?)?), &second(), &args, &from), + Scan | ScanFirst if first => inverse_scan(&origin, function(&g)?, matches!(kind, ScanFirst), None, left, right), + Rank => { + let mut ranks = second().read(crate::ReadKind::Structure)?; if !first && (2..=3).contains(&ranks.len()) { - let mut items: Vec<_> = ranks.elements().collect(); - let n = items.len(); - items.swap(n - 2, n - 1); - ranks = Value::new(ranks.shape().to_vec(), items).described("invalid inverse ranks")?; + let mut items: Vec<_> = ranks.elements().collect(); let n = items.len(); items.swap(n - 2, n - 1); + ranks = Value::new(ranks.shape().to_vec(), items)?; } - return rank(&operand_inverse(g)?, &ranks, left, right, span); - } - FunctionNode::Fold(g, h) if h.scan && first => inverse_scan(g, *h, None, left, right, span), - FunctionNode::Axis(g, axis) => { - let mut g = g; - while let FunctionNode::Axis(inner, _) = g.node() { g = inner; } - match g.node() { - Primitive(p) => crate::primitive::inverse(*p, bound, right, Some(axis), span), - FunctionNode::Fold(g, h) if h.scan && first => inverse_scan(g, *h, Some(axis), left, right, span), - _ => Err(Error::domain("this axis-qualified function has no known inverse")), + rank_call(&origin, &operand_inverse(function(&g)?)?, &ranks, &args, &from) + } + Axis => { + let axes = value_call(&second(), &origin, &args, &from)?; + if let Some(primitive) = function(&g)?.as_primitive() { + let values: Vec<_> = std::iter::once(axes.clone()).chain(args.iter().cloned()).collect(); + let sources: Vec<_> = std::iter::once(None).chain(from.iter().copied()).collect(); + let call = projected_call(&origin, Operation::Primitive { primitive, pairing: Pairing::Explicit }, &values, &sources)?; + return (primitive.info().inverse)(function(&g)?, call.reborrow(), first); } - } - System(f) if first => crate::system::inverse(f, left, right, span), - _ => Err(Error::domain("this function has no known inverse")), - }?; - Ok(Bound::from(array)) -} - -fn inverse_outer(f: &Function, bound: &Value, first: bool, right: &Value, span: &Span) -> Result { - crate::program::trace::per_item(Some(bound), right)?; - let rank = bound.shape().len(); - if bound.is_empty() || right.shape().len() < rank { return Err(Error::domain("outer-product inverse needs a nonempty matching bound frame")); } - let split = if first { rank } else { right.shape().len() - rank }; - let (prefix, suffix) = right.shape().split_at(split); - let (frame, shape) = if first { (prefix, suffix) } else { (suffix, prefix) }; - if frame != bound.shape() { return Err(Error::domain("outer-product inverse needs a matching bound frame")); } - let count = crate::array::element_count(shape)?; - let layout = right.layout().axes(if first { split..right.shape().len() } else { 0..split }); - let mut keys = vec![None; right.shape().len()]; - for axis in 0..rank { keys[if first { axis } else { split + axis }] = bound.keys(axis).cloned(); } - let right = crate::keyed::reorder(right, &keys, false).described("outer-product bound keys must agree")?; - let mut result: Vec = Vec::with_capacity(count.max(1)); - for j in 0..if count == 0 { 1 } else { bound.len() } { - let a = Operand::Value(bound.at(j)); - let f = Operand::Function(f.clone()); - let operands = if first { [a, f] } else { [f, a] }; - let inverse = Function::new(FunctionNode::Composed(if first { OperatorKind::Before } else { OperatorKind::After }, operands), span)?.inverse(span)?; - for i in 0..count.max(1) { - let value = if count == 0 { right.prototype() } else { right.at(if first { j * count + i } else { i * bound.len() + j }) }; - let candidate = inverse.call_array(None, &value, span)?; - if j == 0 { result.push(candidate); } - else if !result[i].matches(&candidate, span)? { return Err(Error::domain("outer-product cells do not have a consistent inverse")); } - } - } - let prototype = result[0].prototype(); - if count == 0 { result.clear(); } - layout.collect(result, prototype).described("invalid outer-product inverse") -} - -fn inverse_scan(f: &Function, h: FoldKind, axis: Option<&Value>, seed: Option<&Value>, right: &Value, span: &Span) -> Result { - let axis = axis.map(|a| h.axis(Some(a), right)).transpose()?; - let axis = match FoldPlan::new(h, axis, seed, right, || unreachable!("a scan fills no lane"))?.1 { - FoldPlan::Lanes { lanes, .. } => lanes, FoldPlan::One(item) if seed.is_some() => return f.inverse(span)?.call_array(seed, &item, span), - _ => return Ok(right.clone()) - }; - let inverse = f.inverse(span)?; - if let Some(p) = f.as_primitive() { - let seed = seed.map(|s| p.weaken_value(s, right).unwrap_or_else(|| s.clone())); - if let Some(result) = crate::pervasive::inverse_scan(p, right, seed.as_ref(), &axis) { - return result.with_layout(right.layout().clone()).described("invalid inverse scan"); - } - } - crate::explain::itemwise(rustymath::events::Route::InverseScan, right.len()); - let mut data: Vec<_> = right.elements().collect(); - for i in 0..axis.outer { - for j in usize::from(seed.is_none())..axis.len { - for k in 0..axis.inner { - let offset = axis.offset(i, j, k); - let previous = if j == 0 { seed.unwrap().clone() } else { right.at(axis.offset(i, j - 1, k)) }; - data[offset] = inverse.call_array(Some(&previous), &right.at(offset), span)?; + if first && let FunctionNode::Applied { op: Operator::Kind(kind @ (Scan | ScanFirst)), .. } = function(&g)?.node() { + let operand = function(&g)?.operand(&origin, 0); + return inverse_scan(&origin, function(&operand)?, matches!(kind, ScanFirst), Some(&axes), left, right); } + Err(Error::domain("this axis-qualified function has no known inverse")) } - } - right.layout().collect(data, || right.prototype()).described("invalid inverse scan") -} - -/// The Key `derived`, `f⌸`. -pub(super) fn key(derived: &Function, left: Option<&Value>, right: &Value, span: &Span) -> Result { - let FunctionNode::Modified(_, Operand::Function(f)) = derived.node() else { unreachable!("a Key holds its operand") }; - let (keys, key_cells) = key_cells(left, right)?; - groups(f, keys, &key_cells, left, right, None, span) -} - -/// Key's keys, `left` or else `right`, with their major cells, once its arguments check. -fn key_cells<'v>(left: Option<&'v Value>, right: &'v Value) -> Result<(&'v Value, crate::search::Cells<'v>), Error> { - let keys = left.unwrap_or(right); - if keys.is_unit() || right.is_unit() { return Err(Error::new(Kind::Rank, "key arguments must have major cells")); } - if keys.shape()[0] != right.shape()[0] { return Err(Error::new(Kind::Length, "key arguments must have equal tallies")); } - Ok((keys, crate::search::Cells::of(keys, keys.shape().len() - 1).described("invalid key cells")?)) -} - -/// Key's fast combinations (`rewrite`): `GroupCounts` counts each group without building it, and `GroupFolds` folds each group of a vector in one pass, with no call for each group. Other arguments take Key's general path with the operand that the combination stands for, `≢⊢` or the fold of `⊢`, which reuses the class numbers that the fold read. -pub(super) fn grouped(s: crate::primitive::Special, left: Option<&Value>, right: &Value, span: &Span) -> Result { - use {crate::primitive::Special, rustymath::events::Route}; - let (keys, key_cells) = key_cells(left, right)?; - let (operand, numbered) = match s { - Special::GroupCounts if keys.shape()[0] > 0 => { - let sizes = crate::search::class_counts(&key_cells, span)?; - let value = Value::positions(vec![sizes.len()], keys.shape()[0] + 1, sizes.into_iter()).described("invalid key result")?; - super::rewrite::taken(Route::GroupCounts, s); - return Ok(Bound::from(value)); - } - Special::GroupFolds(by, first) => { - let suits = left.is_some() && keys.shape()[0] > 0 && right.shape().len() == 1 && !crate::transform::carried(None, right); - let numbered = if suits { Some(crate::search::numbered(&key_cells, span)?) } else { None }; - if let Some(result) = numbered.as_ref().and_then(|n| crate::pervasive::fold_groups(by.primitive(), right, n)) { - super::rewrite::taken(Route::GroupFolds, s); - return Ok(Bound::from(result)); - } - rustymath::events::declined(Route::GroupFolds, "arguments that don't suit it", right.len()); - (Function::new(FunctionNode::Fold(Function::primitive(by.primitive()), FoldKind { scan: false, first }), span)?, numbered) + Power if first => { + let count = second(); + if !count.is_atom() { return Err(Error::domain("this power has no known inverse")); } + let count = dispatch_in(&origin, Operation::Primitive { primitive: Primitive::Arithmetic(crate::number::Arithmetic::Minus), pairing: Pairing::Axes }, &[count.into_owned()], &[None])?; + loop_call(&origin, function(&g)?, &count, &args, &from, None) } - _ => (Function::primitive(Primitive::Tally), None), - }; - let f = Function::new(FunctionNode::Composed(OperatorKind::Atop, [Operand::Function(operand), Operand::Function(Function::primitive(Primitive::Identity(false)))]), span)?; - groups(&f, keys, &key_cells, left, right, numbered, span) -} - -/// Key's general path: `f` called on each group with its key, from the classes that `numbered` gives, or else that the key cells give. -fn groups( - f: &Function, keys: &Value, key_cells: &crate::search::Cells, left: Option<&Value>, right: &Value, numbered: Option, span: &Span -) -> Result { - let classes = match numbered { Some(n) => rustymath::search::Grouped::new(n.firsts, &n.numbers), None => crate::search::grouped(&key_cells, span)? }; - let count = classes.firsts.len(); - // A monadic Key's values are the positions along `right`, which are each group's rows themselves unless that axis has keys. - let values = match left { - Some(_) => Some(right.clone()), None if right.keys(0).is_some() => Some(crate::keyed::selectors(right, &[0]).described("invalid group indices")?), - None => None - }; - let width = values.as_ref().map_or(Ok(1), |v| generated_len(&v.shape()[1..]))?; - let select = |rows: &[u32]| { - let Some(values) = &values else { return Value::positions(vec![rows.len()], right.shape()[0], rows.iter().copied()) }; - let layout = values.layout().select(0, rows.iter().map(|&r| Some(r as usize)))?; - let mut data = crate::array::Gather::new(&[values], rows.len() * width); - data.rows(values, rows, width); - data.finish(layout, || values.prototype()) - }; - if count == 0 { - let cells = keys.cells(keys.shape().len() - 1).described("invalid key cells")?; - let x = cells.prototype().described("invalid key prototype")?; - let y = select(&[]).described("invalid key group")?; - let result = on_prototypes(f, Some(&if cells.shape().is_empty() { x.at(0) } else { x }), &y, span)?.array(span)?; - return Ok(Bound::from(Value::assemble(&[0], &[], &result).described("invalid key result")?)); - } - let firsts = &classes.firsts; - crate::explain::itemwise(rustymath::events::Route::KeyGroups, count); - let mut results = Vec::with_capacity(count); - let mut apply = |g: usize, y: Value, span: &Span| -> Result<(), Error> { - results.push(f.call_with(Some(&*key_cells.get(firsts[g])), y, span)?.array(span)?); - Ok(()) - }; - match &values { - None => { - for (g, y) in crate::search::class_positions(classes.groups, right.shape()[0]).described("invalid key group")?.into_iter().enumerate() { - apply(g, y, span)? - } - } - Some(_) => { for (g, rows) in classes.groups.each().enumerate() { apply(g, select(rows).described("invalid key group")?, span)? } } - } - Ok(Bound::from(Value::assemble(&[count], &results, &results[0]).described("invalid key result")?)) -} - -/// The primitive in `f`, when `f` is a primitive whose form for this valence is a pervasive function, or a `gᵇ` called with two arguments (`Function::dyadic_primitive`). Each and Outer call such a primitive on whole arrays. -fn pervasive_primitive(f: &Function, dyadic: bool) -> Option { - (if dyadic { f.dyadic_primitive() } else { f.as_primitive() }).filter(|p| p.facts(dyadic).pervasive()) -} - -pub(super) fn outer(operand: &Function, left: Option<&Value>, right: &Value, span: &Span) -> Result { - let left = left.ok_or_else(|| Error::new(ErrorKind::Syntax, "outer product needs a left argument"))?; - if left.is_atom() && right.is_atom() { return operand.call(Some(left), right, span); } - if let Some(p) = pervasive_primitive(operand, true) { return Ok(Bound::from(p.outer(left, right, span)?)); } - outer_items(operand, left, right, span) -} - -/// The outer product's general path: `operand` on each pair of an item of `left` and an item of `right` (`each_pair`). -// Out of line, as `each_items` is, so that `outer`'s frame holds only its fast paths. -#[inline(never)] -fn outer_items(operand: &Function, left: &Value, right: &Value, span: &Span) -> Result { - crate::program::trace::per_item(Some(left), right)?; - let agreement = Agreement::outer(left.layout(), right.layout()).described("outer product is too large")?; - // A batched try reads each argument laid over the result's frame (`batch::Arguments::laid`), as Each's does (`items_whole`). - let whole = || crate::batch::Arguments::laid(&agreement, Some(items_whole(left)), items_whole(right)); - each_pair(operand, agreement.len, agreement.layout.clone(), |i| Ok(agreement.values(Some(left), right, i)), whole, None, span) -} - -/// `f.g` with function operands. Primitives call the operation that they name (`Operation::call`). Other operands route to a running level, then take one batched call of `g` (`by_triples`), or else the general path (`Contraction::items`). -fn inner(f: &Function, g: &Function, left: Option<&Value>, right: &Value, span: &Span) -> Result { - if let (Some(pf), Some(pg)) = (f.as_primitive(), g.as_primitive()) { - return Operation::Product(pf, pg).call(Pairing::Axis(None), left, right, span).map(Bound::from); - } - if session().transforming() { - if let Some(result) = crate::transform::call(crate::transform::Op::Product, left, right, span) { return result.map(Bound::from); } - } - let Some(left) = left else { return expansion(f, g, right, span) }; - let c = Contraction::new(left, right)?; - if c.ordered && !left.is_singleton() && !right.is_singleton() { - if let Some(result) = by_triples(f, g, left, right, &c, span) { return result.map(Bound::from); } - } - c.items(left, right, f, g, span).map(Bound::from) -} - -/// The sizes and layout of `left f.g right`, which every route of a product reads first: `n` pairs along the contraction for each of `rows` rows of `left` and `cols` columns of `right`, giving `size` items laid out by `layout`. `positions` reads the items of `right` along the contraction in the order that `left` pairs them. `ordered` where the lengths agree, `positions` reads them in order, and there are items to compute, as routes on whole arrays need. -pub(crate) struct Contraction { nx: usize, n: usize, rows: usize, cols: usize, size: usize, layout: crate::array::Layout, positions: Mapping, ordered: bool } - -impl Contraction { - /// The contraction of the last axis of `left` with the first axis of `right`. A singleton argument pairs with every item along the other's axis. Other lengths that differ give LENGTH ERROR. - pub(crate) fn new(left: &Value, right: &Value) -> Result { - let nx = left.shape().last().copied().unwrap_or(1); - let ny = right.shape().first().copied().unwrap_or(1); - let positions = Mapping::contract(left.layout(), left.shape().len().saturating_sub(1), right.layout(), 0)?; - if nx != ny && !left.is_singleton() && !right.is_singleton() { return Err(Error::new(Kind::Length, "product contraction lengths must agree")); } - let n = if left.is_singleton() { ny } else { nx }; - let layout = left.layout().contracted(right.layout()); - let size = generated_len(layout.shape())?; - let rows = generated_len(&left.shape()[..left.shape().len().saturating_sub(1)])?; - let cols = generated_len(&right.shape()[usize::from(!right.is_unit())..])?; - generated_len(&[n.max(1), cols.max(1)])?; - Ok(Self { nx, n, rows, cols, size, ordered: nx == ny && n > 0 && size > 0 && matches!(positions, Mapping::Linear(1)), layout, positions }) - } - /// The general path: each item of the result is the left operand's fold of the right operand's results on the pairs of a row of `left` and a column of `right`. An empty result, or an empty contraction, calls the operands only for prototypes, in prototype mode. - pub(crate) fn items(&self, left: &Value, right: &Value, f: &Function, g: &Function, span: &Span) -> Result { - let Self { nx, n, rows, cols, size, .. } = *self; - let item = |a: &Value, offset| if n == 0 || a.is_empty() { a.prototype() } else { a.at(if a.is_singleton() { 0 } else { offset }) }; - let mut results = Vec::with_capacity(size.max(1)); - for i in 0..rows.max(1) { - let mut columns = vec![Vec::with_capacity(n.max(1)); cols.max(1)]; - for k in 0..n.max(1) { - for (j, column) in columns.iter_mut().enumerate() { - let rk = if n == 0 { 0 } else { self.positions.index(k) }; - column.push( - session().prototype_mode(size == 0 || n == 0, || g.call_with(Some(item(left, i * nx + k)), item(right, rk * cols + j), span)?.array(span))?); - } - } - for column in columns { - let paired = - if n == 0 { Value::empty(vec![0], column[0].prototype()) } else { Value::new(vec![n], column) }.described("invalid product cell")?; - results.push(session().prototype_mode(size == 0, || fold(f, FoldKind::REDUCE, None, None, &paired, span))?); - } - } - if self.layout.shape().is_empty() { return Ok(results.remove(0)); } - let prototype = results[0].zeroed(); - if size == 0 { results.clear(); } - self.layout.collect(results, prototype).described("invalid inner product result") + _ => Err(Error::domain("this function has no known inverse")), } } -/// `left f.g right` as one batched call of `g` on every triple of a row of `left`, a position along the contraction and a column of `right`, followed by the fold of `f` along the contraction by the fold's own route, for each chunk of rows (`by_rows`). `None` where a chunk's run falls back. After a fallback, the loop over rows runs from the start and keeps effects in order. -fn by_triples(f: &Function, g: &Function, left: &Value, right: &Value, c: &Contraction, span: &Span) -> Option> { - if !crate::batch::tries(c.rows * c.n * c.cols) { return None; } - by_rows(c, left, right, |x, y| { - let agreement = Agreement::new(x.layout(), y.layout()).ok()?; - let frame = agreement.layout.clone(); - let products = crate::batch::tried(g, || crate::batch::Arguments::laid(&agreement, Some((x, 0)), (y, 0)), &frame, false, span)?; - Some(products.and_then(|p| fold(f, FoldKind::REDUCE, Some(&Value::integer(1)), None, &p, span))) - }) -} - -/// `left f.g right` through `product`, which takes rows of `left` laid out as rows by contraction by 1 and `right` laid out as 1 by contraction by columns, and gives their products folded along the contraction. The rows go in chunks whose products hold at most `batch::CHUNK` items, or one row where a row's products hold more, and the chunks' results join along the rows. `None` where `product` gives `None` for a chunk. -fn by_rows( - c: &Contraction, left: &Value, right: &Value, mut product: impl FnMut(Value, Value) -> Option>) -> Option> { - use {crate::array::Layout, rustymath::gather::Steps}; - let Contraction { rows, n, cols, .. } = *c; - let per = (crate::batch::CHUNK / n.saturating_mul(cols).max(1)).max(1); - let y = right.relaid(Layout::from(vec![1, n, cols])).ok()?; - // One chunk lays out `left` as it is. Chunks take their rows from `left`'s items in order. - let flat = if per < rows { Some(left.relaid(Layout::from(vec![rows * n])).ok()?) } else { None }; - let mut parts = Vec::with_capacity(rows.div_ceil(per)); - for start in (0..rows).step_by(per) { - let count = per.min(rows - start); - let x = match &flat { - None => left.relaid(Layout::from(vec![rows, n, 1])), Some(flat) => crate::primitive::copied( - flat, Layout::from(vec![count * n]), &rustymath::agreement::Mapping::from(vec![Steps::Stride { start: start * n, len: count * n, step: 1 }])) - .and_then(|x| x.relaid(Layout::from(vec![count, n, 1]))) - }; - let x = match x.described("invalid product rows") { Ok(x) => x, Err(e) => return Some(Err(e)) }; - parts.push(match product(x, y.clone())? { Ok(p) => p, Err(e) => return Some(Err(e)) }); - } - let whole = if parts.len() == 1 { parts.pop().expect("one chunk") } else { - let mut joined = Gather::new(&parts.iter().collect::>(), rows * cols); - for part in &parts { joined.append(part); } - match joined.finish(Layout::from(vec![rows, cols]), || parts[0].prototype()).described("invalid product rows") { - Ok(v) => v, Err(e) => return Some(Err(e)) - } - }; - Some(if c.layout.shape().is_empty() { Ok(whole.at(0)) } else { whole.relaid(c.layout.clone()).described("invalid inner product result") }) +fn discards(kind: &OperatorKind, operands: &[Value], dyadic: bool) -> bool { + if matches!(kind, OperatorKind::Inverse) { return false; } + operands.iter().any(|operand| matches!(operand, Value::Function(f) if f.discards(dyadic))) } -/// `left f.g right` for primitives `f` and `g`, as `Operation::run` gives it: rustymath's inner-product kernel where it applies, then `g` once on rows of `left` and on `right` laid out against each other, followed by `f`'s fold along the contraction, where `g` is pervasive (`by_rows`), and otherwise the general path (`Contraction::items`). A running level takes each call that these make. -pub(crate) fn primitive_product(f: Primitive, g: Primitive, left: &Value, right: &Value, span: &Span) -> Result { - let c = Contraction::new(left, right)?; - if c.ordered && !left.is_unit() && !right.is_unit() && !crate::transform::carried(Some(left), right) { - let plan = crate::plan::product_on(f, g, left, right).map_err(Error::domain)?; - if let Some(result) = - crate::pervasive::inner(&plan, left, right, &rustymath::agreement::Frames::one(), [c.rows, c.n, c.cols], c.layout.shape().to_vec()) { - let result = result?; - return if c.layout.shape().is_empty() { Ok(result.at(0)) } else { result.with_layout(c.layout).described("invalid inner product result") }; - } +fn bitwise_call(origin: &Origin<&[Value]>, f: &Function, args: &[Value], _: &[Option]) -> Result { + let mut bits = 0; + for i in 0..if args.len() == 2 { 4 } else { 2 } { + let mut values = vec![Value::Number(crate::Number::from_bool(i & 1 != 0))]; + if args.len() == 2 { values.insert(0, Value::Number(crate::Number::from_bool(i & 2 != 0))); } + bits |= u8::from(f.call_bound(origin, &values, &vec![None; values.len()])?.read(crate::ReadKind::Value)?.boolean()?) << i; } - if c.ordered && !left.is_singleton() && !right.is_singleton() && g.facts(true).pervasive() { - let fold = Operation::Fold(f, FoldKind::REDUCE); - if let Some(result) = by_rows(&c, left, right, |x, y| Some(g.call(Some(&x), &y, span).and_then(|p| fold.call(Pairing::Axis(Some(1)), None, &p, span)))) - { return result; } - } - c.items(left, right, &Function::primitive(f), &Function::primitive(g), span) + dispatch_in(origin, Operation::Primitive { primitive: Primitive::Bitwise(bits), pairing: Pairing::Axes }, args, &vec![None; args.len()]) } -/// Monadic `f.g` for primitives `f` and `g`, as `Operation::run` and the combinations `Determinant` and `Permanent` give it: for a square matrix of numbers, the determinant of `-.×` (`primitive::determinant`) and, below 64 rows, the permanent of `+.×`, and otherwise the expansion by minors that function operands take (`expansion`), whose calls of the operation by its primitives a running level takes. -pub(crate) fn primitive_expansion(f: Primitive, g: Primitive, right: &Value, span: &Span) -> Result { - use rustymath::events::Route; - let route = match Operation::Product(f, g) { Operation::MINUS_TIMES => Some(Route::Determinant), Operation::PLUS_TIMES => Some(Route::Permanent), _ => None }; - if let Some(route) = route { - let square = matrix(right).filter(|&[m, n]| m == n && n > 0 && !crate::transform::carried(None, right)).map(|[_, n]| n); - let fast = match (route, square) { - (Route::Determinant, Some(n)) => crate::primitive::determinant(right, n)?, - (Route::Permanent, Some(n)) if n < 64 => permanent(right, n)?, - _ => None, - }; - let Some(value) = fast else { - rustymath::events::declined(route, "arguments that don't suit it", right.len()); - return expansion(&Function::primitive(f), &Function::primitive(g), right, span)?.array(span); - }; - if rustymath::events::running() { crate::explain::fast(route, format!("{}.{}", f.glyph(), g.glyph())); } - return Ok(value); - } - expansion(&Function::primitive(f), &Function::primitive(g), right, span)?.array(span) -} - -/// The permanent of the `n`×`n` matrix `right`, by rustymath's `permanent_with` at the matrix's own field, which `primitive::determinant` also reads, or `None` where it doesn't hold numbers. An integer permanent wraps where the call wraps (`Number::wrapped`). -fn permanent(right: &Value, n: usize) -> Result, Error> { - use crate::number::{Domain, Field, read, with_field}; - let Some((domain, width)) = Domain::of_arrays(&[right]) else { return Ok(None) }; - let settings = crate::number::Numeric::for_values([right])?; - let p = with_field!(domain, [Exact, Single, Double, Complex], T => rustymath::linalg::exact::permanent(&read::(right)?, n)?.number(width).wrapped(settings)); - Ok(Some(Value::Number(p))) -} - -/// `op` on each matrix of the stack `right`, or on each pair of matrices of the stacks `left` and `right`, as `op⍤2` pairs them, as `Operation::run` gives it: rustymath's kernels on whole stacks where they apply (`primitive::stacked`), and otherwise `op` on each matrix or pair, whose results mix as Rank mixes them. A running level takes each of those calls. -pub(crate) fn matrices(op: Operation, left: Option<&Value>, right: &Value, span: &Span) -> Result { - if !crate::transform::carried(left, right) { if let Some(result) = crate::primitive::stacked(op, left, right) { return result; } } - let pairs = CellPairs::new(left, right, [left.map_or(0, |a| within(a.shape().len(), 2)), within(right.shape().len(), 2)])?; - assembled(pairs.agreement.len, pairs.agreement.layout.clone(), Some("invalid rank result"), |i, _| { - let (x, y) = pairs.pair(i)?; - op.call(Pairing::Axis(None), x.as_ref(), &y, span) - }) -} - -/// Monadic `f.g` with function operands (`expanded`). -fn expansion(f: &Function, g: &Function, right: &Value, span: &Span) -> Result { - expanded(right, span, &mut |x, y| { - check()?; - match y { None => fold(g, FoldKind::REDUCE, None, None, x, span), Some(y) => inner(f, g, Some(x), y, span)?.array(span) } - }).map(Bound::from) -} - -/// Monadic `f.g` of `right`, as J defines it: the expansion of a matrix by minors along its first column. A vector is one column, and an atom one item. `call(x, Some(y))` is `x f.g y`, and `call(x, None)` is `g/x`. `expansion` calls it. `-.×` is the determinant and `+.×` the permanent, which `primitive_expansion` computes by faster algorithms. -pub(crate) fn expanded(right: &Value, span: &Span, call: &mut dyn FnMut(&Value, Option<&Value>) -> Result) -> Result { - crate::program::trace::per_item(None, right)?; - let [m, n] = matrix(right).ok_or_else(|| Error::new(Kind::Rank, "monadic f.g needs a matrix, a vector or a number"))?; +fn expansion(origin: &Origin<&[Value]>, f: &Function, g: &Function, right: &Value) -> Result { + let [m, n] = match *right.shape() { [] => [1, 1], [m] => [m, 1], [m, n] => [m, n], _ => return Err(Error::new(Kind::Rank, "monadic f.g needs a matrix, a vector or a number")) }; if m > 64 { return Err(Error::new(Kind::Limit, "monadic f.g expands at most 64 rows")); } - let items: Vec = (0..m * n).map(|i| right.at(i)).collect(); + let flat = dispatch_in(origin, Operation::Read(ReadOp::Ravel), &[right.clone()], &[None])?; + let items = (0..m * n).map(|i| dispatch_in(origin, Operation::Primitive { primitive: Primitive::Bracket, pairing: Pairing::Axes }, + &[Value::integer(i as i64), flat.clone()], &[None, None])).collect::>>()?; let rows = if m == 0 { 0 } else { u64::MAX >> (64 - m) }; - expand(&items, [m, n], rows, &right.prototype(), &mut HashMap::new(), span, call) -} - -/// The rows and columns that monadic `f.g` reads `right` as: a vector is one column, and a number one item. -pub(super) fn matrix(right: &Value) -> Option<[usize; 2]> { - match *right.shape() { [] => Some([1, 1]), [m] => Some([m, 1]), [m, n] => Some([m, n]), _ => None } + expanded(origin, f, g, &items, [m, n], rows, &right.prototype(), &mut HashMap::new()) } -/// The expansion of the rows in the set `rows` of the `m`×`n` matrix `items`, from column `m` minus the size of `rows` on. While columns are left, it is `call` between that column's items in `rows` and the expansions without each of those rows, as `f.g`. With no columns left, it is `call` on no items, as `g/`. `memo` holds the expansion of each set of rows already expanded. -fn expand( - items: &[Value], [m, n]: [usize; 2], rows: u64, prototype: &Value, memo: &mut HashMap, span: &Span, - call: &mut dyn FnMut(&Value, Option<&Value>) -> Result) -> Result { +fn expanded(origin: &Origin<&[Value]>, f: &Function, g: &Function, items: &[Value], [m, n]: [usize; 2], rows: u64, + prototype: &Value, memo: &mut HashMap) -> Result { if let Some(value) = memo.get(&rows) { return Ok(value.clone()); } - let vector = |items: Vec| { - if items.is_empty() { Value::empty(vec![0], prototype.clone()) } else { Value::new(vec![items.len()], items) }.described("invalid expansion") - }; + let vector = |items: &[Value]| if items.is_empty() { Ok(Value::empty([0], prototype.clone())?) } + else { dispatch_in(origin, Operation::Join(crate::primitive::Notation::Strand), items, &vec![None; items.len()]) }; let column = m - rows.count_ones() as usize; - let value = if column == n { call(&vector(vec![])?, None)? } else { - let set: Vec = (0..m).filter(|&i| rows >> i & 1 == 1).collect(); - let first = vector(set.iter().map(|&i| items[i * n + column].clone()).collect())?; - let minors = set.iter().map(|&i| expand(items, [m, n], rows & !(1 << i), prototype, memo, span, call)).collect::, _>>()?; - call(&first, Some(&vector(minors)?))? + let value = if column == n { + fold_call(origin, g, &OperatorKind::Reduce, &[vector(&[])?], &[None])? + } else { + let set: Vec<_> = (0..m).filter(|&i| rows >> i & 1 == 1).collect(); + let first = vector(&set.iter().map(|&i| items[i * n + column].clone()).collect::>())?; + let minors = set.iter().map(|&i| expanded(origin, f, g, items, [m, n], rows & !(1 << i), prototype, memo)).collect::>>()?; + product_call(origin, f, g, &[first, vector(&minors)?], &[None, None])? }; memo.insert(rows, value.clone()); Ok(value) } -/// The cell ranks that rank operand `ranks` gives, as `[monad, left, right]`. ∞ and ¯∞ become `isize::MAX` and `isize::MIN`, which `within` clamps. -fn cell_ranks(ranks: &Value) -> Result<[isize; 3], Error> { - if ranks.shape().len() > 1 { return Err(Error::new(Kind::Rank, "rank operand must be a unit or vector")); } - if !(1..=3).contains(&ranks.len()) { return Err(Error::new(Kind::Length, "rank operand needs one to three items")); } - let ranks = ranks.elements().map(|e| match e.observed().map_err(Error::from).map(|e| (e.infinity(), e))? { - (Some(negative), _) => Ok(if negative { isize::MIN } else { isize::MAX }), - (None, Plain::Number(n)) => { - n.integer().map(|n| isize::try_from(n).unwrap_or(if n < 0 { isize::MIN } else { isize::MAX })).described("cell ranks must be whole numbers") - } - _ => Err(Error::domain("cell ranks must be numeric")), - }).collect::, _>>()?; - Ok(match ranks.as_slice() { [r] => [*r, *r, *r], [q, r] => [*r, *q, *r], [p, q, r] => [*p, *q, *r], _ => unreachable!() }) -} - -/// The rank of the cells of cell rank `k` in an array or cell of rank `n`. A negative `k` counts down from `n`. ∞ gives `n`, and ¯∞ gives 0. -fn within(n: usize, k: isize) -> usize { if k < 0 { n.saturating_sub(k.unsigned_abs()) } else { n.min(k as usize) } } - -pub(super) fn rank(operand: &Function, ranks: &Value, left: Option<&Value>, right: &Value, span: &Span) -> Result { - let [p, q, r] = cell_ranks(ranks)?; - let yr = within(right.shape().len(), if left.is_some() { r } else { p }); - let xr = left.map_or(0, |a| within(a.shape().len(), q)); - if let Some(result) = whole(operand, left, right, [xr, yr], span) { return result; } - rank_cells(operand, left, right, [xr, yr], span) -} - -/// The cells of rank `xr` of `left` and of rank `yr` of `right`, paired by the agreement of their frames, as Rank pairs them. Rank (`rank_cells`) and stacks of matrices (`matrices`) share it. -struct CellPairs<'a> { xs: Option>, ys: crate::array::Cells<'a>, agreement: Agreement } - -impl<'a> CellPairs<'a> { - fn new(left: Option<&'a Value>, right: &'a Value, [xr, yr]: [usize; 2]) -> Result { - let ys = right.cells(yr).described("invalid rank cells")?; - let xs = left.map(|a| a.cells(xr)).transpose().described("invalid rank cells")?; - let agreement = - Agreement::new(&xs.as_ref().map_or_else(Default::default, |c| c.frame_layout()), &ys.frame_layout()).described("frames do not agree")?; - Ok(Self { xs, ys, agreement }) - } - /// Pair `i`'s cells. Where the frames are empty, the one pair holds each argument's prototype cell. - fn pair(&self, i: usize) -> Result<(Option, Value), Error> { - let [x, y] = self.agreement.positions(i, || [self.xs.as_ref().map_or(0, |c| c.len()), self.ys.len()]); - let cell = |c: &crate::array::Cells<'_>, j: Option| j.map_or_else(|| c.prototype(), |j| c.get(j)).described("invalid rank cells"); - Ok((self.xs.as_ref().map(|c| cell(c, x)).transpose()?, cell(&self.ys, y)?)) - } -} - -/// The results of `call` on pairs 0 to `len`, assembled in `layout` as its items. Where `mixed` gives a message, they mix into one array whose numbers keep their kinds, as Rank mixes them, and the message reports results that don't mix. With no pairs, `call` runs once with `true`, on pair 0's prototypes, and that result gives the fill. Rank, Each (`each_pair`), stacks of matrices (`matrices`) and function arrays (`function_array`) share it. -fn assembled( - len: usize, layout: crate::array::Layout, mixed: Option<&'static str>, mut call: impl FnMut(usize, bool) -> Result) -> Result { - let empty = len == 0; - let mut data = Gather::items(len); - let mut first = None; - for i in 0..len.max(1) { let item = call(i, empty)?; if empty { first = Some(item) } else { data.add(item) } } - let items = data.finish(layout, || first.expect("a call on prototypes").zeroed()).described("invalid result")?; - match mixed { Some(what) => items.mix(crate::array::Widening::Items, None).described(what), None => Ok(items) } -} - -/// Rank's general path: `operand` on each pair of cells of ranks `xr` and `yr`, paired by the agreement of the cells' frames (`each_pair`). -// Out of line, as `each_items` is, so that `rank`'s frame holds only its fast paths (`whole`). -#[inline(never)] -fn rank_cells(operand: &Function, left: Option<&Value>, right: &Value, [xr, yr]: [usize; 2], span: &Span) -> Result { - let pairs = CellPairs::new(left, right, [xr, yr])?; - each_pair( - operand, pairs.agreement.len, pairs.agreement.layout.clone(), |i| pairs.pair(i), - || crate::batch::Arguments::laid(&pairs.agreement, left.map(|a| (a.clone(), xr)), (right.clone(), yr)), Some("invalid rank result"), span) -} - -/// `operand⍤k` as calls on whole arguments, for operands where those calls give what one call for each cell gives. `xr` and `yr` are the cell ranks. It gives `None` for other operands, and Rank then calls `operand` once for each cell. -fn whole(operand: &Function, left: Option<&Value>, right: &Value, [xr, yr]: [usize; 2], span: &Span) -> Option> { - // With no frame, the one cell is the whole argument. - if yr == right.shape().len() && left.is_none_or(|x| xr == x.shape().len()) { return Some(operand.call(left, right, span)); } - if let Some(result) = decomposed(operand, left, right, [xr, yr], span) { return Some(result); } - if let Some(result) = operand.operation().and_then(|op| whole_cells(op, left.map(|x| (x, xr)), (right, yr), span)) { return Some(result.map(Bound::from)); } - if let Some(result) = pervasive_cells(operand, left, right, [xr, yr], span) { return Some(result); } - // A fold with a number on its left takes the number as every lane's seed. - if let Some(x) = left { - let (Value::Number(_), FunctionNode::Fold(g, kind)) = (x, operand.node()) else { return None }; - return folded_cells(pervasive_primitive(g, true)?, *kind, &[kind.default_axis(yr)], Some(x), right, yr, span).map(|result| result.map(Bound::from)); - } - cell_rule(operand, right, yr, span).map(|result| result.map(Bound::from)) -} - -/// `operation` on each pair of cells of `left` and `right`, each given with the rank of its cells, as calls on the whole arguments. An argument with no axes besides its cells' is the same in every cell. The other arguments' frames agree, as Rank pairs them. `None` where no route covers the call. Rank (`whole`) and batched maps (`batch::call`) share it. The routes, in order: -/// - `f.g` where rustymath has a product kernel for the pair, `⌹` and `-.×` on stacks of matrices: rustymath's kernels, which read compact storage (`primitive::stacked`), and for stacks that a running level carries, such as `∂`'s, the level's rule for every pair of matrices at once (`Operation::call` with `Pairing::Matrices`). -/// - `f.g` on cells of rank 1, where `g` is pervasive: `f` folds `g`'s pairing of the cells along their axis (`vector_product`). -/// - A pervasive primitive, in one call with padded frames (`padded_call`). -/// - Any other primitive with one argument, or with a left argument that is the same in every cell, by its rule for cells (`primitive_cells`). -/// - A primitive whose two arguments' frames match, or whose right argument is the same in every cell, by its rule for pairs of cells (`paired_cells`). An empty argument takes the path through cells, because a call on prototype cells gives its result's shape. -pub(crate) fn whole_cells(operation: Operation, left: Option<(&Value, usize)>, (y, yk): (&Value, usize), span: &Span) -> Option> { - let x = left.map(|(x, _)| x); - if yk == 2 && left.is_none_or(|(_, xk)| xk == 2) && operation.stacks(x.is_some()) { - if crate::transform::carried(x, y) { return Some(operation.call(Pairing::Matrices, x, y, span)); } - if let Some(result) = crate::primitive::stacked(operation, x, y) { return Some(result); } - } - if let (Operation::Product(f, g), Some((x, 1)), 1) = (operation, left, yk) { return vector_product(f, g, x, y, span); } - let Operation::Primitive(p) = operation else { return None }; - let frame = |v: &Value, k: usize| v.shape().len() - k; - if p.facts(left.is_some()).pervasive() { - let fx = left.map(|(x, xk)| [frame(x, xk)]); - if let Some(result) = padded_call(p, x.zip(fx.as_ref().map(|f| &f[..])), (y, &[frame(y, yk)]), span) { return Some(result); } - } - let Some((x, xk)) = left else { return primitive_cells(p, None, y, yk, span) }; - let (fx, fy) = (frame(x, xk), frame(y, yk)); - if fx == 0 { return primitive_cells(p, Some(x), y, yk, span); } - if x.is_empty() || y.is_empty() || (fy > 0 && x.layout().axes(0..fx) != y.layout().axes(0..fy)) { return None; } - paired_cells(p, x, xk, y, yk, span).map(Ok) -} - -/// `f.g` on cells of rank 1 of `x` and `y`, where `g` is pervasive: `g` pairs the cells' items in one call, and `f` folds along the paired axis. That's the route of a fold of a paired product, so `+.×` on large cells runs as one product (`program::contraction`). Plain cells pair with padded frames (`padded_call`). Cells whose axis has the same name in both arguments pair by that name, with their frames paired as Rank pairs them. `None` for empty arguments, cells of different lengths, cells with different names and a `g` that isn't pervasive, which take the path through cells. -fn vector_product(f: Primitive, g: Primitive, x: &Value, y: &Value, span: &Span) -> Option> { - if !g.facts(true).pervasive() || x.is_empty() || y.is_empty() { return None; } - let (lx, ly) = (x.shape().len() - 1, y.shape().len() - 1); - let (nx, ny) = (x.shape()[lx], y.shape()[ly]); - let name = y.axis_name(ly); - if (nx != ny && nx != 1 && ny != 1) || x.axis_name(lx) != name { return None; } - let paired = match name { Some(_) => g.call(Some(x), y, span), None => padded_call(g, Some((x, &[lx])), (y, &[ly]), span)? }; - let paired = match paired { Ok(paired) => paired, Err(e) => return Some(Err(e)) }; - let axis = match name { Some(n) => paired.layout().axis_named(n)?, None => paired.shape().len() - 1 }; - Some(Operation::Fold(f, FoldKind::REDUCE).call(Pairing::Axis(Some(axis)), None, &paired, span)) -} - -/// `f` on each rank-`k` cell of `y`, as calls on the whole of `y` along the cells' axes. The frame's axes keep their keys and names. `None` where no rule covers `f`. -fn cell_rule(f: &Function, y: &Value, k: usize, span: &Span) -> Option> { - use {crate::primitive::CellRule, FunctionNode as N, Operand::{Function as F, Value as V}, OperatorKind::{Atop, Before}}; - match f.node() { - N::Primitive(p) => primitive_cells(*p, None, y, k, span), - N::Fold(g, kind) => folded_cells(pervasive_primitive(g, true)?, *kind, &[kind.default_axis(k)], None, y, k, span), - N::Composed(Before, [V(a), F(g)]) => primitive_cells(g.as_primitive()?, Some(a), y, k, span), - N::Composed(Atop, [F(g), F(h)]) => { - // A fold of each cell's items, joined into one axis as monadic `,` joins them (`CellRule::Every`), folds all the cell axes at once. That takes folds in turn where the primitive allows them. Folds in turn reach sliding folds over windows. - if let (N::Fold(op, kind), Some(p)) = (g.node(), h.as_primitive()) { - let frame = y.shape().len() - k; - let axes = Value::integers(vec![k], (frame..frame + k).map(|a| a as i64).collect()).expect("axes are integers"); - if p.facts(false).cells == CellRule::Every && !kind.scan && k > 0 && folds_whole(op, None, y) { - return Some(fold(op, *kind, Some(&axes), None, y, span)); - } - } - let inner = match cell_rule(h, y, k, span)? { Ok(inner) => inner, Err(e) => return Some(Err(e)) }; - let ranks = Value::integer((inner.shape().len() - (y.shape().len() - k)) as i64); - Some(rank(g, &ranks, None, &inner, span).and_then(|result| result.array(span))) - } - // Each cell's `(f c) g (h c)`: each tine over the whole of `y`, then `g` on each pair of the tines' cells, as one Rank call. A constant tine, as the `2` of `2×⊢`, is one cell that pairs with every cell of the other. - N::Fork([f, g, h]) => { - if f.as_constant().is_some() && h.as_constant().is_some() { return None; } - let frame = y.shape().len() - k; - let tine = |t: &Function| -> Option> { - if let Some(a) = t.as_constant() { return Some(Ok((a.clone(), a.shape().len()))); } - match cell_rule(t, y, k, span)? { Ok(v) => { let r = v.shape().len().checked_sub(frame)?; Some(Ok((v, r))) } Err(e) => Some(Err(e)) } - }; - let (right, rr) = match tine(h)? { Ok(t) => t, Err(e) => return Some(Err(e)) }; - let (left, lr) = match tine(f)? { Ok(t) => t, Err(e) => return Some(Err(e)) }; - let ranks = Value::integers(vec![2], vec![lr as i64, rr as i64]).expect("ranks are integers"); - Some(rank(g, &ranks, Some(&left), &right, span).and_then(|result| result.array(span))) - } - _ => None, - } -} - -/// The primitive `p` on each rank-`k` cell of `y`, as calls on the whole of `y` along the cells' axes, with `left` as the left argument of every cell where it has one. The frame's axes keep their keys and names. `None` where no rule covers `p`. The routes for whole arguments use it (`whole_cells`), and so does Rank for a monadic operand (`cell_rule`). -fn primitive_cells(p: Primitive, left: Option<&Value>, y: &Value, k: usize, span: &Span) -> Option> { - use crate::primitive::CellRule; - let frame = y.shape().len() - k; - // Cell axes `from..from + count`, numbered as axes of the whole of `y`. - let axes = - |from: usize, count: usize| Value::integers(vec![count], (frame + from..frame + from + count).map(|a| a as i64).collect()).expect("axes are integers"); - // `y` with each frame axis kept in place and each cell axis `j` sent to cell axis `to[j]`, by dyadic `⍉`. Cell axes that `to` leaves out follow, as `⍉` places them. - let moved = |to: &[usize]| { - let to: Vec = (0..frame).chain(to.iter().map(|d| frame + d)).map(|a| a as i64).collect(); - Primitive::Transpose.call(Some(&Value::integers(vec![to.len()], to).expect("axes are integers")), y, span) - }; - let Some(a) = left else { - if p.extends_at(None, k) { return Some(p.call(None, y, span)); } - return match p.facts(false).cells { - CellRule::First if k > 0 => Some(p.call_axes(None, y, &axes(0, 1), span)), - CellRule::Every => Some(p.call_axes(None, y, &axes(0, k), span)), - CellRule::Encloses if k > 0 => Some(p.call_axes(None, y, &axes(0, k), span)), - CellRule::Length | CellRule::Shape => cell_constant(p, &y.layout().axes(frame..frame + k)) - .map(|c| crate::agreement::spread(&c, &y.layout().axes(0..frame)).described("invalid cell shapes")), - // Monadic `⍉` reverses each cell's axes. - CellRule::Axes => Some(moved(&(0..k).rev().collect::>())), - CellRule::Orders if k > 0 => crate::primitive::ordered_cells(p, y, k), - CellRule::Same if k == 0 => Some(Ok(y.clone())), - CellRule::Table => { - // Keys and names on a cell's axes would stay on its table's axes. - let cell = y.layout().axes(frame..y.shape().len()); - if cell.has_names() || cell.has_keys() { return None; } - let table = [cell.shape().first().copied().unwrap_or(1), cell.shape().iter().skip(1).product()]; - Some(y.relaid(y.layout().axes(0..frame).concat(&crate::array::Layout::from(table))).described("invalid table")) - } - CellRule::Head if y.is_simple() && !y.is_empty() => { - if k == 0 { Some(Ok(y.clone())) } else { primitive_cells(Primitive::Index, Some(&Value::integer(0)), y, k, span) } - } - CellRule::Enlist if y.is_simple() => primitive_cells(Primitive::Ravel, None, y, k, span), - _ => None, - }; - }; - if a.keys(0).is_some() { return None; } - if p.extends_at(Some(a), k) { return Some(p.call(Some(a), y, span)); } - let vector = a.shape().len() <= 1; - match p.facts(true).cells { - // A left argument of a form that runs along leading cell axes has one item for each axis it applies to. A named selector stays on the cell path, because the whole array's frame axes could pair with its name. - CellRule::Leading if vector && a.len() <= k && !a.elements().any(|e| e.layout().has_names()) => Some(p.call_axes(Some(a), y, &axes(0, a.len()), span)), - CellRule::First if a.shape().is_empty() && k > 0 => Some(p.call_axes(Some(a), y, &axes(0, 1), span)), - CellRule::Along if vector && k > 0 => Some(p.call_axes(Some(a), y, &axes(0, 1), span)), - // Each destination lies inside the cell. - CellRule::Axes if vector => a.as_items().nonnegative_integers::().ok().filter(|to| to.iter().all(|&d| d < k)).map(|to| moved(&to)), - // A reshape reads each cell's items in order, repeating them where the shape holds more: a view where it keeps every item, and otherwise one gather of the flattened cells. - CellRule::Reshape if vector => { - let shape: Vec = a.as_items().nonnegative_integers().ok()?; - let (outer, cell) = y.shape().split_at(frame); - let (n, k) = (crate::array::count(cell), crate::array::count(&shape)); - let to = |v: Value| v.relaid(y.layout().axes(0..frame).concat(&crate::array::Layout::from(shape.clone()))); - if k == n { return Some(to(y.clone()).described("invalid cell reshape")); } - if n == 0 { return None; } - let flat = |m: usize| y.layout().axes(0..frame).concat(&crate::array::Layout::from([m])); - let steps: Vec<_> = rustymath::gather::Steps::row_major(outer, n, 0) - .chain([rustymath::gather::Steps::padded(0, k, n, 1, rustymath::structure::Edge::Wrap)]).collect(); - Some( - y.relaid(flat(n)).and_then(|f| crate::primitive::copied(&f, flat(k), &rustymath::agreement::Mapping::from(steps))).and_then(to) - .described("invalid cell reshape")) - } - // The haystack on the left is the same in every cell. Each cell holds whole needles, of the haystack's major cells' rank or more, so the frame stays outside the needles. - CellRule::Searches { haystack_left: true } if !a.is_unit() && k + 1 >= a.shape().len() => Some(p.call(Some(a), y, span)), - // Any other form pairs each cell with the left argument, the same in every cell. - _ => paired_cells(p, a, a.shape().len(), y, k, span).map(Ok), - } -} - -/// The primitive `p` on each pair of cells of `x` and `y`, of ranks `xk` and `yk`, as one call on the whole arguments. An argument with no axes besides its cell's is the same in every cell. Any other argument has the frame: the axes before its cells, the same in both arguments. A form that takes an argument that is the same in every cell gets it repeated over the frame first (`agreement::spread`). The routes for whole arguments use it (`whole_cells`). These forms have a rule: -/// - A form that joins its arguments' cells (`CellRule::Join`) joins the whole arguments along the cell axis that one call on a pair of cells joins. An argument with no axes extends to every cell. Otherwise the cells differ in rank by at most 1, and the lower one gains an axis of length 1 where they join, as in a call on one pair. -/// - A form that extends, at cell ranks of at least its own. A call on two arguments pairs their frames as Rank does, wherever the call succeeds (`declared_ranks_match_rank` in `tests/core.rs`). -/// - `⌷`, `⊃` and bracket indexing whose positions or right argument differ by cell select as one bracket selection of the whole (`primitive::paired_select`). -/// - `∊`, with the same haystack on the right in every cell, searches all the cells of `x` at once, where each cell holds whole needles (`CellRule::Searches`). -/// - `⍳`, `∊` and `⍸` whose haystack differs from cell to cell search each cell's needles in its own haystack (`primitive::searched_cells`). -/// - `≡` and `≢` compare every pair of cells at once (`CellRule::Matches`). -/// -/// `None` for any other form, for frames whose names one whole call would pair otherwise than Rank (`frames_pair`), and where the call fails. The calls on each cell then give the result or the error. Names on cell axes pair in the whole call as calls on cells pair them. -fn paired_cells(p: Primitive, x: &Value, xk: usize, y: &Value, yk: usize, span: &Span) -> Option { - use crate::primitive::CellRule; - if !frames_pair(x.layout(), x.shape().len() - xk, y.layout(), y.shape().len() - yk) { return None; } - if let Some(selected) = crate::primitive::paired_select(p, x, xk, y, yk, span) { return Some(selected); } - let frame = if x.shape().len() > xk { x.layout().axes(0..x.shape().len() - xk) } else { y.layout().axes(0..y.shape().len() - yk) }; - let unit = |v: &Value| v.shape().is_empty(); - // An argument that is the same in every cell, repeated over the frame. One with no axes needn't be, because it extends to every cell. - let spread = |v: &Value, k: usize| if v.shape().len() > k || unit(v) { Some(v.clone()) } else { crate::agreement::spread(v, &frame).ok() }; - match p.facts(true).cells { - CellRule::Join { last } => { - if !unit(x) && !unit(y) && xk.abs_diff(yk) > 1 { return None; } - let k = xk.max(yk); - // Atoms join into a vector, so each argument with axes gains a last axis of length 1 first. - let joined = |v: &Value, vk: usize| { - let v = spread(v, vk)?; - if k > 0 || unit(&v) { Some(v) } else { v.relaid(v.layout().concat(&crate::array::Layout::from(vec![1]))).ok() } - }; - let axis = frame.shape().len() + if last { k.max(1) - 1 } else { 0 }; - p.call_axes(Some(&joined(x, xk)?), &joined(y, yk)?, &Value::integer(axis as i64), span).ok() - } - // The haystack on the right is the same in every cell, and each cell of `x` holds whole needles. - CellRule::Searches { haystack_left: false } if y.shape().len() == yk && !unit(y) && xk + 1 >= yk => p.call(Some(x), y, span).ok(), - // Each cell holds its own haystack. - CellRule::Searches { haystack_left } => crate::primitive::searched_cells(p, x, xk, y, yk, &frame, haystack_left), - CellRule::Matches { not } => crate::primitive::matched_cells(x, xk, y, yk, &frame, not), - _ => { - let [rx, ry] = p.info().dyad.ranks.map(usize::from); - if !p.facts(true).extends() || xk < rx || yk < ry { return None; } - p.call(Some(&spread(x, xk)?), &spread(y, yk)?, span).ok() - } - } -} - -/// A fold or scan by `p` of each rank-`k` cell of `y` along the cell's axis, or a fold along several of its axes, given in `axes`, with `seed` as each lane's left argument, as one fold of the whole of `y` along those axes past its frame, where `p` is a pervasive primitive and `y` is simple. A pervasive primitive's fold or scan of each cell of a simple array is simple, so Rank's mix of those results gives what the whole-array fold gives. One axis takes the call of the operation (`Operation::call`). Several take the fold along several axes (`fold`), whose routes include one pass over windows (`folded_along`). `None` otherwise. Rank (`cell_rule`) and batched maps (`batch::call`) share it. -pub(crate) fn folded_cells( - p: Primitive, kind: FoldKind, axes: &[usize], seed: Option<&Value>, y: &Value, k: usize, span: &Span) -> Option> { - if k == 0 || !y.is_simple() || !p.facts(true).pervasive() { return None; } - let frame = y.shape().len() - k; - if let [axis] = axes { return Some(Operation::Fold(p, kind).call(Pairing::Axis(Some(frame + axis)), seed, y, span)); } - let axes = Value::integers(vec![axes.len()], axes.iter().map(|&a| (frame + a) as i64).collect()).expect("axes are integers"); - Some(fold(&Function::primitive(p), kind, Some(&axes), seed, y, span)) -} - -/// What `p` gives a cell laid out by `cell`, where its rule for cells says that depends only on the cell's shape: the cell's length for `≢` and its shape for monadic `⍴`. `None` for any other rule, and for the shape of a cell with named axes, which the names key. Rank lays it over the frame (`primitive_cells`), and a batched run gives it as every cell's result (`batch::call`). -pub(crate) fn cell_constant(p: Primitive, cell: &crate::array::Layout) -> Option { - use crate::primitive::CellRule; - let shape = cell.shape(); - match p.facts(false).cells { - CellRule::Length => Some(Value::integer(shape.first().map_or(1, |&n| n as i64))), - CellRule::Shape if !cell.has_names() => Value::integers(vec![shape.len()], shape.iter().map(|&n| n as i64).collect()).ok(), _ => None - } -} - -/// `f⍤k` in one call, where `f` is a pervasive primitive, or Rank nested down to one, as in `(-⍤1)⍤2 1`. Each level of Rank pairs its arguments' frames as leading axes, so each argument's frame splits into one run of axes for each level (`frames`), and one padded call pairs them all (`padded_call`). -fn pervasive_cells(operand: &Function, left: Option<&Value>, right: &Value, ranks: [usize; 2], span: &Span) -> Option> { - let (f, levels) = rank_levels(operand, left.is_some(), ranks)?; - let (fx, fy) = (left.map(|x| frames(x, &levels, 0)), frames(right, &levels, 1)); - padded_call(pervasive_primitive(f, left.is_some())?, left.zip(fx.as_deref()), (right, &fy), span).map(|result| result.map(Bound::from)) -} - -/// Whether one call on whole arguments laid out as `x` and `y`, whose frames are their first `fx` and `fy` axes, pairs their names as Rank does, frame with frame and cell with cell: each name on either frame appears in the other argument only on its frame axis at the same position, if at all. Otherwise the whole call adds a differently named frame axis after the cells, where Rank adds it to the frame, or pairs a frame axis with a cell axis of the same name. -fn frames_pair(x: &crate::array::Layout, fx: usize, y: &crate::array::Layout, fy: usize) -> bool { - let fits = |a: &[Option>], fa: usize, b: &[Option>], fb: usize| { - a.iter().take(fa).enumerate() - .all(|(i, n)| n.as_ref().is_none_or(|n| b.iter().enumerate().all(|(j, m)| m.as_ref().is_none_or(|m| (m == n) == (j == i && j < fb))))) - }; - fits(x.names(), fx, y.names(), fy) && fits(y.names(), fy, x.names(), fx) -} - -/// The pervasive primitive `p` in one call on `left` and `right`, each with the lengths of its frame's runs of axes. A monad maps each item, whatever its cells. For a dyad, each argument gets axes of length 1 after each run, up to the length of the other argument's run, so the call pairs frames with frames and cells with cells (`agreement::padded`). Names on cell axes pair in that call as separate calls on cells pair them. Names on frame axes pair in that call as Rank pairs them where `frames_pair` allows, and other named frames take the path through cells. So does an empty argument, because its result's shape comes from a call on prototype cells, and so do arguments whose padded layouts don't agree. That path reports the error. -fn padded_call(p: Primitive, left: Option<(&Value, &[usize])>, (y, fy): (&Value, &[usize]), span: &Span) -> Option> { - if left.iter().map(|&(x, _)| x).chain([y]).any(|a| a.is_empty()) { return None; } - let Some((x, fx)) = left else { return Some(p.call(None, y, span)) }; - let (x, y) = (crate::agreement::padded(x, fx, fy), crate::agreement::padded(y, fy, fx)); - // Padding lines the frames up axis by axis. - let frame: usize = fx.iter().zip(fy).map(|(n, m)| n.max(m)).sum(); - if !frames_pair(x.layout(), frame, y.layout(), frame) { return None; } - // A unit agrees with any argument, and so does an argument laid out the same. - if !(x.shape().is_empty() || y.shape().is_empty() || x.layout() == y.layout()) { Agreement::new(x.layout(), y.layout()).ok()?; } - Some(p.call(Some(&x), &y, span)) -} - -/// The pervasive primitive that `operand` is, or that Rank nested in `operand` comes down to, with each level's cell ranks as `[left, right]`, starting from `ranks`. -fn rank_levels<'a>(operand: &'a Function, dyadic: bool, ranks: [usize; 2]) -> Option<(&'a Function, Vec<[usize; 2]>)> { - let (mut f, mut levels) = (operand, vec![ranks]); - while pervasive_primitive(f, dyadic).is_none() { - let FunctionNode::Composed(OperatorKind::Rank, [Operand::Function(g), Operand::Value(k)]) = f.node() else { return None }; - let [m, q, r] = cell_ranks(k).ok()?; - let [xr, yr] = *levels.last()?; - levels.push([within(xr, q), within(yr, if dyadic { r } else { m })]); - f = g; - } - Some((f, levels)) -} - -/// The length of each level's frame in `a`: the axes between the cells of the level above and the level's own cells. `side` is 0 for the left argument's cell ranks, 1 for the right's. -fn frames(a: &Value, levels: &[[usize; 2]], side: usize) -> Vec { - let mut above = a.shape().len(); - levels.iter().map(|l| std::mem::replace(&mut above, l[side]) - l[side]).collect() -} - -/// `•decompose⍤2` on a whole stack of matrices in one call, with its kind bound to it or as one whole left cell (`primitive::stacked_decompositions`). `xr` and `yr` are the cell ranks. `None` where it takes the general path of one call for each cell. -fn decomposed(operand: &Function, left: Option<&Value>, right: &Value, [xr, yr]: [usize; 2], span: &Span) -> Option> { - // The stacked kernels read compact storage, which a dual doesn't have: its cells go through the operand one by one (`derivative`). - if yr != 2 || crate::transform::carried(left, right) { return None; } - let decompose = |f: &Function| matches!(f.node(), FunctionNode::System(s) if s.entry == Some(crate::system::Entry::DECOMPOSE)); - let kind = match (operand.node(), left) { - (FunctionNode::System(_), Some(kind)) if decompose(operand) && xr == kind.shape().len() => kind, - (FunctionNode::Composed(OperatorKind::Before, [Operand::Value(kind), Operand::Function(f)]), None) if decompose(f) => kind, _ => return None - }; - Some(crate::primitive::stacked_decompositions(kind, right, span)?.map(Bound::from)) -} - -/// `f⍠spec`: a primitive with its own axis form, a reduce or scan along an axis, or `f` on the cells that the axes make. A keyed `spec` is an options record (`with_options`). -pub(super) fn along_axes(f: &Function, spec: &Value, left: Option<&Value>, right: &Value, span: &Span) -> Result { - if spec.keys(0).is_some() { return with_options(f, spec, left, right, span); } - match f.node() { - FunctionNode::Primitive(p) if p.facts(left.is_some()).axes => Ok(Bound::from(p.call_axes(left, right, spec, span)?)), - FunctionNode::Fold(operand, kind) => Ok(Bound::from(fold(operand, *kind, Some(spec), left, right, span)?)), _ => cell_axes(f, spec, left, right, span) - } -} - -/// `f⍠R` with an options record `R`, whose one option, `fill`, gives the item that `f` reads at each position that it fills (`filled`). -fn with_options(f: &Function, spec: &Value, left: Option<&Value>, right: &Value, span: &Span) -> Result { - let options = crate::data::Options::new("⍠", Some(spec), None, &["fill"])?; - let fill = options.values.get("fill").ok_or_else(|| Error::domain("⍠'s options record needs fill"))?; - filled(f, spec, fill, left, right, span) -} - -/// `f` called with `fill` at each position that it fills, from the options record `spec`. The fill reaches a primitive or a primitive's inverse that fills (`Primitive::with_fill`), through an argument that `a↣f` or `f↢a` binds, and through `f⍠A`. A primitive with an axis form of its own reads `A` with the fill. Any other `f⍠A` calls `f⍠spec` on the cells that `A` makes, as `along_axes` calls `f`. A function that gives `A` runs on the arguments first. In `(f⍠R1)⍠R2`, `R1`'s fill applies. `↕` and `⌺` take their fill in their own options records. Any other function reads no fill, and is a DOMAIN error. -fn filled(f: &Function, spec: &Value, fill: &Value, left: Option<&Value>, right: &Value, span: &Span) -> Result { - use {FunctionNode as N, Operand::{Function as F, Value as V}, OperatorKind::{After, Axis, Before, Stencil}}; - let primitive = |p: Primitive, inverse: bool, axes: Option<&Value>| p.with_fill(inverse, left, right, fill, axes, span).map(Bound::from); - match f.node() { - N::Primitive(Primitive::Windows) | N::Composed(Stencil, _) => Err(Error::domain("↕ and ⌺ take their fill in their own options records")), - N::Primitive(p) => primitive(*p, false, None), - N::Inverse(g) if let N::Primitive(p) = g.node() => primitive(*p, true, None), - N::Composed(Before, [V(_), F(_)]) | N::Composed(After, [F(_), V(_)]) if left.is_some() => Err(Error::new(ErrorKind::Syntax, ONE_ARGUMENT)), - N::Composed(Before, [V(a), F(g)]) => filled(g, spec, fill, Some(a), right, span), - N::Composed(After, [F(g), V(a)]) => filled(g, spec, fill, Some(right), a, span), - N::Composed(Axis, [F(g), axes @ F(_)]) => filled(&Function::new(N::Axis(g.clone(), computed(axes, left, right, span)?), span)?, spec, fill, left, right, span), - N::Axis(g, inner) if inner.keys(0).is_some() => with_options(g, inner, left, right, span), - N::Axis(g, axes) => match g.node() { - N::Primitive(p) if p.facts(left.is_some()).axes => primitive(*p, false, Some(axes)), - _ => cell_axes(&Function::new(N::Axis(g.clone(), spec.clone()), span)?, axes, left, right, span), - }, - _ => Err(Error::domain("this function reads no fill")), - } -} - -/// Apply `f` to the cells made of the selected axes. The other axes form the frame. The selected axes must exist in the higher-rank argument. An axis given by name is selected wherever each argument's layout puts it, and an axis given by number at the higher-rank argument's position. An argument that lacks one of them is one whole cell. -pub(super) fn cell_axes(f: &Function, spec: &Value, left: Option<&Value>, right: &Value, span: &Span) -> Result { - let target = match left { Some(x) if x.shape().len() > right.shape().len() => x, _ => right }; - let order = target.shape().len(); - let selected = crate::primitive::axes(&crate::primitive::resolve_axes(spec, target)?, order)?; - let others = |n: usize| (0..n).filter(|i| !selected.contains(i)); - let named = crate::primitive::names_axes(spec); - // Move each argument's selected axes to the end, so that rank sees them as the cells. - let cells = |x: &Value| -> Result<(Value, usize), Error> { - let n = x.shape().len(); - let own = if named { crate::primitive::resolve_axes(spec, x).ok().and_then(|s| crate::primitive::axes(&s, n).ok()) } - else { selected.iter().all(|&i| i < n).then(|| selected.clone()) }; - match own { Some(own) => Ok((moved_last(x, &own)?, own.len())), None => Ok((x.clone(), n)) } - }; - let (y, yr) = cells(right)?; - let x = left.map(cells).transpose()?; - let ranks: Vec<_> = x.iter().map(|(_, r)| *r as i64).chain([yr as i64]).collect(); - let ranks = Value::integers(vec![ranks.len()], ranks).described("invalid cell ranks")?; - let result = rank(f, &ranks, x.as_ref().map(|(x, _)| x), &y, span)?.array(span)?; - // Rank pairs the frames by name, so named axes can build a grid larger than the target's frame. That result keeps Rank's order: the paired frame, then the cells. - let frame = order - selected.len(); - let frame_layout = |v: &Value, r: usize| v.layout().axes(0..v.shape().len() - r); - let paired = Agreement::new(&x.as_ref().map_or_else(Default::default, |(x, r)| frame_layout(x, *r)), &frame_layout(&y, yr)).map_or(frame, |a| a.layout.shape().len()); - if paired != frame { return Ok(Bound::from(result)); } - // Result cells of the selected rank return to the selected axes. Other result cells start at the first selected axis. - let cell = result.shape().len().saturating_sub(frame); - let destination: Vec<_> = if cell == selected.len() { others(order).chain(selected.iter().copied()).collect() } else { - let start = selected.first().map_or(frame, |&s| others(order).take_while(|&i| i < s).count()); - (0..frame + cell).map(|j| { - if j < start { j } else if j < frame { j + cell } else { start + j - frame } - }).collect() - }; - Ok(Bound::from(crate::primitive::transpose_axes(destination, &result)?)) -} - -/// `x` with the `selected` axes moved to the end, in their order. -fn moved_last(x: &Value, selected: &[usize]) -> Result { - let n = x.shape().len(); - crate::primitive::reorder(x, &(0..n).filter(|i| !selected.contains(i)).chain(selected.iter().copied()).collect::>()) -} - -/// Calls `operand` on the argument pairs `pair(0)`, `pair(1)` and so on up to `len`, and assembles the results in `layout`. They become the result's items, or with `mixed` they mix into one array whose numbers keep their kinds, as Rank mixes them. `mixed` gives the message for results that don't mix. It first tries one call on batches of all the cells, from the whole arguments that `whole` builds (`batch::tried`), except for one cell of atoms. With no pairs it calls the operand once on prototypes (`on_prototypes`), on `pair(0)`, for the result's prototype. A call that gives no result contributes `⍬`. -fn each_pair( - operand: &Function, len: usize, layout: crate::array::Layout, pair: impl Fn(usize) -> Result<(Option, Value), Error>, - whole: impl FnOnce() -> Option, mixed: Option<&'static str>, span: &Span) -> Result { - // One cell of atoms runs faster on its own than through a batched call. A cell of rank 1 or more doesn't. - let atoms = len == 1 && { let (x, y) = pair(0)?; x.as_ref().is_none_or(Value::is_atom) && y.is_atom() }; - if !atoms { - if let Some(result) = crate::batch::tried(operand, whole, &layout, mixed.is_some(), span) { return result.map(Bound::from); } - } - let items = assembled(len, layout, mixed, |i, empty| { - let (x, y) = pair(i)?; - result_item(if empty { on_prototypes(operand, x.as_ref(), &y, span)? } else { operand.call_with(x, y, span)? }.value) - })?; - Ok(if mixed.is_some() { Bound::from(items) } else { Bound::new(Binding::Value(items)) }) -} - -/// A call's result as an item of a map's result. No result gives `⍬`. -// Each map calls this once per cell. Out of line, even with `#[inline]`, it cost unbatched maps about 50 instructions per cell. -#[inline(always)] -fn result_item(result: Binding) -> Result { - match result { - Binding::Value(a) => Ok(a), Binding::Function(f) => Ok(Value::Function(f)), - Binding::NoResult => Ok(crate::syntax::zilde(Numeric::current().float(0.0))), - _ => Err(Error::new(ErrorKind::Syntax, "the operand must return an array, function or no result")) - } -} - -/// Calls each function among `array`'s items on the whole arguments, in ravel order, and mixes the results as Rank does, with `array`'s layout as their frame. Any other item is its own result. The function of a unit returns its result as it is, as Rank does for a call on whole arguments. -pub(super) fn function_array(array: &Value, left: Option<&Value>, right: &Value, span: &Span) -> Result { - if array.is_unit() { - let Value::Function(f) = array.at(0) else { unreachable!("a function array's unit holds a function") }; - return f.call(left, right, span); - } - let items = assembled(array.len(), array.layout().clone(), Some("invalid function array result"), |i, _| match array.at(i) { - Value::Function(f) => result_item(f.call(left, right, span)?.value), item => Ok(item) - })?; - Ok(Bound::from(items)) -} - -pub(super) fn each(operand: &Function, left: Option<&Value>, right: &Value, span: &Span) -> Result { - if right.is_atom() && left.is_none_or(Value::is_atom) { return operand.call(left, right, span); } - if pervasive_primitive(operand, left.is_some()).is_some() { return operand.call(left, right, span); } - each_items(operand, left, right, span) -} - -/// Each's general path: `operand` on each pair of items, paired by the agreement of the arguments' frames (`each_pair`). -// Out of line, so that `each`'s frame holds only its fast paths. Recursion through them, as in Each on a scalar, keeps that frame on the browser's stack, whose size limits the depth of recursion (`tests/wasm.mjs`). -#[inline(never)] -fn each_items(operand: &Function, left: Option<&Value>, right: &Value, span: &Span) -> Result { - crate::program::trace::per_item(left, right)?; - let agreement = Agreement::new(left.map_or(&Default::default(), Value::layout), right.layout()).described("frames do not agree")?; - each_pair( - operand, agreement.len, agreement.layout.clone(), |i| Ok(agreement.values(left, right, i)), - || crate::batch::Arguments::laid(&agreement, left.map(items_whole), items_whole(right)), None, span) -} - -/// An argument of Each or of an outer product as their batched try reads it, with the rank of its cells. Both pass their operand each item, where Rank passes each cell. A scalar gives every cell its one item, whatever that item's rank. A nested array whose items are compact arrays of one layout and dtype gives one compact array that holds them, so that the try runs as Rank does on the array of rows. Any other argument gives its cells of rank 0. -fn items_whole(a: &Value) -> (Value, usize) { - if a.shape().is_empty() && !a.is_atom() { - let item = a.at(0); - let rank = item.shape().len(); - return (item, rank); - } - stacked(a).unwrap_or_else(|| (a.clone(), 0)) -} - -/// One compact array whose cells are the items of the nested array `a`, and their rank, where every item is a compact array of one layout and dtype: each cell then holds exactly an item, with no padding or widening. It gathers the items in one pass and gives up at the first that differs. -fn stacked(a: &Value) -> Option<(Value, usize)> { - use crate::array::{Gather, Items, Widening}; - if !a.holds_values() { return None; } - let Items::Values(items @ [first, ..]) = a.as_items() else { return None }; - let (dtype, layout, size) = (first.dtype().filter(|_| !first.is_atom())?, first.layout(), first.len()); - let mut data = Gather::with_widening(&[first], items.len() * size, Widening::Items); - for item in items { - if item.is_atom() || item.dtype() != Some(dtype) || item.layout() != layout { return None; } - data.extend(item, 0..size); - } - Some((data.finish(a.layout().concat(layout), || first.prototype()).ok()?, layout.shape().len())) -} - -/// `operand` on each item of `right`, with `left` whole for each: how a builtin that takes items maps over a right argument that holds several (`primitive::Item::maps`). It's Each with `left` enclosed. -pub(super) fn each_whole_left(operand: &Function, left: Option<&Value>, right: &Value, span: &Span) -> Result { - let left = left.map(Value::enclose).transpose().described("invalid left argument")?; - each_items(operand, left.as_ref(), right, span) +pub(crate) fn primitive_expansion(origin: &Origin<&[Value]>, f: Primitive, g: Primitive, right: &Value) -> Result { + expansion(origin, &Function::primitive(f), &Function::primitive(g), right) } -/// The identity of a fold by `p` of items like `prototype`, which fills a fold's empty lanes. -fn identity(p: Primitive, prototype: &Value, span: &Span) -> Result { - use crate::primitive::Identity::*; - let id = p.info().dyad.identity.ok_or_else(|| Error::domain("this function has no reduction identity"))?; - // A float identity, such as the ¯∞ of `⌈`, takes the width that a weak float takes beside these items under NEP 50. - let number = |n: f64| Ok(Value::Number(crate::Number::float(n, Numeric::current().width_of([prototype.float_width()])))); - match (id, prototype) { - (Empty(first), _) => { - let axis = (!first).then(|| prototype.shape().len().saturating_sub(1)); - Primitive::Replicate.call_axis(Some(&Value::integer(0)), prototype, axis, span) +fn inverse_scan(origin: &Origin<&[Value]>, f: &Function, first: bool, axes: Option<&Value>, seed: Option<&Value>, right: &Value) -> Result { + let inverse = f.inverse(&Span::whole(Source::new("", "")))?; + if right.is_unit() { return match seed { Some(seed) => inverse.call_bound(origin, &[seed.clone(), right.clone()], &[None, None]), None => Ok(right.clone()) }; } + let axis = match axes { + Some(axes) => { + let axes = axes.read(crate::ReadKind::Structure)?; + let selected = crate::primitive::axes(&crate::primitive::resolve_axes(&axes, right.layout())?, right.rank())?; + if selected.len() != 1 { return Err(Error::domain("inverse scan requires one axis")); } selected[0] } - (_, Value::Function(_)) => Err(Error::domain("function elements have no numeric reduction identity")), - (_, a @ Value::Array(_)) => { - let data = a.elements().map(|e| identity(p, &e, span)).collect::, _>>()?; - let fill = identity(p, &a.prototype(), span)?; - a.layout().collect(data, fill).described("invalid identity") - } - (Boolean(b), _) => Ok(Value::Number(crate::Number::from_bool(b))), - (Logical(b), Value::Number(value)) if value.as_bool().is_some() => Ok(Value::Number(crate::Number::from_bool(b))), - (Infinity(positive), _) => number(if positive { f64::INFINITY } else { f64::NEG_INFINITY }), - (Number(n), Value::Number(value)) => Ok(Value::Number(value.like(n))), - (Logical(b), Value::Number(value)) => Ok(Value::Number(value.like(b.into()))), - (Number(n), _) => number(f64::from(n)), - (Logical(b), _) => number(f64::from(u8::from(b))), - } -} - -pub(crate) fn fold(operand: &Function, kind: FoldKind, axis: Option<&Value>, left: Option<&Value>, right: &Value, span: &Span) -> Result { - let primitive = operand.dyadic_primitive(); - if left.is_none() && session().transforming() { - if crate::transform::carried(None, right) { if let Some(folded) = in_turn(operand, kind, axis, right, span)? { return Ok(folded); } } - if primitive.is_none() { - if let Some(result) = crate::transform::call(crate::transform::Op::Fold(kind, axis), None, right, span) { return result; } - } - } - let axis = axis.map(|a| crate::primitive::resolve_axes(a, right)).transpose()?; - if !kind.scan { - if let Some(axes) = axis.as_ref().filter(|a| !a.is_singleton()) { - if folds_whole(operand, left, right) { - if left.is_none() { if let Some(folded) = in_turn(operand, kind, Some(axes), right, span)? { return Ok(folded); } } - return fold_ravels(operand, kind, axes, left, right, span); - } - let mut reduction = Function::new(FunctionNode::Fold(operand.clone(), kind), span)?; - if let Some(seed) = left { reduction = before(seed.clone(), reduction, span)?; } - let ravelled = Function::new( - FunctionNode::Composed(OperatorKind::Atop, [Operand::Function(reduction), Operand::Function(Function::primitive(Primitive::Ravel))]), span)?; - // The seed binds first, so the general axis rule doesn't split it along the axes. - return cell_axes(&ravelled, axes, None, right, span)?.array(span); - } - } - let along = kind.axis(axis.as_ref(), right)?; - let axis = axis.is_some().then_some(along); - match primitive { - Some(p) => Operation::Fold(p, kind).call(Pairing::Axis(axis), left, right, span), None => fold_array(operand, kind, axis, left, right, span) - } -} - -/// Whether a fold by `operand` gives a simple result for each cell of `right`: a pervasive primitive on a simple array, with no seed or an atom seed. Other folds can give arrays or nested results. Folds over several axes, and Rank's rules for whole arrays, then call the fold once for each cell. -fn folds_whole(operand: &Function, left: Option<&Value>, right: &Value) -> bool { - right.is_simple() && left.is_none_or(Value::is_atom) && pervasive_primitive(operand, true).is_some() -} - -/// A fold along several axes by a primitive that `pervasive::folds` names (`+`, `×`, `⌈`, `⌊`, or a bitwise and, or, xor or xnor), through `folded_along`. It serves an argument that a running level carries, whose rules fold along one axis, and any simple array, which then needs no move of the axes last, as `fold_ravels` makes. `None` for any other fold. -#[cold] -#[inline(never)] -pub(crate) fn in_turn(operand: &Function, kind: FoldKind, axis: Option<&Value>, right: &Value, span: &Span) -> Result, Error> { - let Some(axis) = axis.filter(|_| !kind.scan) else { return Ok(None) }; - let Some(p) = operand.as_primitive().filter(|&p| crate::pervasive::folds(p).is_some()) else { return Ok(None) }; - let axes = crate::primitive::resolve_axes(axis, right)?; - if axes.is_singleton() { return Ok(None); } - folded_along(p, kind, &crate::primitive::axes(&axes, right.shape().len())?, right, span).map(Some) -} - -/// The fold by `p`, a primitive that `pervasive::folds` names, of `right` along `axes`. Adjacent axes merge into one axis of a view, which moves no item, and one fold runs along it. A `+` fold of a pending or traced `×` then contracts them all as one product (`program::contraction`). Axes that aren't adjacent contract as one product too, where the `×` is traced (`trace::contracted`) or pending (`fusion::fold`). Where no product folds a pending `×`, its first axis folds as one fold would, and the others follow. Other axes, and the axes of a copy, pending or traced, such as windows, fold one at a time from the last. A batch folds instead as one fold of its whole along all the axes (`batch::folded`), which takes this function's routes for the whole. These operands are associative and commutative. Folding a merged axis, or one axis at a time, therefore gives the fold of the ravelled cells. A fold along several axes (`in_turn`) and a cotangent's sum along a broadcast's spread axes (`linear::copied_back`) both fold here. -pub(crate) fn folded_along(p: Primitive, kind: FoldKind, axes: &[usize], right: &Value, span: &Span) -> Result { - if let Some(folded) = crate::batch::folded(right, p, kind, axes, span) { return folded; } - let mut axes = axes.to_vec(); - axes.sort_unstable_by(|a, b| b.cmp(a)); - let without = |done: &[usize]| right.layout().axes((0..right.shape().len()).filter(|i| !done.contains(i))); - // Two axes of a pending copy of windows fold in one pass over its source, where folding one at a time would build the first fold's result. - if let [a, b] = axes[..] && let Some(folded) = crate::fusion::fold_windows(right, p, kind, [b, a]) { - return folded.with_layout(without(&axes)).described("invalid fold keys"); - } - let along = |a: usize, v: &Value| Operation::Fold(p, kind).call(Pairing::Axis(Some(a)), None, v, span); - // Adjacent axes merge into one axis of a view, and one fold runs along it. A copy, pending or traced, such as windows, keeps folding one axis at a time, because a block stage takes short folds over its axes as its steps. - let copy = crate::fusion::source(right).is_some() || right.carrier().is_some_and(crate::transform::Carrier::copies); - if let (Some(&last), Some(&first)) = (axes.first(), axes.last()) && axes.len() > 1 && last + 1 - first == axes.len() && !copy { - let len = right.shape()[first..=last].iter().product(); - let merged = right.relaid(right.layout().replace(first..last + 1, &crate::array::Layout::from(vec![len]))).described("invalid fold keys")?; - return along(first, &merged); - } - let (mut folded, mut rest) = (right.clone(), &axes[..]); - if p == Primitive::Arithmetic(crate::number::Arithmetic::Plus) && !kind.scan && axes.len() > 1 && !copy { - if let Some(product) = crate::program::trace::contracted(p, kind, &axes, right) { - return product.with_layout(without(&axes)).described("invalid fold keys"); - } - // The fold of a pending array keeps its lanes regular, as one fold's plan does where its lanes hold two or more items. - if axes.iter().all(|&a| right.shape()[a] > 1) && let Some((product, done)) = crate::fusion::fold(right, p, kind, &axes, None) { - (folded, rest) = (product.with_layout(without(&axes[..done])).described("invalid fold keys")?, &axes[done..]); - } - } - rest.iter().try_fold(folded, |folded, &a| along(a, &folded)) -} - -/// A fold over several axes, as one fold of each cell's ravel. The selected axes move last, in their order, and merge into one axis. -fn fold_ravels(operand: &Function, kind: FoldKind, axes: &Value, left: Option<&Value>, right: &Value, span: &Span) -> Result { - let selected = crate::primitive::axes(axes, right.shape().len())?; - let others: Vec = (0..right.shape().len()).filter(|i| !selected.contains(i)).collect(); - let mut shape: Vec = others.iter().map(|&i| right.shape()[i]).collect(); - shape.push(selected.iter().map(|&i| right.shape()[i]).product()); - let ravels = crate::primitive::reshape_to(&shape, &moved_last(right, &selected)?, span)?; - let last = Value::integer(others.len() as i64); - let folded = fold(operand, kind, Some(&last), left, &ravels, span)?; - if others.is_empty() { return Ok(folded); } - folded.with_layout(right.layout().axes(others.iter().copied())).described("invalid fold keys") -} - -/// What a fold or scan of `right` meets along axis `axis`, or by default along its own axis. Every fold route decides it this way before its own kernels, identities and calls. -enum FoldPlan { - /// A fold of a unit with no axis given, or a scan of an atom: its one item, which only a seed changes. A fold puts the item on the seed's left, and a scan puts it on the seed's right. - One(Value), - /// The argument's items unchanged, in the result's shape: a scan of an empty array, a scan of lanes of one item with no seed, or a fold of them where no level carries the argument. - Whole(Value), - /// A fold with nothing left to compute: no results, or lanes of no items, each of which gives the seed or the operand's identity. - Done(Value), - /// The lanes to fold or scan, and the result's shape. - Lanes { lanes: Lanes, shape: Vec }, -} - -impl FoldPlan { - /// The plan, with the axis it runs along. `identity` gives the result of a lane of no items where there is no seed. - fn new( - kind: FoldKind, axis: Option, seed: Option<&Value>, right: &Value, identity: impl FnOnce() -> Result - ) -> Result<(usize, Self), Error> { - let rank = right.shape().len(); - let along = axis.unwrap_or(kind.default_axis(rank)); - let whole = right.is_unit() && axis.is_none(); - if whole && !kind.scan { return Ok((along, Self::One(right.at(0)))); } - let lanes = if whole { Lanes { outer: 1, len: 1, inner: 1 } } else { Lanes::new(right.shape(), along)? }; - if kind.scan { - let plan = if right.is_atom() { Self::One(right.clone()) } - else if right.is_empty() || (seed.is_none() && lanes.len == 1) { Self::Whole(right.clone()) } - else { Self::Lanes { lanes, shape: right.shape().to_vec() } }; - return Ok((along, plan)); - } - let mut shape = right.shape().to_vec(); - shape.remove(along); - let size = generated_len(&shape)?; - let plan = if size == 0 { Self::Done(Value::empty(shape, right.prototype()).described("invalid empty fold")?) } - else if lanes.len == 0 { - let item = match seed { Some(seed) => seed.clone(), None => identity()? }; - Self::Done(Value::new(shape, vec![item; size]).described("invalid identity result")?) - } else if seed.is_none() && lanes.len == 1 && !crate::transform::carried(None, right) { - Self::Whole(right.relaid(crate::array::Layout::from(shape)).described("invalid fold result")?) - } else { Self::Lanes { lanes, shape } }; - Ok((along, plan)) - } -} - -/// A fold or scan by an operand that isn't a primitive, which `primitive_fold` takes: one batched map over the lanes where it can (`by_lanes`), and otherwise the operand on each pair of items. Only a seed fills an empty lane, because such an operand has no identity. -fn fold_array(operand: &Function, kind: FoldKind, axis: Option, seed: Option<&Value>, right: &Value, span: &Span) -> Result { - let (along, plan) = FoldPlan::new(kind, axis, seed, right, || Err(Error::domain("this function has no reduction identity")))?; - let result = match plan { - FoldPlan::One(item) => match seed { - Some(seed) if kind.scan => operand.call_with(Some(seed), item, span)?.array(span)?, Some(seed) => operand.call_with(Some(item), seed, span)?.array(span)?, None => item - }, - FoldPlan::Whole(items) => items, - FoldPlan::Done(result) => result, - FoldPlan::Lanes { lanes, shape } => match by_lanes(operand, kind, seed, right, along, &lanes, span) { - Some(result) => result?, - None => item_lanes(kind.scan, &lanes, shape, seed, right, |x, y| operand.call_with(Some(x), y, span)?.array(span), crate::transform::carried(None, right))?, - }, - }; - folded(result, kind, along, right) -} - -/// A fold or scan by the primitive `p` along `axis`, or by default the fold's own axis, with `seed` as each lane's left argument: by fusion and the kernels where they apply, and otherwise by `p` on each pair of items, whose calls a running level takes as it takes any primitive call. A weak seed takes the width of `right`'s floats, as `p` gives it beside each item. Under static kinds, a lane of one item gives the item at the kind that `p` computes (`pervasive::lone`). `Operation::run` runs a fold or scan through it. -pub(crate) fn primitive_fold(p: Primitive, kind: FoldKind, axis: Option, seed: Option<&Value>, right: &Value, span: &Span) -> Result { - use crate::number::Arithmetic::{Plus, Times}; - let seed = seed.map(|s| p.weaken_value(s, right).unwrap_or_else(|| s.clone())); - let seed = seed.as_ref(); - let call = |x: Value, y: Value| p.call(Some(&x), &y, span); - let lone = |v: Value| if seed.is_none() { crate::pervasive::lone(p, v) } else { v }; - // Kernels that read numbers directly leave a value that a level carries to the path through items. - let active = crate::transform::carried(None, right); - let (along, plan) = FoldPlan::new(kind, axis, seed, right, || identity(p, &right.prototype(), span))?; - let mut last = None; - let result = match plan { - FoldPlan::One(item) => match seed { Some(seed) if kind.scan => scan_step(p, call)(seed.clone(), item)?, Some(seed) => call(item, seed.clone())?, None => lone(item) }, - FoldPlan::Whole(items) => lone(items), - FoldPlan::Done(result) => result, - FoldPlan::Lanes { lanes, shape } if kind.scan => { - if let Some(result) = (!active).then(|| crate::pervasive::scan(p, right, seed, &lanes)).flatten() { - result - } else if let (Primitive::Arithmetic(op), false, true) = (p, active, right.all_numbers() && seed.is_none_or(|a| matches!(a, Value::Number(_)))) { - number_lanes(op, true, right, seed, &lanes, shape)? - } else { item_lanes(true, &lanes, shape, seed, right, scan_step(p, call), active)? } - } - FoldPlan::Lanes { lanes, shape } => { - // Where `p`'s kernel folds in any order, a number seed joins the fold's value, as one more item would. The fold then takes the fusion and kernel routes of a fold with no seed. The seed joins after `folded`, where a vector's fold is one atom, because a call on atoms costs much less than one on a rank-0 array. A level that carries `right` keeps the seed in its items, because its kernels don't apply there. So do Booleans under a `ᵇ` table that `sets_high_bits`: they fold by the logical function, and a number seed turns the fold to the integer function. Fixed kinds need the seed before the fold chooses its wrapping width, so fusion folds it in as each lane's last item. - let fixed_seed = seed.is_some_and(|s| matches!(s, Value::Number(_)) && (right.fixed_width().is_some() || s.fixed_width().is_some())); - let seed = match seed { - Some(s @ Value::Number(_)) if !fixed_seed && !active && crate::pervasive::seed_last(p, right) => { last = Some(s); None } s => s - }; - match (seed.is_none() || fixed_seed).then(|| crate::fusion::fold(right, p, kind, &[along], seed)).flatten() { - Some((fused, _)) => fused, - None => { - let kernel = !active && (seed.is_none() || fixed_seed); - // Float sums and products take the kernel for any number of items, because they fold in any order. Reading the items to see that would compute a pending argument, which the kernel reads more cheaply, so lanes of two or more items take it without asking. - let any = || { - matches!(p, Primitive::Arithmetic(Plus | Times)) - && matches!(right.as_items(), crate::array::Items::Compact(rustymath::buffer::View::Floats(_))) - }; - let result = match ((kernel && (lanes.len >= 2 || any())).then(|| crate::pervasive::fold(p, right, along, seed)).flatten(), p) { - (Some(result), _) => result, - (None, Primitive::Arithmetic(op)) if kernel && right.all_numbers() => number_lanes(op, false, right, seed, &lanes, shape)?, - _ => item_lanes(false, &lanes, shape, seed, right, call, active)?, - }; - if lanes.len == 1 { lone(result) } else { result } - } - } - } - }; - let result = folded(result, kind, along, right)?; - match last { Some(s) => call(result, s.clone()), None => Ok(result) } -} - -/// The value of a fold or scan of `right` along axis `along`, from `result`, its lanes' results, where every fold route ends, fused folds included. A fold of a vector gives its one item, and a fold of a unit gives `result`. Any other result takes the layout that `Layout::folded` gives. -fn folded(result: Value, kind: FoldKind, along: usize, right: &Value) -> Result { - if !kind.scan && right.shape().len() == 1 { return Ok(result.at(0)); } - if !kind.scan && right.is_unit() { return Ok(result); } - result.with_layout(right.layout().folded(kind.scan, along)).described("invalid fold or scan keys") -} - -/// A fold or scan by `apply` on each pair of items in turn, where neither a kernel nor a batched map takes it, with `seed` as each lane's left argument, laid out by `shape`. Over a batch (`active`), each result is a batch: a fold gives one for each lane of the cells, and a scan one for each stage. They join into one batch (`batch::joined`). Where it runs on items rather than batches, it records an `itemwise` row in `•explain`. -fn item_lanes( - scan: bool, lanes: &Lanes, shape: Vec, seed: Option<&Value>, right: &Value, apply: impl FnMut(Value, Value) -> Result, active: bool -) -> Result { - crate::program::trace::per_item(seed, right)?; - if !active { crate::explain::itemwise(rustymath::events::Route::ItemLanes, right.len()); } - let data = if scan { scan_items(lanes, seed.cloned(), |i| right.at(i), apply)? } else { fold_items(lanes, seed, |i| right.at(i), apply)? }; - let value = Value::new(shape, data).described(if scan { "invalid scan result" } else { "invalid fold result" })?; - Ok(if active { crate::batch::joined(&value).unwrap_or(value) } else { value }) -} - -/// A fold or scan by `op` of plain numbers, with an optional `seed`: the general path for an arithmetic primitive when every item is a number. -fn number_lanes(op: crate::number::Arithmetic, scan: bool, right: &Value, seed: Option<&Value>, axis: &Lanes, shape: Vec) -> Result { - crate::explain::itemwise(rustymath::events::Route::ItemLanes, right.len()); - let number = |e: Value| { let Value::Number(n) = e else { unreachable!() }; n }; - let (item, seed) = (|i| number(right.at(i)), seed.cloned().map(number)); - let (p, t) = (Primitive::Arithmetic(op), Numeric::current()); - let step = |x: crate::Number, y: crate::Number| { check()?; p.number(Some(&x), &y, t).map_err(Error::from) }; - let data = if scan { scan_items(axis, seed, item, scan_step(p, step))? } else { fold_items(axis, seed.as_ref(), item, step)? }; - Value::new(shape, data.into_iter().map(Value::Number).collect()).described("invalid numeric result") -} - -/// Each lane of `axis` reduced from the right, starting from `seed` or else the lane's last item: `apply(item, result)`. -fn fold_items(axis: &Lanes, seed: Option<&T>, item: impl Fn(usize) -> T, mut apply: impl FnMut(T, T) -> Result) -> Result, Error> { - let mut data = Vec::with_capacity(axis.outer * axis.inner); - for i in 0..axis.outer { - for k in 0..axis.inner { - let item = |j| item(axis.offset(i, j, k)); - let (mut result, end) = match seed { Some(seed) => (seed.clone(), axis.len), None => (item(axis.len - 1), axis.len - 1) }; - for j in (0..end).rev() { result = apply(item(j), result)?; } - data.push(result); - } - } - Ok(data) -} - -/// `apply` as a step of `p`'s scan, which takes the running result and the next item in the order that keeps rustymath's rule beside NaN. `⌈` and `⌊` take the item on the left, because a maximum or minimum keeps its right argument beside NaN. Every other function takes the running result on the left, as a scan from the left does. -fn scan_step(p: Primitive, mut apply: impl FnMut(V, V) -> Result) -> impl FnMut(V, V) -> Result { - let turned = matches!(p, Primitive::Math(crate::number::Math::Ceiling | crate::number::Math::Floor)); - move |total, item| if turned { apply(item, total) } else { apply(total, item) } -} - -/// Each lane of `axis` scanned from the left, starting from `seed` or else the lane's first item: `apply(result, item)`. -fn scan_items(axis: &Lanes, seed: Option, item: impl Fn(usize) -> T, mut apply: impl FnMut(T, T) -> Result) -> Result, Error> { - let mut data = vec![item(0); axis.outer * axis.len * axis.inner]; - for i in 0..axis.outer { - for k in 0..axis.inner { - let first = axis.offset(i, 0, k); - let (mut value, start) = match seed.clone() { - Some(value) => (value, 0), - None => { - let value = item(first); - data[first] = value.clone(); - (value, 1) - } - }; - for j in start..axis.len { - let index = axis.offset(i, j, k); - value = apply(value, item(index))?; - data[index] = value.clone(); - } - } - } - Ok(data) -} - -/// The fold or scan of `right` along `axis` by `operand`, with `seed` as each lane's left argument, as one batched map over the lanes that `lanes` gives. The map's operand is the derived fold or scan along the last axis. The fold axis moves last for the run, and a scan moves it back in its result. Inside the run, the fold calls `operand` once for each stage, on every lane at once (`batch::call`). `None` for one lane, for lanes with no call to make, and where the run falls back. After a fallback, the loop over lanes runs from the start and keeps effects in order. -fn by_lanes(operand: &Function, kind: FoldKind, seed: Option<&Value>, right: &Value, axis: usize, lanes: &Lanes, span: &Span) -> Option> { - if lanes.outer * lanes.inner < 2 || lanes.len <= usize::from(seed.is_none()) || !crate::batch::tries(lanes.outer * lanes.inner) { return None; } - let rank = right.shape().len(); - let order: Vec = (0..rank).filter(|&a| a != axis).chain([axis]).collect(); - let moved = match crate::primitive::reorder(right, &order) { Ok(moved) => moved, Err(e) => return Some(Err(e)) }; - let frame = moved.layout().axes(0..rank - 1); - let fold = match Function::new(FunctionNode::Fold(operand.clone(), FoldKind { scan: kind.scan, first: false }), span) { - Ok(fold) => fold, Err(e) => return Some(Err(e)) + None => if first { 0 } else { right.rank() - 1 }, }; - use crate::batch::Arguments; - let arguments = || Some(Arguments { left: seed.map(|s| (s.clone(), s.shape().len())), right: (moved, 1) }); - let result = crate::batch::tried(&fold, arguments, &frame, kind.scan, span)?; - Some(if kind.scan { result.and_then(|v| crate::primitive::transpose_axes(order, &v)) } else { result }) + if right.shape()[axis] == 0 { return Ok(right.clone()); } + let spec = Value::integer(axis as i64); + let slice = |primitive, count| dispatch_in(origin, Operation::Primitive { primitive, pairing: Pairing::Explicit }, + &[Value::integer(count), right.clone(), spec.clone()], &[None, None, None]); + let head = slice(Primitive::Take, 1)?; + let head = if let Some(seed) = seed { + let seed = if seed.rank() + 1 == right.rank() { + let mut shape = seed.shape().to_vec(); shape.insert(axis, 1); seed.with_shape(shape)? + } else { seed.clone() }; + each_call(origin, &inverse, &[seed, head], &[None, None])? + } else { head }; + let previous = slice(Primitive::Drop, -1)?; + let current = slice(Primitive::Drop, 1)?; + let tail = each_call(origin, &inverse, &[previous, current], &[None, None])?; + dispatch_in(origin, Operation::Primitive { primitive: Primitive::CatenateFirst, pairing: Pairing::Explicit }, &[head, tail, spec], &[None, None, None]) } diff --git a/src/eval/rewrite.rs b/src/eval/rewrite.rs deleted file mode 100644 index 7f79faaf..00000000 --- a/src/eval/rewrite.rs +++ /dev/null @@ -1,531 +0,0 @@ -//! The form rewriter: the one home of fast combinations, the combinations of functions that BPL computes by an algorithm of its own. Each rule is one row of `RULES`: a pattern written in BPL over `⍵`, with `⍺` in a dyadic one, beside the function without a glyph that computes it (`Special`), as `⍟1+⍵` beside `Log1p`. Any other name in a pattern captures an argument, which the replacement takes as its left argument, as `k` does in `k↑⍋⍵`. -//! -//! The rewriter reads what a function computes, and what an expression computes, as one tree of calls (`Form`). It replaces each subtree that a rule matches, inner subtrees first, with a call of the rule's function. `Function::new` rewrites a function's monadic and dyadic forms (`function`), and `dispatch` calls the results in the function's place. The binder rewrites each list of nodes before it walks it, after dot access and joins (`expression`). So `⍟1+x`, `⍟ 1+x`, `⍟(x+1)`, the train `⍟1+` and `⍟∘(1+)` all compute through `log1p`, from the first call on. An expression is read as far as its functions are constant and its arguments are arrays, as `Session::static_category` reads them, which keeps what it read about names in the recording's facts. -//! -//! A combination's function gives the combination's results. Where its algorithm doesn't suit the arguments, or a running level carries one, it calls the functions it combines (`combined`), which the level then sees. `•explain` reports each combination that its own algorithm computes. -use super::*; -use crate::primitive::{Folding, Order, Pick, Special}; -use crate::syntax::{Node, NodeKind, cover}; -use rustymath::events::Route; -use std::{cell::Cell, ops::Range, sync::OnceLock}; -use super::tree::Facts; - -/// Each fast combination apart from the selections, which `rules` adds: its pattern, and the function that computes it. -const RULES: &[(&str, Special)] = &[ - ("⍟1+⍵", Special::Log1p), ("¯1+*⍵", Special::Expm1), - ("⍋⍋⍵", Special::Ranks(false)), ("⍋⍒⍵", Special::Ranks(true)), - ("(≢⊢)⌸⍵", Special::GroupCounts), ("⍺(≢⊢)⌸⍵", Special::GroupCounts), - ("⍺(+/⊢)⌸⍵", Special::GroupFolds(Folding::Plus, false)), ("⍺(+⌿⊢)⌸⍵", Special::GroupFolds(Folding::Plus, true)), - ("⍺(⌈/⊢)⌸⍵", Special::GroupFolds(Folding::Max, false)), ("⍺(⌈⌿⊢)⌸⍵", Special::GroupFolds(Folding::Max, true)), - ("⍺(⌊/⊢)⌸⍵", Special::GroupFolds(Folding::Min, false)), ("⍺(⌊⌿⊢)⌸⍵", Special::GroupFolds(Folding::Min, true)), - ("⍺(×/⊢)⌸⍵", Special::GroupFolds(Folding::Times, false)), ("⍺(×⌿⊢)⌸⍵", Special::GroupFolds(Folding::Times, true)), - ("-.×⍵", Special::Determinant), ("+.×⍵", Special::Permanent), -]; - -/// The pattern of each monadic rule whose function maps its argument, as `⍟1+⍵`. -pub(crate) fn mapping_patterns() -> impl Iterator { - RULES.iter().filter(|&&(text, s)| !text.contains('⍺') && Primitive::Special(s).facts(false).extension == crate::primitive::Extension::Pervasive).map(|&(text, _)| text) -} - -/// Whether a rule matches some combination in expression `code`, read in `session`. -pub(crate) fn rewrites(session: &Session, code: &str) -> bool { - let Ok(parsed) = crate::syntax::parse(crate::error::Source::new("", code)).complete() else { return false }; - parsed.statements.iter().any(|s| matches!(expression(session, &s.nodes, &mut None), Cow::Owned(_))) -} - -/// Where an expression's form lies: a list of nodes, by its number in `Reader::lists`, and the nodes it covers there. -type At = (usize, Range); - -/// What a function or an expression computes, as a tree of calls, with where an expression's form lies. -#[derive(Clone, Debug)] -struct Form { kind: Kind, at: Option } - -#[derive(Clone, Debug)] -enum Kind { - /// An argument of a function or a pattern: `⍵`, or `⍺` where `true`. - Arg(bool), - /// A pattern's capture, by its number. - Hole(usize), - /// A literal, or an array that a function holds as an operand. - Value(Value), - /// An array that an expression's name, which it holds, or group gives, which the form doesn't read into. - Subject(Option), - /// A call of a function on its left and right arguments. - Call(Function, Option>, Box
), -} - -impl Form { - fn new(kind: Kind) -> Self { Self { kind, at: None } } - /// The needs mask of the functions that this form calls, which a form needs to match a rule (`possible`). - fn needs(&self) -> u64 { - match &self.kind { Kind::Call(f, left, right) => f.node.needs | left.as_ref().map_or(0, |l| l.needs()) | right.needs(), _ => 0 } - } -} - -/// A rule, read from its row: its pattern, the bit of the primitive or operator that its pattern applies outermost (`root`), whether its pattern spans more than one call, the function that replaces a match, and the argument that the function takes on its left, if any: `⍺`, or the pattern's capture. -struct Rule { pattern: Form, root: u64, spans: bool, with: Special, left: Option } - -impl Rule { - fn read(text: &str, with: Special) -> Self { - let parsed = crate::syntax::parse(crate::error::Source::new("", text)).complete().expect("a rule parses"); - let nodes = &parsed.statements[0].nodes; - let mut reader = Reader { session: session(), facts: None, lists: vec![nodes], holes: Vec::new() }; - let Some((pattern, 0)) = reader.read(0, nodes.len()) else { panic!("rule {text} doesn't read as calls on ⍵") }; - assert!(reader.holes.len() <= 2, "rule {text} captures at most two arguments"); - let left = if reads(&pattern, true) { Some(Kind::Arg(true)) } else if reader.holes.is_empty() { None } else { Some(Kind::Hole(0)) }; - let Kind::Call(f, l, r) = &pattern.kind else { panic!("rule {text} isn't a call") }; - let spans = matches!(r.kind, Kind::Call(..)) || l.as_deref().is_some_and(|l| matches!(l.kind, Kind::Call(..))); - Self { root: root(f), spans, pattern, with, left } - } - /// What a match of `form` binds, where `form` matches. - fn matched<'f>(&self, form: &'f Form) -> Option> { - let mut binds = Binds::default(); - matches(&self.pattern, form, Some(&mut binds)).then_some(binds) - } - /// The call of the rule's function that replaces a match, on copies of the arguments that the match bound. - fn replaced(&self, binds: Binds<'_>, at: Option) -> Form { - let left = match &self.left { Some(Kind::Arg(_)) => binds.args[1].cloned(), Some(Kind::Hole(i)) => binds.holes[*i].cloned(), _ => None }; - let right = binds.args[0].cloned().expect("a pattern reads ⍵"); - Form { kind: Kind::Call(Function::primitive(Primitive::Special(self.with)), left.map(Box::new), Box::new(right)), at } - } -} - -/// Whether `form` reads the argument `⍺`, where `left`, or `⍵`. -fn reads(form: &Form, left: bool) -> bool { - match &form.kind { Kind::Arg(side) => *side == left, Kind::Call(_, l, r) => l.as_deref().is_some_and(|l| reads(l, left)) || reads(r, left), _ => false } -} - -/// The rules, read once, in `all`: `RULES`, and a selection for each order and pick, from `k↑⍋⍵` to `k⊃>⍵`. `masks` holds each rule's needs mask, in the same order, and `keys` the `keys` of all of them and of the spanning ones. The first `spanning` rules have patterns that span more than one call, as `⍟1+⍵` and `k↑⍋⍵` do. A pattern of one call, as `-.×⍵`, is one function, which `Function::new` rewrites. -struct Rules { all: Vec, masks: Vec, keys: [u64; 2], spanning: usize } - -/// The rules, read once. `None` while they're being read, since reading a pattern builds functions, which `Function::new` would rewrite. -fn rules() -> Option<&'static Rules> { - static READ: OnceLock = OnceLock::new(); - thread_local! { static READING: Cell = const { Cell::new(false) }; } - if let Some(rules) = READ.get() { return Some(rules); } - if READING.get() { return None; } - READING.set(true); - let selections = [Order::Grade(false), Order::Grade(true), Order::Sort(false), Order::Sort(true)].into_iter() - .flat_map(|o| [Pick::Take, Pick::First, Pick::Index, Pick::Mix].map(|p| (format!("{}{}{}⍵", if p == Pick::First { "" } else { "k" }, p.primitive().glyph(), o.primitive().glyph()), Special::Selected(o, p)))); - let mut all: Vec = RULES.iter().map(|&(text, with)| Rule::read(text, with)).chain(selections.map(|(text, with)| Rule::read(&text, with))).collect(); - all.sort_by_key(|r| !r.spans); - let (masks, spanning): (Vec, _) = (all.iter().map(|r| r.pattern.needs()).collect(), all.partition_point(|r| r.spans)); - let keys = [keys(&masks), keys(&masks[..spanning])]; - READING.set(false); - Some(READ.get_or_init(|| Rules { all, masks, keys, spanning })) -} - -/// Whether some rule's pattern needs only what the needs mask `held` holds, given the rules' `masks` and their `keys`. -fn possible(masks: &[u64], keys: u64, held: u64) -> bool { held & keys != 0 && masks.iter().any(|m| m & !held == 0) } - -/// One bit of each of `masks`, the one that the fewest of them share, together. A form that matches a rule holds its rule's bit, so a needs mask that holds none of them matches no rule. -fn keys(masks: &[u64]) -> u64 { - let shared = |b: u32| masks.iter().filter(|m| *m >> b & 1 == 1).count(); - masks.iter().fold(0, |keys, &m| keys | (0..64).filter(|&b| m >> b & 1 == 1).min_by_key(|&b| shared(b)).map_or(0, |b| 1 << b)) -} - -/// The parts of a matched form that a match binds to the pattern's arguments and captures. A pattern has at most two captures. -#[derive(Default)] -struct Binds<'f> { args: [Option<&'f Form>; 2], holes: [Option<&'f Form>; 2] } - -/// Whether `form` matches `pattern`. With `binds`, each argument and capture of the pattern binds where the pattern first reads it, and compares with that binding where it reads it again. Without, the two forms must compute the same thing: the same argument, number or name, or the same calls of them. -fn matches<'f>(pattern: &Form, form: &'f Form, mut binds: Option<&mut Binds<'f>>) -> bool { - if let Some(b) = binds.as_deref_mut() { - match pattern.kind { - Kind::Arg(side) => return bind(&mut b.args[usize::from(side)], form), - Kind::Hole(i) => return bind(&mut b.holes[i], form), - _ => {} - } - } - match (&pattern.kind, &form.kind) { - (Kind::Arg(x), Kind::Arg(y)) => x == y, (Kind::Value(x), Kind::Value(y)) => same_value(x, y), (Kind::Subject(Some(x)), Kind::Subject(Some(y))) => x == y, - (Kind::Call(g, pl, pr), Kind::Call(h, fl, fr)) => { - same(g, h) && match (pl, fl) { (None, None) => true, (Some(p), Some(f)) => matches(p, f, binds.as_deref_mut()), _ => false } && matches(pr, fr, binds) - } - _ => false, - } -} - -fn bind<'f>(slot: &mut Option<&'f Form>, form: &'f Form) -> bool { - match slot { Some(bound) => matches(bound, form, None), None => { *slot = Some(form); true } } -} - -/// Whether two functions are built from the same parts, with the same whole numbers in them. -fn same(f: &Function, g: &Function) -> bool { f.equal_by(g, &mut |x, y, _| Ok::<_, Infallible>(same_value(x, y))).unwrap_or(false) } - -fn same_value(x: &Value, y: &Value) -> bool { integer(x).is_some_and(|n| integer(y) == Some(n)) } - -fn integer(v: &Value) -> Option { v.as_number()?.integer().ok() } - -/// The call of `f` on `left` and `right`, with a whole-number right argument of `+`, `×` or `-` put first where the left one isn't a whole number, as patterns spell it: `x+1` reads as `1+x`, and `x-1` as `¯1+x`. -fn call(f: &Function, left: Option, right: Form, at: Option) -> Form { - use crate::number::Arithmetic::*; - let whole = |form: &Form| match &form.kind { Kind::Value(v) => integer(v), _ => None }; - let constant = whole(&right).is_some() && left.as_ref().is_some_and(|l| whole(l).is_none()); - let negated = whole(&right).map(|n| Value::integer(-n)); - let kind = match (f.as_primitive(), left, negated) { - (Some(Primitive::Arithmetic(Plus | Times)), Some(l), _) if constant => Kind::Call(f.clone(), Some(Box::new(right)), Box::new(l)), - (Some(Primitive::Arithmetic(Minus)), Some(l), Some(n)) if constant => { - Kind::Call(Function::primitive(Primitive::Arithmetic(Plus)), Some(Box::new(Form { kind: Kind::Value(n), at: right.at })), Box::new(l)) - } - (_, left, _) => Kind::Call(f.clone(), left.map(Box::new), Box::new(right)), - }; - Form { kind, at } -} - -/// What `f` computes from the forms of its arguments, `left` absent in a monadic call, through its trains and compositions. Any other function is one call of itself. -fn applied(f: &Function, left: Option, right: Form) -> Form { - use {FunctionNode as N, OperatorKind as O, Operand::{Function as F, Value as V}}; - match (f.node(), left) { - (N::Composed(O::Atop, [F(g), F(h)]), left) => applied(g, None, applied(h, left, right)), - (N::Composed(O::Before, [V(a), F(g)]), None) => applied(g, Some(Form::new(Kind::Value(a.clone()))), right), - (N::Composed(O::Before, [F(g), F(h)]), left) => { - let x = left.unwrap_or_else(|| right.clone()); - applied(h, Some(applied(g, None, x)), right) - } - (N::Composed(O::After, [F(g), V(a)]), None) => applied(g, Some(right), Form::new(Kind::Value(a.clone()))), - (N::Composed(O::After, [F(g), F(h)]), left) => { - let x = left.unwrap_or_else(|| right.clone()); - applied(g, Some(x), applied(h, None, right)) - } - (N::Fork([g, h, k]), left) => applied(h, Some(applied(g, left.clone(), right.clone())), applied(k, left, right)), - (N::Modified(O::Commute, F(g)), left) => { - let x = left.unwrap_or_else(|| right.clone()); - applied(g, Some(right), x) - } - (_, left) => call(f, left, right, None), - } -} - -/// The function that computes `form` from its arguments: `⍺` and `⍵` where `dyadic`, and `⍵` alone otherwise. -fn built(form: &Form, dyadic: bool, span: &Span) -> Result { - use {FunctionNode as N, OperatorKind as O}; - let atop = |f: Function, g: Function| Function::new(N::Composed(O::Atop, [Operand::Function(f), Operand::Function(g)]), span); - match &form.kind { - Kind::Arg(left) => Ok(Function::primitive(Primitive::Identity(*left))), - Kind::Value(a) => Function::constant(a.clone(), span), - Kind::Call(f, None, r) if !dyadic && matches!(r.kind, Kind::Arg(false)) => Ok(f.clone()), - Kind::Call(f, Some(l), r) if dyadic && matches!((&l.kind, &r.kind), (Kind::Arg(true), Kind::Arg(false))) => Ok(f.clone()), - Kind::Call(f, None, r) => atop(f.clone(), built(r, dyadic, span)?), - Kind::Call(f, Some(l), r) => match (&l.kind, dyadic) { - (Kind::Value(a), false) => { - let bound = Function::new(N::Composed(O::Before, [Operand::Value(a.clone()), Operand::Function(f.clone())]), span)?; - if matches!(r.kind, Kind::Arg(false)) { Ok(bound) } else { atop(bound, built(r, dyadic, span)?) } - } - _ => Function::new(N::Fork([built(l, dyadic, span)?, f.clone(), built(r, dyadic, span)?]), span), - }, - Kind::Hole(_) | Kind::Subject(_) => unreachable!("a function's form reads only its arguments"), - } -} - -/// The needs mask of a function with node `node`, from its own operator and its operands' masks, which `Function::new` keeps. -pub(super) fn built_from(node: &FunctionNode) -> u64 { - let operand = |o: &Operand| if let Operand::Function(f) = o { f.node.needs } else { 0 }; - match node { - FunctionNode::Primitive(p) => glyph(*p), - FunctionNode::Fold(f, kind) => bit(OperatorKind::Fold(*kind).glyph()) | f.node.needs, - FunctionNode::Modified(op, a) => bit(op.glyph()) | operand(a), - FunctionNode::Composed(op, [a, b]) => bit(op.glyph()) | operand(a) | operand(b), - FunctionNode::Fork(fs) => fs.iter().fold(0, |m, f| m | f.node.needs), - _ => 0, - } -} - -/// The bit of the primitive or operator that the function `f` applies outermost, or 0 for any other function. -fn root(f: &Function) -> u64 { - match f.node() { - FunctionNode::Primitive(p) => bit(p.glyph()), - FunctionNode::Fold(_, kind) => bit(OperatorKind::Fold(*kind).glyph()), - FunctionNode::Modified(op, _) | FunctionNode::Composed(op, _) => bit(op.glyph()), - _ => 0, - } -} - -/// The needs mask of the primitives and operators that `node` writes, in its groups too. -fn written(node: &Node) -> u64 { - match &node.kind { - NodeKind::Function(crate::primitive::Builtin::Primitive(p)) => glyph(*p), - NodeKind::Operator(op) => bit(op.glyph()), - NodeKind::Group(inner) | NodeKind::Run(inner) => inner.iter().fold(0, |m, n| m | written(n)), - _ => 0, - } -} - -/// `form` with each subtree that a rule matches replaced by a call of the rule's function, inner subtrees first, with its needs mask. A subtree tries only the rules whose root it applies outermost and whose mask its own covers. `changed` records whether any subtree was replaced. -fn rewritten(form: Form, rules: &Rules, changed: &mut bool) -> (Form, u64) { - let Form { kind: Kind::Call(f, left, right), at } = form else { return (form, 0) }; - let root = root(&f); - let (left, held) = match left { Some(l) => { let (l, m) = rewritten(*l, rules, changed); (Some(Box::new(l)), m) } None => (None, 0) }; - let (right, mask) = rewritten(*right, rules, changed); - let held = held | mask | f.node.needs; - let form = Form { kind: Kind::Call(f, left, Box::new(right)), at }; - let mut tried = rules.all.iter().zip(&rules.masks).filter(|&(rule, mask)| rule.root == root && mask & !held == 0); - match tried.find_map(|(rule, _)| Some((rule, rule.matched(&form)?))) { - Some((rule, binds)) => { *changed = true; let replaced = rule.replaced(binds, form.at.clone()); let mask = replaced.needs(); (replaced, mask) } - None => (form, held), - } -} - -/// The functions that compute `f`'s monadic and dyadic calls, where a rule matches part of what `f` computes in that call. -pub(super) fn function(f: &Function, span: &Span) -> [Option; 2] { - let Some(rules) = rules() else { return [None, None] }; - if !possible(&rules.masks, rules.keys[0], f.node.needs) { return [None, None]; } - [false, true].map(|dyadic| { - let mut changed = false; - let (form, _) = rewritten(applied(f, dyadic.then(|| Form::new(Kind::Arg(true))), Form::new(Kind::Arg(false))), rules, &mut changed); - if changed { built(&form, dyadic, span).ok() } else { None } - }) -} - -/// The structural combination that `f` computes and the value it binds on its left, where `built` made `f` as the combination's function, alone or with a value bound on its left. `dispatch` calls `combined` with them. -pub(super) fn direct(f: &Function) -> Option<(Special, Option<&Value>)> { - let special = |g: &Function| match g.node() { FunctionNode::Primitive(Primitive::Special(s)) if s.combines() => Some(*s), _ => None }; - match f.node() { - FunctionNode::Composed(OperatorKind::Before, [Operand::Value(a), Operand::Function(g)]) => Some((special(g)?, Some(a))), - _ => Some((special(f)?, None)), - } -} - -/// Whether a train with `tines` may hold a combination that a rule matches, so that the caller builds the train's function, which `function` rewrites, instead of calling its tines (`operators::calls_tines`). -pub(super) fn may_combine(tines: &[Tine]) -> bool { - let Some(rules) = rules() else { return false }; - possible(&rules.masks, rules.keys[0], tines.iter().fold(0, |m, t| match t { Tine::Function(f) => m | f.node.needs, _ => m })) -} - -/// `nodes` with each combination in them that a rule whose pattern spans more than one call matches (`Rules::spanning`) replaced by a group that calls the rule's function, or `nodes` as they are. Only a combination that lies in `nodes` itself is replaced here: one inside a group is replaced when the binder walks the group. `facts` keeps what a reading that found a combination read about what names hold. -pub(super) fn expression<'a>(session: &Session, nodes: &'a [Node], facts: &mut Facts) -> Cow<'a, [Node]> { - let Some(Rules { all, masks, keys, spanning }) = rules() else { return Cow::Borrowed(nodes) }; - let rules = &all[..*spanning]; - if !possible(&masks[..*spanning], keys[1], nodes.iter().fold(0, |m, n| m | written(n))) { return Cow::Borrowed(nodes); } - let mut out = Cow::Borrowed(nodes); - loop { - let mut read = Vec::new(); - let found = { - let mut logged: Facts = facts.is_some().then_some(&mut read); - let mut reader = Reader { session, facts: Some(&mut logged), lists: vec![&out[..]], holes: Vec::new() }; - reader.read(0, out.len()).and_then(|(form, _)| reader.first(&form, rules)) - }; - let Some((range, node)) = found else { return out }; - if let Some(facts) = facts { read.into_iter().for_each(|f| if !facts.contains(&f) { facts.push(f) }) } - out.to_mut().splice(range, [node]); - } -} - -/// Reads forms from lists of nodes, right to left, as the binder groups them. A pattern's reader, with `facts` of `None`, reads `⍵` and `⍺` as arguments and any other name as a capture. -struct Reader<'a, 'f, 'r> { session: &'a Session, facts: Option<&'f mut Facts<'r>>, lists: Vec<&'a [Node]>, holes: Vec } - -impl<'a> Reader<'a, '_, '_> { - fn category(&mut self, node: &Node) -> Category { - match (&mut self.facts, &node.kind) { - (None, NodeKind::Name(..)) => Category::Value, - (Some(facts), _) => self.session.static_category(node, facts), - (None, _) => self.session.static_category(node, &mut None), - } - } - - /// The form of the longest run of calls that ends at `end` in list `list` and starts from an array, with where it starts. - fn read(&mut self, list: usize, end: usize) -> Option<(Form, usize)> { - let mut start = end.checked_sub(1)?; - let mut form = self.subject(list, start)?; - while let Some((f, from)) = self.function(list, start) { - // An array just before the function is its left argument, unless an array before that strands with it. - let nodes = self.lists[list]; - let (left, from) = match from.checked_sub(1).and_then(|i| Some((self.subject(list, i)?, i))) { - Some((_, i)) if i > 0 && self.category(&nodes[i - 1]) == Category::Value => break, - Some((left, i)) => (Some(left), i), - None => (None, from), - }; - form = call(&f, left, form, Some((list, from..end))); - start = from; - } - Some((form, start)) - } - - /// The form of the array at node `i` of list `list`: a literal's value, a pattern's argument or capture, the calls in a group, or an array that a name or group gives. `None` where the node isn't an array, or is a dyadic operator's operand. - fn subject(&mut self, list: usize, i: usize) -> Option { - let nodes = self.lists[list]; - if i > 0 && self.category(&nodes[i - 1]).valence() == Some(true) { return None; } - let kind = match &nodes[i].kind { - NodeKind::Literal(v) => Kind::Value(v.clone()), - NodeKind::Name(name, _) if self.facts.is_none() => match *name { - Name::OMEGA => Kind::Arg(false), Name::ALPHA => Kind::Arg(true), - _ => Kind::Hole(self.holes.iter().position(|h| h == name).unwrap_or_else(|| { self.holes.push(*name); self.holes.len() - 1 })), - }, - NodeKind::Group(inner) | NodeKind::Run(inner) if self.category(&nodes[i]) == Category::Value => { - self.lists.push(inner); - match self.read(self.lists.len() - 1, inner.len()) { Some((form, 0)) => return Some(form), _ => Kind::Subject(None) } - } - NodeKind::Name(name, _) if self.category(&nodes[i]) == Category::Value => Kind::Subject(Some(*name)), - _ if self.category(&nodes[i]) == Category::Value => Kind::Subject(None), - _ => return None, - }; - Some(Form { kind, at: Some((list, i..i + 1)) }) - } - - /// The constant function whose nodes end just before node `end` of list `list`, with where they start: a primitive, or operators on constant operands, as `+/`, `(≢⊢)⌸` and `-.×`. - fn function(&mut self, list: usize, end: usize) -> Option<(Function, usize)> { - let nodes = self.lists[list]; - let from = self.term(nodes, end.checked_sub(1)?)?; - // No dyadic operator before the function may take it as its operand. - if from > 0 && self.category(&nodes[from - 1]).valence() == Some(true) || !nodes[from..end].iter().all(constant) { return None; } - let f = match &nodes[from..end] { - [Node { kind: NodeKind::Function(crate::primitive::Builtin::Primitive(p)), .. }] => Function::primitive(*p), - run => match Binder::evaluate(run, self.session, false).ok()? { Step::Done(Bound { value: Binding::Function(f), .. }) => f, _ => return None }, - }; - Some((f, from)) - } - - /// Where the function or operand whose last node is node `k` starts: a monadic operator takes the operand before it, and a dyadic operator the operands on both sides of it. - fn term(&mut self, nodes: &[Node], k: usize) -> Option { - match self.category(&nodes[k]).valence() { - Some(false) => self.operand(nodes, k.checked_sub(1)?), - Some(true) => None, - None if k > 0 && self.category(&nodes[k - 1]).valence() == Some(true) => self.operand(nodes, k.checked_sub(2)?), - None => (self.category(&nodes[k]) == Category::Function).then_some(k), - } - } - - /// Where the operand whose last node is node `k` starts: a function, or an array. - fn operand(&mut self, nodes: &[Node], k: usize) -> Option { - self.term(nodes, k).or_else(|| (self.category(&nodes[k]) == Category::Value).then_some(k)) - } - - /// The first subtree of `form`, inner subtrees first, that lies in the reader's first list and that a rule matches: the range of its nodes, and the group that replaces them. - fn first(&self, form: &Form, rules: &[Rule]) -> Option<(Range, Node)> { - let Kind::Call(_, left, right) = &form.kind else { return None }; - if let Some(found) = left.as_deref().and_then(|l| self.first(l, rules)).or_else(|| self.first(right, rules)) { return Some(found); } - let Some((0, range)) = form.at.clone() else { return None }; - let (rule, binds) = rules.iter().find_map(|rule| Some((rule, rule.matched(form)?)))?; - let Kind::Call(f, left, right) = rule.replaced(binds, None).kind else { unreachable!("a replacement is a call") }; - let span = cover(&self.lists[0][range.clone()]); - let mut group: Vec = left.map(|l| self.node(&l)).into_iter().collect(); - group.push(Node { kind: NodeKind::Function(crate::primitive::Builtin::Primitive(f.as_primitive()?)), span: span.clone() }); - group.push(self.node(&right)); - Some((range, Node { kind: NodeKind::Group(group), span })) - } - - /// The node that gives a matched argument: its own node, or a group of the nodes it covers. - fn node(&self, form: &Form) -> Node { - let (list, range) = form.at.clone().expect("an expression's forms know their nodes"); - match &self.lists[list][range] { [node] => node.clone(), nodes => Node { kind: NodeKind::Group(nodes.to_vec()), span: cover(nodes) } } - } -} - -/// Whether a node is constant: a primitive, an operator, a literal, or a group of those. -fn constant(node: &Node) -> bool { - match &node.kind { - NodeKind::Function(crate::primitive::Builtin::Primitive(_)) | NodeKind::Operator(_) | NodeKind::Literal(_) => true, - NodeKind::Group(inner) | NodeKind::Run(inner) => inner.iter().all(constant), - _ => false, - } -} - -/// The Key or product that combination `s` replaces, which a running level sees in its place. -fn original(s: Special, span: &Span) -> Result { - use {crate::number::Arithmetic::*, FunctionNode as N, OperatorKind as O}; - let p = Function::primitive; - let key = |f: Function| { - let operand = Function::new(N::Composed(O::Atop, [Operand::Function(f), Operand::Function(p(Primitive::Identity(false)))]), span)?; - Function::new(N::Modified(O::Key, Operand::Function(operand)), span) - }; - let product = |f| Function::new(N::Composed(O::Product, [Operand::Function(p(Primitive::Arithmetic(f))), Operand::Function(p(Primitive::Arithmetic(Times)))]), span); - match s { - Special::GroupCounts => key(p(Primitive::Tally)), - Special::GroupFolds(by, first) => key(Function::new(N::Fold(p(by.primitive()), FoldKind { scan: false, first }), span)?), - Special::Determinant => product(Minus), - Special::Permanent => product(Plus), - _ => unreachable!("only Key's and the products' combinations have a function of their own to stand for"), - } -} - -/// A combination's function on these arguments, apart from `Log1p` and `Expm1`, which are kernels: its own algorithm where the arguments suit it, and otherwise the functions it combines. `call_axis` calls it before any running level, which then sees those functions. -pub(crate) fn combined(s: Special, left: Option<&Value>, right: &Value, span: &Span) -> Result { - use crate::number::Arithmetic::*; - // A running level sees the Key or product that the combination replaces, and applies its own rule to it, as `∂` does to `+.×`. - if matches!(s, Special::GroupCounts | Special::GroupFolds(..) | Special::Determinant | Special::Permanent) && crate::transform::carried(left, right) { - return original(s, span)?.call_array(left, right, span); - } - let (route, fast) = match s { - Special::Selected(order, pick) => (if matches!(order, Order::Grade(_)) { Route::PartialGrade } else { Route::PartialSort }, selected(order, pick, left, right, span)), - Special::Ranks(down) => (Route::Ranks, ranks(down, right, span)), - Special::GroupCounts | Special::GroupFolds(..) => return grouped(s, left, right, span)?.array(span), - Special::Determinant => return primitive_expansion(Primitive::Arithmetic(Minus), Primitive::Arithmetic(Times), right, span), - Special::Permanent => return primitive_expansion(Primitive::Arithmetic(Plus), Primitive::Arithmetic(Times), right, span), - _ => unreachable!("a kernel computes this combination"), - }; - if let Some(result) = fast { - if result.is_ok() { taken(route, s) } - return result; - } - rustymath::events::declined(route, "arguments that don't suit it", right.len()); - match s { - Special::Selected(order, pick) => pick.primitive().call(left, &order.primitive().call(None, right, span)?, span), - Special::Ranks(down) => Primitive::Grade(false).call(None, &Primitive::Grade(down).call(None, right, span)?, span), - _ => unreachable!("only selections and ranks decline here"), - } -} - -/// Records for `•explain`, where it's on, that the function of combination `s` computed it by its own algorithm, along `route`. It names the combination by its row's glyph, as `↑⍋` names every selection. -pub(super) fn taken(route: Route, s: Special) { - if rustymath::events::running() { crate::explain::fast(route, Primitive::Special(s).glyph().into()) } -} - -/// The items of `right`'s order that `pick` takes, through rustymath's partial selection, where `right` is a vector without keys and they are at most a quarter of its items. `left` is the count to take or the position to pick. -fn selected(order: Order, pick: Pick, left: Option<&Value>, right: &Value, span: &Span) -> Option> { - if right.shape().len() != 1 || right.keys(0).is_some() || crate::transform::carried(left, right) { return None; } - let n = right.len(); - let (range, item) = match pick { - Pick::Take => { - let k = integer(left?)?; - let m = usize::try_from(k.unsigned_abs()).ok().filter(|&m| m <= n)?; - (if k < 0 { n - m..n } else { 0..m }, false) - } - Pick::First => { let i = rustymath::layout::position(0, n)?; (i..i + 1, true) } - Pick::Index | Pick::Mix => { let i = rustymath::layout::position(integer(left?)?, n)?; (i..i + 1, true) } - }; - // Large runs cost more to comparison-sort than grading the whole vector by radix or counting. - if range.len() > n / 4 { return None; } - let (grade, down) = match order { Order::Grade(down) => (true, down), Order::Sort(down) => (false, down) }; - let taken = rustymath::search::view_selected(right.view()?, range, down)?; - Some((|| { - let positions = Value::positions_u32([taken.len()], n, taken).and_then(|p| p.along_first_of(right)).described("invalid selected order")?; - let selected = if grade { positions } else { - let selector = Value::new([1], vec![positions]).described("invalid selected order")?; - Primitive::Index.call(Some(&selector), right, span)? - }; - if item { crate::primitive::pick(&Value::integer(0), &selected, session().prototype.get(), None, span) } else { Ok(selected) } - })()) -} - -/// Each major cell's rank in the order that `⍋` or, where `down`, `⍒` gives: the inverse of the grade's permutation. -fn ranks(down: bool, right: &Value, span: &Span) -> Option> { - use {crate::array::Items, rustymath::buffer::View}; - if right.shape().is_empty() || right.keys(0).is_some() || crate::transform::carried(None, right) { return None; } - let grade = match Primitive::Grade(down).call(None, right, span) { Ok(g) => g, Err(e) => return Some(Err(e)) }; - let Items::Compact(View::Integers(ints)) = grade.as_items() else { return Some(Primitive::Grade(false).call(None, &grade, span)) }; - let n = grade.len(); - let ranks = rustymath::with_ints!(ints, |positions| rustymath::search::inverse_permutation(positions.iter().map(|&p| i64::from(p) as usize))); - Some(Value::positions_u32([n], n, ranks).and_then(|r| r.along_first_of(right)).described("invalid ranks")) -} - -/// The bit that a primitive or operator with glyph `glyph` sets in a needs mask: its first character's position among the characters that the rules' patterns write, or none for a character that no pattern writes, which can't help a form match. The characters come from the rules' text, so a function's mask doesn't wait for the rules to be read. -fn bit(glyph: &str) -> u64 { - static WRITTEN: OnceLock> = OnceLock::new(); - let written = WRITTEN.get_or_init(|| { - let selections = [Pick::Take, Pick::Index, Pick::Mix].map(|p| p.primitive().glyph()).into_iter() - .chain([Order::Grade(false), Order::Grade(true), Order::Sort(false), Order::Sort(true)].map(|o| o.primitive().glyph())); - let mut chars: Vec = RULES.iter().map(|&(text, _)| text).chain(selections).flat_map(str::chars) - .filter(|c| !c.is_alphanumeric() && !"⍺⍵¯() ".contains(*c)).collect(); - chars.sort_unstable(); - chars.dedup(); - assert!(chars.len() <= 64, "a needs mask holds 64 characters"); - chars - }); - glyph.chars().next().and_then(|c| written.binary_search(&c).ok()).map_or(0, |i| 1 << i) -} - -/// The needs mask of primitive `p`, which `Function::plain` keeps. A `-` also counts as `+`, as `call` reads `x-1` as `¯1+x`. -pub(super) fn glyph(p: Primitive) -> u64 { - use crate::number::Arithmetic::*; - bit(p.glyph()) | if p == Primitive::Arithmetic(Minus) { bit(Primitive::Arithmetic(Plus).glyph()) } else { 0 } -} diff --git a/src/eval/session.rs b/src/eval/session.rs index 723aff85..4d7bdb23 100644 --- a/src/eval/session.rs +++ b/src/eval/session.rs @@ -11,60 +11,36 @@ impl Session { turn.clear(); turn } - /// A turn at the process's one session, as earlier turns left it. It waits while another thread holds a turn. - pub fn current() -> Turn { Turn::take() } - /// A session's fields as a new session has them, for the process's slot and for `clear`. `Session` has no public constructor, so no code builds a second session outside the slot. pub(super) fn blank() -> Self { Self { execution: Default::default(), prefacing: Default::default(), bindings: Default::default(), loading: Default::default(), - stacks: Default::default(), depth: Default::default(), prototype: Default::default(), capture: Default::default(), - fusion_reports: Default::default(), levels: Default::default(), transforming: Default::default(), generator: Default::default(), - drawn: Default::default(), args: Default::default(), tacit: Default::default() } + depth: Default::default(), prototype: Default::default(), capture: Default::default(), levels: Default::default(), traces: Default::default(), + generator: crate::distribution::fresh(), args: Default::default(), error: Default::default(), token: Default::default(), guards: Default::default() } } - /// Puts every field except the running evaluation's `execution` back as a new session has it, for `]clear` and a new session's turn. The destructuring names each field, so a field added later is a compile error here until it's reset too. pub(crate) fn clear(&self) { use std::sync::PoisonError; - let Session { - execution: _, - prefacing, - bindings, - loading, - stacks, - depth, - prototype, - capture, - fusion_reports, - levels, - transforming, - generator, - drawn, - args, - tacit: _, - } = Session::blank(); - self.prefacing.set(prefacing.get()); - self.bindings.replace(bindings); - self.loading.replace(loading.into_inner()); - self.stacks.replace(stacks.into_inner()); - self.depth.set(depth.get()); - self.prototype.set(prototype.get()); - self.capture.set(capture.get()); - self.fusion_reports.set(fusion_reports.get()); - self.levels.replace(levels.into_inner()); - self.transforming.set(transforming.get()); + self.bindings.replace(Bindings::default()); + self.prefacing.set(false); self.depth.set(0); self.prototype.set(false); self.capture.set(false); + self.loading.borrow_mut().clear(); self.args.borrow_mut().clear(); self.guards.borrow_mut().clear(); + self.levels.clear(); self.traces.borrow_mut().clear(); self.error.take(); self.token.take(); + let generator = crate::distribution::fresh(); std::mem::swap(&mut *self.generator.lock().unwrap_or_else(PoisonError::into_inner), &mut *generator.lock().unwrap_or_else(PoisonError::into_inner)); - *self.drawn.lock().unwrap_or_else(PoisonError::into_inner) = drawn.into_inner().unwrap_or_else(PoisonError::into_inner); - self.args.replace(args.into_inner()); - self.tacit.clear(); } /// A new session's turn for a person at a terminal, with boxed display elided to the terminal's width. pub fn interactive() -> Turn { Self::make_interactive(); Self::new() } /// Makes the interactive display settings the current ones and the ones that `]clear` restores. Settings are process-wide, so this needs no session. Python's BPL magics use it on the workspace they share with the Python API. pub fn make_interactive() { crate::settings::set_display_defaults(crate::display::Settings::interactive()); } - /// `value` as this session's implicit display shows it, as text. - pub fn show(&self, value: &Value) -> String { self.display_settings().array(value, false) } - /// The implicit MIME display of `value`, without publishing it. Rendering is interruptible; a renderer's side output is discarded. + /// `value` as this session's implicit display shows it, as text. Display is a forcing read (`graph::forced`): a data error in the value's work raises here. + pub fn show(&self, value: &Value) -> Result { + self.within(|| { + let value = value.read(crate::ReadKind::Value)?; + self.raise()?; + Ok(self.display_settings().array(&value, false)) + }) + } + pub fn render(&self, value: &Value, interrupt: crate::InterruptHandle) -> Result { let _running = self.execution.begin(crate::EvalOptions { interrupt, echo: false, output: Some(Arc::new(|_| {})), ..crate::EvalOptions::default() }); - self.render_value(value, &Span::whole(Source::new("", ""))) + self.within(|| self.render_value(value, &Span::whole(Source::new("", "")))) } /// The display settings, with floats of the default width shown without a marker. pub(crate) fn display_settings(&self) -> crate::display::Settings { @@ -73,6 +49,12 @@ impl Session { settings } pub fn names(&self) -> Vec { self.bindings.names() } + pub(crate) fn capture_binding(&self, parent: Option, module: usize, name: Name) -> Result, Error> { + let Some((place, binding)) = self.bindings.capture(parent, module, name) else { return Ok(None) }; + if matches!(binding, Binding::NoResult | Binding::Absent | Binding::Released) { return Ok(None); } + let span = Span::whole(Source::new("", name.text())); + Ok(Some((place, binding.into_value(&span)?))) + } pub fn name_class(&self, name: &str) -> i64 { match crate::inspection::item(name, crate::settings::hanzi_aliases()) { Some(NodeKind::Name(name, _)) => self.bindings.lookup(name).map_or(0, |(_, binding)| binding.class()), @@ -85,7 +67,7 @@ impl Session { pub fn inspect(&self, name: &str) -> Option { let info = match crate::inspection::item(name, crate::settings::hanzi_aliases()) { Some(NodeKind::Name(name, _)) => self.bindings.lookup(name).and_then(|(_, binding)| binding.inspection(self)), - Some(NodeKind::Function(builtin)) => Binding::Function(builtin.function()).inspection(self), + Some(NodeKind::Function(builtin)) => Binding::from(builtin.function()).inspection(self), Some(NodeKind::Operator(op)) => Some(crate::Inspection::new("operator", op.glyph().to_string())), _ => None }; info.or_else(|| crate::inspection::documentation(name).map(|_| crate::Inspection::new("syntax", name.into()))) @@ -98,7 +80,8 @@ impl Session { display.boxed = false; display.elide = Elide { limit: 100, edges: 2, width: 80, ..display.elide }; let (kind, text) = match binding { - Binding::Value(a) => ("array", display.array(&a, false)), Binding::Function(f) => ("function", f.text(display.elide, &mut 100)), + Binding::Value(a) => ("array", display.array(&a, false)), binding @ Binding::Function(_) => + ("function", Function::from_value(binding, &Span::whole(Source::new("", name.text()))).ok()?.text(display.elide, &mut 100)), Binding::Operator(op) => ("operator", op.text(display.elide)), _ => return None }; let mut chars = text.chars(); @@ -111,51 +94,34 @@ impl Session { FUNCTION_CLASS | OPERATOR_CLASS => Ok(self.inspect(name).unwrap().source), SUBJECT_CLASS => Err(Kind::Domain.into()), _ => Err(ErrorKind::Value) } } - /// Whether a level whose kind wraps values is running (`transform::Levels::transforming`). - #[inline] - pub(crate) fn transforming(&self) -> bool { self.transforming.get() } - /// Starts a level of `kind` and returns its id, which is larger than every running level's. A `∂` call's id is its tag, and a record level's id is the level its records carry. - pub(crate) fn begin(&self, kind: crate::transform::Kind) -> u64 { - let frames = self.bindings.depth(); - let id = self.levels.borrow_mut().begin(kind, frames); - self.transforming.set(true); - id - } - /// Ends the innermost level, applying its kind's exit rule to each binding outside its frames that it wrote (`transform::Kind::exit`). - pub(crate) fn end(&self) { - let level = self.levels.borrow_mut().end(); - self.transforming.set(self.levels.borrow().transforming()); - let Some(exit) = level.kind.exit() else { return }; - for place in level.writes { - let Some(Binding::Value(v)) = self.bindings.get(place) else { continue }; - self.bindings.insert(place, Binding::Value(exit(&v, level.id))); + pub(crate) fn guard_node(&self, node: &crate::graph::Node) { + let mut guards = self.guards.borrow_mut(); + let Some(scope) = guards.last_mut() else { return }; + if scope.nodes.len() >= scope.live.saturating_mul(2).max(8) { scope.nodes.retain(|node| node.strong_count() > 0); scope.live = scope.nodes.len(); } + scope.nodes.push(node.downgrade()); + } + fn guarded(&self, value: Option<&Value>) -> Result<(), Error> { + let scope = std::mem::take(self.guards.borrow_mut().last_mut().expect("a running error guard has a scope")); + for node in scope.nodes.into_iter().filter_map(|node| node.upgrade()).map(crate::graph::Node) { + for output in 0..node.descs().len() { + Value::Pending(Pending { node: node.clone(), output: output as u32 }).read(crate::ReadKind::Value)?; + } } - } - /// Records a write to the binding at `place`, for each running level whose exit acts on it, or stops a batched map's run before it (`transform::Levels::record_write`). - pub(super) fn record_write(&self, place: Place) -> Result<(), crate::transform::Stop> { self.levels.borrow_mut().record_write(place) } - /// Checks `effect` against the running levels (`transform::Levels::effect`). - pub(crate) fn effect(&self, effect: crate::transform::Effect) -> Result<(), crate::transform::Stop> { self.levels.borrow().effect(effect) } - /// The running trace's level and tracer, if a trace is running (`transform::Levels::tracing`). - pub(crate) fn tracing(&self) -> Option<(u64, std::sync::Arc)> { - self.levels.borrow().tracing().map(|(level, t, _)| (level, t.clone())) - } - /// Keeps a fresh name in the running trace or batched run (`transform::Levels::keep_fresh`). - pub(crate) fn keep_fresh(&self, name: std::sync::Arc) { self.levels.borrow_mut().keep_fresh(name) } - /// Whether `value` holds a fresh name that the running batched run drew (`transform::Levels::holds_fresh`). - pub(crate) fn holds_fresh(&self, value: &Value) -> bool { self.levels.borrow().holds_fresh(value) } - /// The id and frame of the batched map that is running, if one is (`transform::Levels::batched`). - pub(crate) fn batched(&self) -> Option<(u64, crate::array::Layout)> { self.levels.borrow().batched().map(|(id, frame)| (id, frame.clone())) } - /// The id of the batched run, if one is running, without the frame that `batched` clones. - pub(crate) fn batch_level(&self) -> Option { self.levels.borrow().batched().map(|(id, _)| id) } - /// Whether the running levels let a map run batched (`transform::Levels::admits_batch`). - pub(crate) fn admits_batch(&self) -> bool { self.levels.borrow().admits_batch() } - /// Whether a map's batched try widens the running batched run (`transform::Levels::widens`). - pub(crate) fn widens(&self) -> bool { self.levels.borrow().widens() } - pub fn erase(&self, name: &str) -> bool { - let Some(NodeKind::Name(name, _)) = crate::inspection::item(name, crate::settings::hanzi_aliases()) else { return false; }; - if name.implicit().is_some() { return false; } - if let Some((place, _)) = self.bindings.lookup(name) { self.bindings.remove(place); } - true + if let Some(value) = value { value.read(crate::ReadKind::Value)?; } + self.raise() + } + pub(super) fn writing(&self, place: Place, span: &Span) -> Result<(), Error> { + if place.frame() != self.bindings.current() { crate::program::trace::Tracing::stop(crate::program::Reason::OuterWrite, Some(span.clone())); } + self.levels.writing(place).map_err(|error| error.at(span)) + } + pub fn erase(&self, name: &str) -> Result { + let Some(NodeKind::Name(name, _)) = crate::inspection::item(name, crate::settings::hanzi_aliases()) else { return Ok(false); }; + if name.implicit().is_some() { return Ok(false); } + if let Some((place, _)) = self.bindings.lookup(name) { + self.writing(place, &Span::whole(Source::new("", name.text())))?; + self.bindings.remove(place); + } + Ok(true) } pub fn complete(&self, prefix: &str) -> Vec { let mut names = self.name_list(&[2, 3, 4], prefix); @@ -172,18 +138,17 @@ impl Session { self.named(right, span, |s, name| Ok(Value::integer(s.name_class(name)))) } pub(crate) fn system_ex(&self, _: Option<&Value>, right: &Value, span: &Span) -> Result { - crate::transform::acts().at(span)?; - self.named(right, span, |s, name| Ok(Value::integer(s.erase(name) as i64))) + self.named(right, span, |s, name| Ok(Value::integer(s.erase(name)? as i64))) } pub(crate) fn system_src(&self, _: Option<&Value>, right: &Value, span: &Span) -> Result { self.named(right, span, |s, name| s.name_source(name).map(|text| crate::keyed::text(&text))) } - /// `•explain text` runs `text` in the caller's scope, as `⍎` does, computes its value, and gives a record of that value and of what happened while it ran and computed (`explain`). Computing a pending value records no `forced` row, as the speed timer's computing records none. - pub(crate) fn system_explain(&self, _: Option<&Value>, right: &Value, span: &Span) -> Result { - crate::transform::acts().at(span)?; - let (result, events) = crate::explain::explained(|| { + /// `•explain text` runs `text` in the caller's scope, as `⍎` does, computes its value, and gives a record of that value and of what happened while it ran and computed (`explain`). Computing a pending value records no `forced` row, as the speed timer's computing records none. Its options record: `"sites":$t` fills the `site` columns of `forced` and `stopped` rows with the code that computed the items or stopped the trace (`explain::site`). + pub(crate) fn system_explain(&self, left: Option<&Value>, right: &Value, span: &Span) -> Result { + let sites = crate::data::Options::new("•explain", left, None, &["sites"])?.boolean("sites", false)?; + let (result, events) = crate::explain::explained(sites, || { let result = self.execute(None, right, span).and_then(|r| r.array(span)); - if let Ok(value) = &result { value.force(); } + let result = result.and_then(|value| crate::graph::forced(&[&value]).map(|()| value)); result }); events.report(result?, span) @@ -202,8 +167,7 @@ impl Session { self.bind(&statement.nodes)?.array(span) } /// `•time t` gives the seconds since `t`, counting from the Unix epoch. `F •time x` gives each function's fastest time per call on `x`, in the layout of `F`. - pub(super) fn system_time(&self, left: Option<&Value>, right: &Value, span: &Span) -> Result { - crate::transform::acts().at(span)?; + pub(crate) fn system_time(&self, left: Option<&Value>, right: &Value, span: &Span) -> Result { // Times stay 64-bit, as `•date` gives them, because a 32-bit float holds seconds since 1970 only to within about two minutes. let second = |s| Value::Number(crate::Number::float(s, FloatWidth::F64)); let Some(functions) = left else { @@ -242,13 +206,12 @@ impl Session { crate::keyed::texts(&[values.len()], values).described("invalid name list").at(span) } pub fn set(&self, name: &str, value: Value) -> Result<(), ErrorKind> { self.set_value(name, Binding::from_element(value)) } - pub fn set_function(&self, name: &str, value: Function) -> Result<(), ErrorKind> { self.set_value(name, Binding::Function(value)) } + pub fn set_function(&self, name: &str, value: Function) -> Result<(), ErrorKind> { self.set_value(name, value.into()) } fn set_value(&self, name: &str, value: Binding) -> Result<(), ErrorKind> { let Some(NodeKind::Name(name, _)) = crate::inspection::item(name, crate::settings::hanzi_aliases()) else { return Err(ErrorKind::Syntax); }; if name.implicit().is_some() { return Err(ErrorKind::Syntax); } let place = self.bindings.at_top_level(Some(0), || self.bindings.here(name)); - self.bindings.insert(place, value); - Ok(()) + self.store_at(place, value, &Span::whole(Source::new("", name.text()))).map_err(|error| error.kind.clone()) } pub fn eval(&self, code: &str) -> Evaluation { self.eval_with(code, crate::EvalOptions::default()) } pub fn eval_with(&self, code: &str, options: crate::EvalOptions) -> Evaluation { @@ -258,12 +221,12 @@ impl Session { pub fn eval_apart(&self, code: &str, options: crate::EvalOptions) -> Evaluation { self.evaluation(options, |s| s.bindings.at_top_level(Some(s.bindings.new_module()), || s.evaluate_source(Source::new("", code)))) } - /// One evaluation under `options`: `evaluate`'s result, with the output it produced. + /// One evaluation under `options`: `evaluate`'s result, with the output it produced. Its value leaves the session computed: a forcing read (`graph::forced`), whose error becomes the evaluation's. fn evaluation(&self, options: crate::EvalOptions, evaluate: impl FnOnce(&Self) -> Evaluation) -> Evaluation { let _running = self.execution.begin(options); - let result = evaluate(self); + let mut result = evaluate(self); + if let Some(Err(error)) = result.value.as_ref().map(|value| crate::graph::forced(&[value])) { result = Evaluation { value: None, error: Some(error), ..result }; } self.execution.flush(); - self.stacks.take(); Evaluation { output: self.execution.take_output(), ..result } } pub fn eval_timeout(&self, code: &str, timeout: std::time::Duration) -> Evaluation { @@ -299,10 +262,10 @@ impl Session { if self.execution.echo.get() && !shy { if let Err(e) = self.display_value(&a, &span) { result.error = Some(e); } } result.value = Some(a); } - Ok(Bound { value: Binding::Function(f), shy, .. }) => { - if self.execution.echo.get() && !shy { self.display_function(&f, None); } - result.function = Some(f); - } + Ok(Bound { value: Binding::Function(f), shy, .. }) => match Function::from_value(Binding::Function(f), &span) { + Ok(f) => { if self.execution.echo.get() && !shy { self.display_function(&f, None); } result.function = Some(f); } + Err(error) => result.error = Some(error), + }, Ok(_) => (), Err(e) => result.error = Some(e), } @@ -375,12 +338,12 @@ impl Session { } _ if bound.shy => None, Binding::Function(f) => { + let f = match Function::from_value(Binding::Function(f), &nodes[0].span) { Ok(f) => f, Err(error) => { result.error = Some(error); return result; } }; if self.execution.echo.get() { self.display_function(&f, dissected.as_deref()); } if i + 1 == parsed.statements.len() { result.function = Some(f); } None } Binding::Operator(op) => { - let op = Operator(Arc::new(op)); if self.execution.echo.get() { self.execution.output(crate::OutputKind::Display, op.text(crate::settings::display().elide)); } if i + 1 == parsed.statements.len() { result.operator = Some(op); } None @@ -427,8 +390,8 @@ impl Session { } if command.eq_ignore_ascii_case("]clear") { if !args.is_empty() { return failed(ErrorKind::Syntax, "usage: ]clear".into()); } - crate::settings::reset(); self.clear(); + crate::settings::reset(); return Evaluation::default(); } if command.eq_ignore_ascii_case("]display") { @@ -444,22 +407,11 @@ impl Session { } failed(ErrorKind::Syntax, "unknown user command".into()) } - pub(super) fn bind(&self, nodes: &[Node]) -> Result { self.bound(nodes, None) } - /// `nodes` evaluated as one statement through `slot`, as `statement_step` uses it, one level deeper. A compiled statement runs at once: one loop runs its steps, so it needs only the depth count. The binder evaluates any other statement with an interrupt check and a stack that grows as needed (`nested`). - pub(super) fn bound(&self, nodes: &[Node], slot: Option<&std::sync::OnceLock>>) -> Result { - let done = |step: Step| { - let Step::Done(result) = step else { unreachable!("a statement that isn't a dfn's last gives its value") }; - result - }; - match super::tree::compiled(slot) { - Some(recording) => { - self.descend(&nodes[0].span)?; - let result = recording.evaluate(nodes, self, false).map(done); - self.ascend(); - result - } - None => self.nested(nodes, |session| session.statement_step(slot, nodes, false).map(done)), - } + pub(super) fn bind(&self, nodes: &[Node]) -> Result { self.bound(nodes) } + pub(super) fn bound(&self, nodes: &[Node]) -> Result { + self.nested(nodes, |session| match session.statement_step(nodes, false)? { + Step::Done(result) => Ok(result), Step::Tail(_) => unreachable!("a non-tail statement returns a value"), + }) } /// `f` evaluating `nodes` one level deeper, after an interrupt check, within the depth limit and on a stack that grows as needed. pub(super) fn nested(&self, nodes: &[Node], f: impl FnOnce(&Self) -> Result) -> Result { @@ -485,7 +437,7 @@ impl Session { pub(super) fn ascend(&self) { self.depth.set(self.depth.get() - 1); } /// `•mime Y` is the MIME bundle that displays `Y`. `X •mime Y` is `Y` displayed through `X`, which holds a function or names a MIME type. A MIME type shows `Y` itself as that type. - pub(super) fn mime(&self, left: Option<&Value>, right: &Value, span: &Span) -> Result { + pub(crate) fn mime(&self, left: Option<&Value>, right: &Value, span: &Span) -> Result { let Some(left) = left else { return self.mime_value(right, span) }; let renderer = match left.is_singleton().then(|| left.at(0)) { Some(Value::Function(f)) => f, @@ -497,7 +449,7 @@ impl Session { right.clone().with_renderer(renderer).described("invalid •mime result").at(span) } fn mime_value(&self, right: &Value, span: &Span) -> Result { - let fallback = crate::keyed::vector(vec!["text/plain".into()], vec![crate::keyed::text(&self.show(right))]).unwrap(); + let fallback = crate::keyed::vector(vec!["text/plain".into()], vec![crate::keyed::text(&self.show(right)?)]).unwrap(); let Some(renderer) = right.renderer().cloned() else { return Ok(fallback); }; let echo = self.execution.echo.replace(false); let rendered = renderer.call_array(None, right, span); @@ -512,15 +464,22 @@ impl Session { Ok(()) } - /// A value with a renderer formats through it. Other values, and a renderer that fails, format as text. + /// A value with a renderer formats through it. Other values, and a renderer that fails, format as text. Display is a forcing read and a checkpoint (`graph::forced`): a data error raises before any output, before a failing renderer's fallback could hide it. fn render_value(&self, value: &Value, span: &Span) -> Result { + let value = value.read(crate::ReadKind::Value)?; + self.raise()?; if value.renderer().is_some() { - match self.mime_value(value, span) { - Ok(bundle) => { return Ok(crate::display::bundle(&bundle).expect("•mime checks its bundle")); } - Err(e) if e.kind.is_control() => return Err(e), Err(_) => () + match self.mime_value(&value, span) { + Ok(bundle) => { + let bundle = bundle.read(crate::ReadKind::Value)?; + self.raise()?; + return Ok(crate::display::bundle(&bundle).expect("•mime checks its bundle")); + } + Err(error) if error.kind.is_control() => return Err(error), + Err(_) => (), } } - Ok([("text/plain".into(), crate::MimeData::Text(self.show(value)))].into_iter().collect()) + Ok([("text/plain".into(), crate::MimeData::Text(self.display_settings().array(&value, false)))].into_iter().collect()) } /// Displays a function using the trees setting, unless that view has already been shown. @@ -532,22 +491,29 @@ impl Session { /// Runs the text that `right` holds, as `⍎` does, with `left`'s keys as names where it's given. Under a trace, it stops the trace first (`program::trace::each_call`). pub(super) fn execute(&self, left: Option<&Value>, right: &Value, span: &Span) -> Result { - crate::program::trace::each_call("text that ⍎ runs")?; + crate::program::trace::Tracing::stop(crate::program::Reason::Execute, Some(span.clone())); let source = Source::new("", Self::source_text(right, span)?); match left { Some(record) => self.scoped(record, span, |session| session.execute_source(source, span)), None => self.execute_source(source, span) } } - /// The text that `right` holds, as `⍎` and `•load` read it: an observing read, which stops a batched run where `right` is a batch of each cell's text (`Value::observed`). + /// The character atom or vector that `⍎` and `•load` observe. fn source_text(right: &Value, span: &Span) -> Result { - right.observed().at(span)?; + let right = right.read(crate::ReadKind::Value).at(span)?; if right.shape().len() > 1 { return Err(Error::new(Kind::Rank, "expected a unit or vector of characters").at(span)); } if !matches!(right.prototype(), Value::Character(_)) { return Err(Error::domain("expected characters").at(span)); } right.elements().map(|e| match e { Value::Character(c) => Ok(c), _ => Err(Error::domain("expected characters").at(span)) }).collect() } + /// The `⎕` read's act (`program::Act::Input`, target part 17): the next line of input as text, or an empty vector at the end of input. + pub(crate) fn line(&self, span: &Span) -> Result { + Ok(match self.execution.line(span)? { + Some(line) => crate::keyed::text(&line), + None => crate::syntax::zilde(crate::Number::float(0.0, crate::settings::float_width())), + }) + } + /// `•load path` runs a file in a module of its own and returns a record of the module's public names: the names its top level assigns that don't start with `_`, in alphabetical order. The file adds no names to the caller, and shows nothing except explicit output. - pub(super) fn load(&self, right: &Value, span: &Span) -> Result { - crate::transform::acts().at(span)?; + pub(crate) fn load(&self, right: &Value, span: &Span) -> Result { let text = Self::source_text(right, span)?; let path = span.path(&text); let code = @@ -586,81 +552,21 @@ impl Session { result } - // Resolving one structural item may execute a group, but never derives an operator or consumes a neighbouring item. The binder alone chooses grammatical reductions. - pub(super) fn resolve(&self, node: &Node) -> Result { - Ok(Bound::new(match &node.kind { - NodeKind::Literal(a) => Binding::Value(a.clone()), - NodeKind::Subscript(index) => Binding::Operator(self::OperatorNode::Subscript(index.clone())), - NodeKind::Function(builtin) => Binding::Function(builtin.function()), - NodeKind::Operator(op) => Binding::Operator(OperatorNode::Primitive(*op)), - NodeKind::Name(name, last) => match last.then(|| self.bindings.take(*name)).flatten().or_else(|| self.bindings.lookup(*name)) { - Some((_, Binding::Released)) => return Err(released(name, &node.span)), - Some((place, value)) => self.read_binding(value, || Some(place))?, - None if *name == Name::ERROR => { - return Err(Error::new(ErrorKind::Value, "$e is the error a handler caught, and exists only in a handler").at(&node.span)); - } - None => return Err(undefined(name, &node.span)), - }, - NodeKind::System(name) => return Err(undefined(name, &node.span)), - NodeKind::Group(_) | NodeKind::Run(_) => return Binder::nested(node, self), - NodeKind::Scope(root, body) => { - let record = self.resolve(root)?.array(&root.span)?; - return self.scoped(&record, &node.span, |session| session.resolve(body)); - } - NodeKind::ArrayLiteral { .. } => self.bracket(node, |nodes, _, _, within| match within { - Within::Key => self.array_result(nodes).map(Some), _ => self.item_result(nodes) - })?, - NodeKind::Dfn(definition) => { - let closure = Closure { definition: definition.clone(), environment: self.bindings.current(), module: self.bindings.module() }; - match definition.kind { - DefinitionKind::Function => Binding::Function(self::Function::new(FunctionNode::Defined(closure), &node.span)?), - DefinitionKind::MonadicOperator | DefinitionKind::DyadicOperator => Binding::Operator(OperatorNode::Defined(closure)), - } - } - NodeKind::Output => { - self.effect(crate::transform::Effect::Acts).at(&node.span)?; - match self.execution.line(&node.span)? { - Some(line) => Binding::Value(crate::keyed::text(&line)), - None => Binding::Value(crate::syntax::zilde(crate::Number::float(0.0, crate::settings::float_width()))), - } - } - NodeKind::Joiner(_) => return Err(Error::new(ErrorKind::Syntax, "ⱼ, ₑ and ᵣ join the two arrays they touch, as in xⱼy or 2ₑn").at(&node.span)), - _ => return Err(Error::new(ErrorKind::Syntax, "unexpected assignment symbol").at(&node.span)), - })) - } - - /// The bracket literal `node`, built from what `part` gives for each of its items and keys. `part` gets them in the order that they evaluate: left to right, with an item's value before its key. It gets a part's nodes, the cell that holds them, their range in that cell and what they are (`Within`), and gives `None` for an absent `⍺`, which drops out. A keyed item's key applies as soon as the key is ready (`Session::keyed`), before the next item evaluates. The key of an item whose value is absent isn't evaluated. A key with no value is a name, which gives both: `[a:]` is `["a":a]`. - pub(super) fn bracket( - &self, node: &Node, mut part: impl FnMut(&[Node], usize, std::ops::Range, Within) -> Result, Error>) -> Result { - let NodeKind::ArrayLiteral { cells, record, .. } = &node.kind else { unreachable!("a bracket literal") }; - if !*record { - let mut arrays = Vec::with_capacity(cells.len()); - for (k, nodes) in cells.iter().enumerate() { arrays.extend(part(nodes, k, 0..nodes.len(), Within::Item)?); } - return Self::list_literal(node, arrays); + /// The role (`binder::role`) of `name`'s binding, or of the field that `keys` reach from the array it holds (`binder::field_role`), as the compiler binds it, read without evaluating anything; an unbound name binds as an array. While a statement compiles, `log` keeps the fact that it read (`tree::Fact`), except for a name whose role no assignment can change (`Name::fixed`). The 1 home of a compile-time role read: the compiler's names and dot reads, and `path_category`. + pub(super) fn path_role(&self, name: Name, keys: &[Arc], log: &mut super::tree::Facts) -> (Category, bool) { + let role = if keys.is_empty() { self.bindings.role(name).unwrap_or(super::binder::UNBOUND) } + else { super::binder::field_role(self.bindings.lookup(name).map(|(_, b)| b), keys) }; + if let Some(log) = log && !(keys.is_empty() && name.fixed()) { + let fact = super::tree::Fact::new(self.bindings.address(name), keys.into(), role); + if !log.contains(&fact) { log.push(fact); } } - let mut entries = Vec::new(); - for (k, nodes) in cells.iter().enumerate() { - let Some(colon) = crate::syntax::key_colon(nodes) else { - entries.extend(part(nodes, k, 0..nodes.len(), Within::Item)?.map(|item| (None, item))); - continue; - }; - // The value and the key evaluate separately, value first. As one expression, `key:f` would be a train. - let named = match &nodes[..colon] { - _ if colon + 1 < nodes.len() => None, [Node { kind: NodeKind::Name(name, _), .. }] => Some(crate::keyed::text(name.text())), - _ => return Err(Error::new(ErrorKind::Syntax, "a key with no value is a name, as in [a:]").at(&nodes[colon].span)) - }; - let at = if named.is_some() { 0..colon } else { colon + 1..nodes.len() }; - let Some(value) = part(&nodes[at.clone()], k, at, Within::Value { colon, named: named.clone() })? else { continue }; - let key = match named { Some(key) => key, None => part(&nodes[..colon], k, 0..colon, Within::Key)?.expect("a key gives an array") }; - entries.extend(self.keyed(node, k, colon, &key, &value)?); - } - Self::record_literal(node, entries) + role } - /// The entries of the keyed item in cell `cell` of the bracket literal `node`, whose `:` is at `colon`: `Keys` applies `key` to `value`, which must give a keyed vector. A name with a batch of atoms gives one entry that holds the batch, as `Keys` gives one entry in each cell, so a record built in a batched run holds batches as a list literal does. `bracket` and a compiled item's end both call it as soon as the item's value is ready, so an item that fails stops the items after it. + /// The entries of the keyed item in cell `cell` of the bracket literal `node`, whose `:` is at `colon`: `Keys` applies `key` to `value`, which must give a keyed vector. A name with a batch of atoms gives one entry that holds the batch, as `Keys` gives one entry in each cell, so a record built in a batched run holds batches as a list literal does. The level that finishes a compiled keyed item calls it as soon as the item's value and key are both ready (`tree::Keyed`). An item that fails there stops the items after it. pub(super) fn keyed(&self, node: &Node, cell: usize, colon: usize, key: &Value, value: &Value) -> Result, Error> { let NodeKind::ArrayLiteral { cells, .. } = &node.kind else { unreachable!("a bracket literal") }; - if let Some(name) = crate::keyed::name(key) && value.carrier().and_then(crate::transform::Carrier::batch).is_some_and(|b| b.rank() == 0) { + if let Some(name) = crate::keyed::name(key) && value.rank() == 0 { return Ok(vec![(Some(name), value.clone())]); } let item = Function::primitive(Primitive::Keys).call(Some(key), value, &cells[cell][colon].span)?.array(&node.span)?; @@ -677,55 +583,64 @@ impl Session { pub(super) fn list_literal(node: &Node, arrays: Vec) -> Result { let NodeKind::ArrayLiteral { form, .. } = &node.kind else { unreachable!("a bracket literal") }; if arrays.is_empty() { return Ok(Binding::from_element(crate::syntax::zilde(crate::Number::float(0.0, crate::settings::float_width())))); } - let result = if *form == ListForm::Cells { - // Each row is a major cell, so unit rows give a vector: `[1⋄2]` is `1 2`. - Value::assemble(&[arrays.len()], &arrays, &arrays[0]) - } else { Value::new(vec![arrays.len()], arrays) }; - Ok(Binding::Value(result.described("invalid array literal").at(&node.span)?)) + let result = if *form == ListForm::Cells { Operation::Join(crate::primitive::Notation::Rows).call(&arrays, 0) } + else { Value::new(vec![arrays.len()], arrays).map_err(Error::from) }; + Ok(Binding::Value(result.map_err(|error| error.at(&node.span))?)) } // Inlined into `dispatch`, this would add its locals to the stack frame of every BPL call, limiting recursion in the browser. #[inline(never)] - pub(super) fn call_defined(&self, first: &Function, left: Option, right: R) -> Result { - let caller = self.bindings.current(); + pub(super) fn call_named(&self, function: &Function, origin: crate::program::Origin<&[Value]>) -> Result { + let FunctionNode::LateBound(name) = function.node() else { unreachable!("a late-bound name") }; + let span = Span::whole(Source::new("", name.text())); + let position = origin.bindings.scope(function).and_then(|(_, scope)| scope.names.iter().find(|entry| entry.name == *name)).and_then(|entry| entry.position.as_ref()) + .ok_or_else(|| undefined(*name, &span))?; + let function = Function::from_value(Binding::from_element(Function::capture_at(&origin, position).into_owned()), &span)?; + let arguments = Function::body_arguments(&origin)?; + let from: Vec<_> = Function::argument_ranges(&origin)?.into_iter().flatten().map(|range| Some(range.start)).collect(); + function.call_bound(&origin, &arguments, &from) + } + pub(super) fn with_call_context(&self, function: &Function, origin: crate::program::Origin<&[Value]>, run: impl FnOnce() -> Result) -> Result { + if function.definition().is_some() || !origin.bindings.scopes.iter().any(|scope| !scope.names.is_empty()) { return run(); } let base = self.bindings.depth(); - let (mut function, mut left, mut right) = (first.clone(), left.map(L::owned), right.owned()); - let mut tail_span = None; - let mut unshy = false; - let mut result = loop { - let (closure, operand, right_operand) = match function.node() { - FunctionNode::Defined(c) => (c, None, None), FunctionNode::Derived(c, operand, right) => (c, Some(operand), right.as_ref()), _ => unreachable!() - }; - let frame = Frame::new(closure.environment, closure.module, closure.definition.locals()); - // The six names that a call binds itself, in slot order (`Name::implicit`): ⍺, absent without a left argument, then ⍵ ⍶ ⍹ ∇ ⍢. - let implicit = [ - Some(left.map_or(Binding::Absent, Binding::Value)), Some(Binding::Value(right)), operand.map(|f| f.value()), right_operand.map(|f| f.value()), - Some(Binding::Function(function.clone())), operand.map(|_| Binding::Operator(OperatorNode::Defined(closure.clone()))), - ]; - if let Err(error) = self.bindings.enter(frame, implicit, &closure.definition.span) { break Err(error); } - match self.run_definition(&closure.definition, (0, 0), true) { - Ok(Step::Done(bound)) => break Ok(bound), - Err(error) => break Err(error), - Ok(Step::Tail(call)) => { - // Keep lexical dependencies, not the tail caller's execution frame. - let environment = call.function.environment().max(call.right.environment()).max(call.left.as_ref().and_then(Value::environment)); - let keep = base.max(environment.map_or(0, |i| i + 1)); - self.bindings.truncate(keep); - function = call.function; - left = call.left; - right = call.right; - tail_span = Some(call.span); - unshy |= call.unshy; - } + let caller = self.bindings.current(); + let span = origin.site.clone().unwrap_or_else(|| Span::whole(Source::new("", ""))); + self.bindings.enter_context(origin, &span)?; + let result = run(); + self.bindings.leave(base, caller); + result + } + pub(super) fn call_defined(&self, function: &Function, origin: crate::program::Origin<&[Value]>) -> Result { + let defined = match function.node() { FunctionNode::Defined(_) => function, FunctionNode::Applied { op: Operator::Closure(defined), .. } => defined, _ => unreachable!("a defined body") }; + let definition = defined.definition().expect("a defined body"); + let (scope_index, scope) = origin.bindings.scope(defined).ok_or_else(Error::planning)?; + let (parent, module) = (scope.parent, scope.module); + let args = Function::body_arguments(&origin)?; + let (left, right) = (if args.len() == 2 { Some(args[0].clone()) } else { None }, args.last().expect("a function has an argument").clone()); + let operands = function.node().operands(); + let operand = |i| (i < operands.len()).then(|| Binding::from_element(function.operand(&origin, i).into_owned())); + let implicit = [Some(left.map_or(Binding::Absent, Binding::Value)), Some(Binding::Value(right)), operand(0), operand(1), + Some(function.clone().into()), matches!(function.node(), FunctionNode::Applied { op: Operator::Closure(_), .. }) + .then(|| Binding::Operator(OperatorValue::Operator(Operator::Closure(defined.clone()))))]; + let (base, caller) = (self.bindings.depth(), self.bindings.current()); + self.bindings.enter(Frame::new(parent, module, definition.locals()), implicit, Some((Some(scope_index), origin.reborrow())), &definition.span)?; + let result = match self.run_definition(definition, (0, 0), true) { + Ok(Step::Done(bound)) => Ok(bound), + Ok(Step::Tail(call)) => { + let value = Operation::dispatch(call.origin.reborrow()).and_then(|outputs| Bound::from_outputs(outputs, &call.span)).map_err(|error| error.at(&call.span)); + value.map(|mut bound| { if call.unshy { bound.shy = false; } bound }) } + Err(error) => Err(error), }; - if let (Err(error), Some(span)) = (&mut result, tail_span) { error.calls.push(span); } - if let Ok(bound) = &mut result { if unshy { bound.shy = false; } } - self.leave(base, caller, result, &first.definition().expect("call_defined calls a dfn").span) + self.leave(base, caller, result, &definition.span)?.array(&definition.span) } /// Drops the frames from `base` on, makes `caller` current and gives back `result`. A result that refers to a dropped frame is an error. fn leave(&self, base: usize, caller: Option, result: Result, span: &Span) -> Result { + let result = result.and_then(|mut bound| { + if matches!(bound.value, Binding::Function(_)) { bound.value = Function::from_value(bound.value, span)?.into(); } + Ok(bound) + }); let result = match result { Ok(bound) if bound.value.environment().is_some_and(|i| i >= base) => Err(Error::domain("result would return a local closure").at(span)), result => result, @@ -735,36 +650,31 @@ impl Session { } /// Runs `f` in a new frame in which each key on the first axis of `record` names what `T.key` selects: an item of a vector, or a major cell of a higher-rank array. Other names resolve in the caller's scope, and assignments stay in the new frame. `T.(expr)` and `T⍎text` evaluate this way. - fn scoped(&self, record: &Value, span: &Span, f: impl FnOnce(&Self) -> Result) -> Result { + pub(super) fn scoped(&self, record: &Value, span: &Span, f: impl FnOnce(&Self) -> Result) -> Result { record.keys(0).ok_or_else(|| ErrorKind::from(Kind::Domain)).described("a scope needs keys on its first axis").at(span)?; let (caller, base) = (self.bindings.current(), self.bindings.depth()); - self.bindings.enter(Frame::scope(caller, self.bindings.module(), record.clone()), Default::default(), span)?; + self.bindings.enter(Frame::scope(caller, self.bindings.module(), record.clone()), Default::default(), None, span)?; let result = f(self); self.leave(base, caller, result, span) } - pub(super) fn array_result(&self, nodes: &[Node]) -> Result { self.bind(nodes)?.array(&nodes[0].span) } - /// An item of a bracket list, or `None` for an absent `⍺`, which drops out of the list. - fn item_result(&self, nodes: &[Node]) -> Result, Error> { listed(self.bind(nodes)?.value, &nodes[0].span) } - - /// A body's last statement, or an error handler's expression, as the dfn's result. `slot` holds a statement's recording of its binder steps (`tree.rs`). - fn return_expression(&self, mut nodes: &[Node], tail: bool, slot: Option<&std::sync::OnceLock>>) -> Result { + /// The last statement or an error handler supplies the dfn's result. + fn return_expression(&self, mut nodes: &[Node], tail: bool) -> Result { if nodes.is_empty() { return Ok(Step::Done(Bound::new(Binding::NoResult))); } let mut unshy = false; while let [Node { kind: NodeKind::Group(inner), .. }] = nodes { nodes = inner; unshy = true; } - let step = self.statement_step(slot, nodes, tail)?; + let step = self.statement_step(nodes, tail)?; match step { Step::Done(mut bound) => { if unshy { bound.shy = false; } bound.result(&nodes[0].span).map(Step::Done) } Step::Tail(mut call) => { call.unshy = unshy; Ok(Step::Tail(call)) } } } - /// Runs `definition`'s bodies in the current frame, from statement `start.1` of body `start.0`. With `tail`, a last statement that calls a function gives the call to the caller's loop (`Step::Tail`), unless an error guard is active. + /// Runs the current call's statements, forcing guarded work before a handler can catch its checked errors. fn run_definition(&self, definition: &Definition, start: (usize, usize), tail: bool) -> Result { - let frame = self.bindings.current().unwrap(); let mut handlers = Vec::new(); let result = (|| { 'bodies: for (b, body) in definition.bodies.iter().enumerate().skip(start.0) { @@ -774,7 +684,7 @@ impl Session { StatementKind::DefaultArgument => { if nodes.len() == 2 { return Err(Error::new(ErrorKind::Syntax, "default argument needs a value").at(&nodes[1].span)); } if self.bindings.category(Name::ALPHA) == Some(Category::Absent) { - let value = self.bound(&nodes[2..], Some(&statement.recording))?.value; + let value = self.bound(&nodes[2..])?.value; if matches!(value, Binding::NoResult | Binding::Absent) { return Err(Error::new(ErrorKind::Value, "default argument requires a value").at(&nodes[0].span)); } @@ -782,26 +692,20 @@ impl Session { } } StatementKind::ErrorGuard { index: i } => { - let catch = Catch::new(&self.bound(&nodes[..i], Some(&statement.recording))?.array(&nodes[0].span)?, &nodes[i].span)?; + crate::program::trace::Tracing::stop(crate::program::Reason::Guard, Some(nodes[i].span.clone())); + let catch = Catch::new(&self.bound(&nodes[..i])?.array(&nodes[0].span)?, &nodes[i].span)?; + if handlers.is_empty() { self.guards.borrow_mut().push(super::GuardScope::default()); } else { self.guarded(None)?; } handlers.push((&nodes[i + 1..], catch)); } StatementKind::Predicate => { let span = crate::syntax::cover(nodes); - let value = self.bound(nodes, Some(&statement.recording))?.array(&nodes[0].span)?; - let level = if self.transforming() { self.batch_level() } else { None }; - let holds = match level.and_then(|level| crate::batch::truths(&value, level).map(|truths| (level, truths))) { - None => value.boolean().described(PREDICATE).at(&span)?, - Some((level, truths)) => { - match self.guard(definition, frame, (b, position), &truths.described(PREDICATE).at(&span)?, level, &span)? { - Guarded::Unanimous(holds) => holds, Guarded::Joined(value) => return Ok(Step::Done(Bound::from(value))) - } - } - }; + let value = self.bound(nodes)?.array(&nodes[0].span)?; + let holds = value.read(crate::ReadKind::Value)?.boolean().described(PREDICATE).at(&span)?; if !holds { continue 'bodies; } } StatementKind::Expression => { - if position + 1 == body.len() { return self.return_expression(nodes, tail && handlers.is_empty(), Some(&statement.recording)); } - self.bound(nodes, Some(&statement.recording))?; + if position + 1 == body.len() { return self.return_expression(nodes, tail && handlers.is_empty()); } + self.bound(nodes)?; } } } @@ -810,78 +714,20 @@ impl Session { Err(Error::domain("every predicate failed, so no body applies").at(&definition.span)) })(); let mut result = result; + if !handlers.is_empty() { + if let Ok(Step::Done(bound)) = &result && let Err(error) = self.guarded(match &bound.value { Binding::Value(value) => Some(value), _ => None }) { result = Err(error); } + self.guards.borrow_mut().pop(); + } while let Err(error) = &result { let Some((handler, catch)) = handlers.pop() else { break }; if !catch.catches(&error.kind) { continue; } - if let Err(stop) = self.effect(crate::transform::Effect::Catches) { result = Err(stop.into()); break; } - if matches!(error.kind, ErrorKind::Interrupt) { crate::execution::clear_interrupt(); } + if let Some(level) = crate::transform::Level::innermost_batched() { return Err(Error::batch_stop(level.id)); } self.bindings.insert(self.bindings.here(Name::ERROR), Binding::Value(caught(error))); - result = self.return_expression(handler, false, None); + result = self.return_expression(handler, false); } result } - /// A guard on predicate `truths`, which gives each cell of batched run `level` a Boolean of its own, at statement `at.1` of body `at.0` of `definition`, running in frame `frame` (`operators::guard`). Each side runs as a guard case on its own cells (`transform::Levels::case`): the rest of the body where the predicate holds, and the bodies after it elsewhere. A case reads each batched value at its cells (`restricted`), and runs its last call before it returns, to give a value. - #[cold] - #[inline(never)] - fn guard(&self, definition: &Definition, frame: usize, at: (usize, usize), truths: &Value, level: u64, span: &Span) -> Result { - let key = definition as *const Definition as usize; - super::operators::guard(truths, level, |holds, rows, cells| { - let (_, first) = session().levels.borrow_mut().case(Some(key), frame, cells.clone()).at(span)?; - let restricted = session().restricted(frame, first, level, truths.len(), rows, span); - let result = restricted.and_then(|bindings| { - let saved = session().bindings.swap(bindings); - let step = session().run_definition(definition, if holds { (at.0, at.1 + 1) } else { (at.0 + 1, 0) }, false); - session().bindings.swap(saved); - step - }); - session().levels.borrow_mut().end_case(); - match result? { Step::Done(bound) => bound.array(span), Step::Tail(_) => unreachable!("a case runs its last call") } - }) - } - - /// The bindings of each frame from `first` to `frame`, with each batched value of run `level` read at a guard case's cells, the cells at positions `rows` of the run's `cells` cells (`batch::read_cells`), for `Bindings::swap`. These frames are the calls that started during the run. A closure that a case calls reads its defining frame, which can be any of them, whether or not it lies on `frame`'s lexical chain. Frames before `first` started before the run and hold none of its batches. A batch made before the run widened has fewer cells than the run, one for each run of cells that the widening's inner frame repeats it over (`read_binding`), so the case reads it at the cells that hold its rows. - fn restricted(&self, frame: usize, first: usize, level: u64, cells: usize, rows: &[u32], span: &Span) -> Result, Error> { - let read = |whole: &Value, axes: usize| { - let repeats = (cells / crate::array::count(&whole.shape()[..axes])) as u32; - let rows: std::borrow::Cow<[u32]> = if repeats == 1 { rows.into() } else { rows.iter().map(|r| r / repeats).collect() }; - Ok((super::operators::cells_at(whole, axes, &rows, span)?, 1)) - }; - (first..=frame).map(|i| { - let mut copy = self.bindings.frame_bindings(i); - for value in copy.values_mut() { *value = crate::batch::read_cells(value, level, &read)?; } - Ok((i, copy)) - }).collect() - } - - /// Runs `run` in the batched run widened to `frame`, the run's frame followed by the frame of a map inside the run (`batch::tried`). Until it returns, a write to a binding in a frame that started before the widening stops the run (`transform::Levels::case`), and code reads each batch of the run that such a frame holds laid over `frame` (`read_binding`). - pub(crate) fn widened(&self, frame: &crate::array::Layout, span: &Span, run: impl FnOnce() -> Result) -> Result { - self.levels.borrow_mut().case(None, self.bindings.depth(), frame.clone()).at(span)?; - let result = run(); - self.levels.borrow_mut().end_case(); - result - } - - /// `binding` as code reads it. Inside a widened batched run, each batch of the run that a binding made before the widening holds reads as laid over the widened frame (`batch::widened`). While a trace runs, a binding at `place` from outside the traced call's frames is a free name of the call (`program::trace::read`). Elsewhere a binding reads as it is. - #[inline] - pub(super) fn read_binding(&self, binding: Binding, place: impl FnOnce() -> Option) -> Result { - if !self.transforming() { return Ok(binding); } - let tracing = self.levels.borrow().tracing().map(|(level, tracer, frames)| (level, tracer.clone(), frames)); - if let Some((level, tracer, frames)) = tracing && let Some(place) = place() && place.frame().is_none_or(|f| f < frames) { - return Ok(crate::program::trace::read(binding, place, level, &tracer)); - } - let Binding::Value(value) = &binding else { return Ok(binding) }; - let levels = self.levels.borrow(); - let Some((level, frame)) = levels.widened() else { return Ok(binding) }; - crate::batch::widened(value, level, frame).map(Binding::Value) - } - /// What the binding at `place` holds now, as a program's replay reads a free name again (`program::Found`). `None` where nothing is bound there. - pub(crate) fn captured(&self, place: Place) -> Option { - Some(match self.bindings.get(place)? { - Binding::Value(Value::Function(f)) | Binding::Function(f) => crate::program::Found::Function(f), - Binding::Value(v) => crate::program::Found::Value(v), _ => crate::program::Found::Other - }) - } } /// The error for a predicate that isn't one Boolean, in a call and in each cell of a batched run. @@ -891,16 +737,14 @@ const PREDICATE: &str = "a predicate requires a Boolean singleton"; pub(super) enum Catch { All, Kinds(Vec) } impl Catch { fn new(value: &Value, span: &Span) -> Result { - if value.observed().at(span)?.infinity() == Some(false) { return Ok(Self::All); } + if value.read(crate::ReadKind::Value)?.as_real() == Some(f64::INFINITY) { return Ok(Self::All); } let invalid = || Error::domain("a guard names error kinds, as in \"DOMAIN\"::, or catches every kind with ∞::").at(span); let (_, names) = crate::keyed::text_items(value).ok_or_else(invalid)?; Ok(Self::Kinds(names.iter().map(|n| ErrorKind::named(n)).collect::>().ok_or_else(invalid)?)) } pub(super) fn catches(&self, kind: &ErrorKind) -> bool { - match self { - Self::All => !kind.is_control() && !matches!(kind, ErrorKind::Unsupported), - Self::Kinds(kinds) => !matches!(kind, ErrorKind::Stop { .. }) && kinds.iter().any(|k| k.name() == kind.name()), - } + if kind.is_control() { return false; } + match self { Self::All => !matches!(kind, ErrorKind::Unsupported), Self::Kinds(kinds) => kinds.iter().any(|k| k.name() == kind.name()) } } } @@ -913,10 +757,7 @@ fn caught(error: &Error) -> Value { crate::keyed::vector(["kind", "message", "source", "line", "column"].map(Into::into).to_vec(), values).expect("an error fits in a record") } -/// A bracket item's value as its list takes it (`Session::item_result`): `None` for an absent `⍺`, which drops out, or the value as an array, where `span` reports one that isn't. +/// A bracket item's value as its literal takes it: `None` for an absent `⍺`, which drops out, or the value as an array, where `span` reports one that isn't. pub(super) fn listed(value: Binding, span: &Span) -> Result, Error> { if matches!(value, Binding::Absent) { Ok(None) } else { value.into_value(span).map(Some) } } - -/// What `Session::bracket` asks its `part` for: an item without a key, the value of a keyed item, with the position of its `:` and the key where the item's name gives it, or a key. -pub(super) enum Within { Item, Value { colon: usize, named: Option }, Key } diff --git a/src/eval/tree.rs b/src/eval/tree.rs index 8c3e23ae..66f2927d 100644 --- a/src/eval/tree.rs +++ b/src/eval/tree.rs @@ -1,771 +1,811 @@ -//! Compiled statements, stage 3 of `meta/calls.md`. On a dfn statement's first evaluation, the binder's recorder (`Recorder`) watches each decision the binder makes: each node it resolves, each move between its stack and its pending stack, and each reduction. The recorder follows the two stacks over what each item is read from, a register or a constant, and compiles an instruction for each node that needs evaluating and each reduction (`Op`). A move between the stacks compiles to nothing, and an instruction reads a constant where the recording keeps it. Each group or run in the statement is a level (`Level`), whose instructions sit inline between an `Enter` and an `Exit`, so one loop runs the whole statement. Later evaluations run the instructions (`Recording::evaluate`), which make the same calls in the same order with no category decisions, dot-access scans or node dispatch. A recording keeps each fact about what names hold that a level's binding read (`Fact`), and runs a level's instructions only while every fact still holds. A level that dot access, a subscript, a joiner or a run that starts with an operator rewrites also keeps the nodes that the binder walked. The binder's decisions depend only on categories, so a recording checks the category of each name, group and call result against the recorded one. Where one differs, the binder of the level that holds the instruction carries on from the state that the instruction's template records (`Template`, `Binder::resumed`). -use super::binder::{ - Binder, Category, Derived, Entity, Resume, Term, array_of, attached, bound_right, derived, function_of, narrow, split_call, strand, tails, train, -}; +//! The binder's uncached register instructions and value constructors. Each statement compiles against its current categories, then executes once. Graph replay bypasses this executor. +use super::binder::Category; +use super::bindings::Callable; +use crate::syntax::{Definition, DefinitionKind, Letter}; use super::bindings::Address; -use super::operators::{calls_tines, train_call, train_readers}; -use super::session::Within; use super::*; -use std::{borrow::Cow, ops::Range, sync::OnceLock}; +use std::{borrow::Cow, ops::Range}; -/// The registers that a recording holds inline while it evaluates. A recording that needs more holds them on the heap. +/// The statement executor's inline registers; larger statements use heap storage. const INLINE: usize = 8; -/// A statement's compiled binder steps (`Op`), the constants they read, how many registers they use and where the result is, with the facts about what names hold that its binding read, the nodes that the binder walked where `Session::joined` or `Session::members` rewrote the statement, and its groups and runs (`Level`), whose steps sit inline. +/// Uncached instructions, constants, registers and role facts for one statement, including its nested runs and groups. #[derive(Clone, Debug)] -pub(crate) struct Recording { - ops: Vec, constants: Vec, registers: usize, result: Read, facts: Vec, rewrite: Option>, levels: Vec +pub(crate) struct StatementCode { + pub(super) ops: Vec, pub(super) constants: Vec, pub(super) registers: usize, pub(super) result: Read, pub(super) call: bool, + pub(super) facts: Vec, pub(super) span: Span, pub(super) levels: Vec } -/// What a statement's binding read about what a name holds (`Session::path_category`): the category of the binding at the name's address (`Bindings::address`), or of the field that `keys` reach from the array it holds, as it was when the statement recorded. Dot access, joiners, and runs that start with an operator or end in a dyadic one rewrite a statement by what names hold. A recording runs a level's instructions only while each of the level's facts still holds. +/// An assignment's target, as its statement compiled it (`Compiler::assigned_target`): what it assigns to, how many index values a run of it keeps (`Split::slot`), the text of its nodes, `span`, and of its first node, `at`. Running the assignment compiles nothing and reads no name's role. +#[derive(Clone, Debug)] +pub(super) struct Target { pub(super) to: To, pub(super) slots: usize, pub(super) span: Span, pub(super) at: Span } + +/// What an assignment's target assigns to, with the text of what it names. +#[derive(Clone, Debug)] +pub(super) enum To { + /// A name: it takes the value, or for a modified assignment, the array that it holds is updated. + Name(Name, Span), + /// `⎕`, which shows the value. + Output(Span), + /// Brackets of targets, each with the key that a pattern reads and the text of its first node. + Names(Vec<(Option>, To, Span)>, Span), + /// `f∂←` where the flag holds, otherwise `f⁻¹←`: a rule for the function that the name holds. + Rule(Name, bool, Span), + /// A selection from an array that a name holds. + Selected(Layer), + /// A target whose error the assignment raises when it runs. + Invalid(Error), +} + +/// A target expression compiled around its root name. The resulting graph goes through the shared selection walk. +#[derive(Clone, Debug)] +pub(super) struct Layer { + pub(super) split: Option, pub(super) prefix: Option>, pub(super) last: Last, pub(super) last_at: Span, pub(super) span: Span, pub(super) at: Span, +} + +/// A selecting primitive and its index expression, evaluated once per assignment. `simple` marks a single array operand; `keyed` allows creation of missing keyed items. +#[derive(Clone, Debug)] +pub(super) struct Split { + pub(super) p: Primitive, pub(super) index: Result, pub(super) slot: usize, pub(super) keyed: bool, pub(super) simple: bool, pub(super) unit: bool, + pub(super) container: Box, pub(super) at: Span, pub(super) p_at: Span, +} + +impl Split { + /// Whether a run takes this split: one array stands before `p`, or a plain assignment evaluated the keys before it (`Session::extend`). + pub(super) fn taken(&self, run: &[Option]) -> bool { self.simple || self.keyed && run[self.slot].is_some() } +} + +/// A layer's last node: the root array's name, the split's container where the container is that node alone, a layer of its own, or a node that can't end a selection, whose error the assignment raises. +#[derive(Clone, Debug)] +pub(super) enum Last { Name(Name, Span), Container, Layer(Box), Invalid(Error) } + +/// A category or postfix role that binding read before this statement ran. #[derive(Clone, Debug, PartialEq)] -pub(super) struct Fact { pub(super) address: Address, pub(super) keys: Box<[Arc]>, pub(super) category: Category } +pub(super) struct Fact { pub(super) address: Address, pub(super) keys: Box<[Arc]>, pub(super) role: (Category, bool) } -/// Where a reading of what names hold logs what it read: a recorder's facts while a statement records, and `None` otherwise. pub(super) type Facts<'r> = Option<&'r mut Vec>; impl Fact { - /// Whether what the name holds has a different category now. + pub(super) fn new(address: Address, keys: Box<[Arc]>, role: (Category, bool)) -> Self { Self { address, keys, role } } fn changed(&self, session: &Session) -> bool { let bindings = &session.bindings; - let now = - if self.keys.is_empty() { Category::named(bindings.category_at(&self.address)) } else { Category::field(bindings.load(&self.address), &self.keys) }; - now != self.category + let current = if self.keys.is_empty() { bindings.role_at(&self.address).unwrap_or(super::binder::UNBOUND) } + else { super::binder::field_role(bindings.load(&self.address), &self.keys) }; + current != self.role } } -/// Where an instruction reads an entity: a register, which the read empties, or one of the recording's constants, which it borrows. +/// Where an instruction reads an entity: a register, which the read empties, or one of the statement's constants, which it borrows. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum Read { Register(u32), Constant(u32) } +pub(super) enum Read { Register(u32), Constant(u32) } -/// A group or run inside a statement, whose instructions sit between an `Enter` and an `Exit`. Levels are numbered from 1 in the order of their `Enter`s, and 0 is the statement. `node` is the group's position among the nodes of the level that holds it, `parent`, and `exit` is the position of its `Exit`. `facts` and `rewrite` are the group's own, as a statement's are. Its value goes to register `to`, with the text `range`, checked against `check`. Where its category differs, `parent` resumes from `resume`. `kind` says how the binder evaluates it. +/// A group or run inside a statement, whose instructions sit between an `Enter` and an `Exit`. Levels are numbered from 1 in the order of their `Enter`s, and 0 is the statement. Its value goes to register `to`, with the text `range` and the category `check` that the compiler fixed. `span` is its first node's text, where errors in it point. `kind` says what it evaluates. #[derive(Clone, Debug)] -struct Level { - node: u32, parent: u32, exit: u32, facts: Vec, rewrite: Option>, to: u32, range: Range, check: Category, resume: Box