Skip to main content

Target: the finished core

Measure for every step, diff and review in the one-graph release. Existing code supplies behavior and reusable algorithms, not a requirement to retain its architecture. Reuse sound algorithms in their single intended homes. This file owns the current design; archive/one-graph-2026-10-31.md keeps reasons, evidence and history.

Goals, required behavior, acceptance criteria, budgets and explicit process rules are constraints. Parts, interfaces, algorithms and ownership describe the current working design. Continuously improve that design as implementation reveals simpler solutions, updating this document and affected consumers together. Ask Jeremy only when proceeding requires changing a constraint, information only he holds, or a substantive trade-off that cannot be resolved within the agreed priorities. A departure from the working design alone is not a blocker.

Python APIs are not fixed constraints. If preserving Python semantics causes non-trivial complexity, stop that piece to discuss simplifying the API rather than add machinery to the core.

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”.
  • fib or similar recursive functions do not need to be fast at all
  • Value-dependent dynamic √, *, ⍟ and ○ do not need fast, fused or recorded execution. Their explicit approximate forms do. The boundary is a required numeric-value read, not a glyph whitelist; kinds or proofs can already make a dynamic call fast.
  • Ordinary integer arithmetic, including floor ÷, stays native. Unbounded exact arithmetic requires the ᵣ domain and need not be fast.

Non-goals (this release)

  • Speed for dfn guards on traced values.
  • Automatic promotion from ordinary machine integers to unbounded integers/rationals; specializing a cached plan on narrower opaque-input ranges (part 2).
  • 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-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).
  • Compact-loop AD, native loop lowering and checkpointing; native segmented/ragged storage and execution, including continuous batching. Existing Power, dfn fold/scan and nested-array behavior remains in scope (parts 1, 12, 13, 15).

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 and facts derived from kinds, keyed literals, graph operations and current shapes/counts (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 in its normal Call stage. 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. Computation-only execution (program::execute) records without raising. Actual host materialization/readback (Node::into_storage) is a checkpoint and raises before exposing a failed value. Other checkpoints are effects, error guards’ exits, display and statement ends. Leaving an error guard forces the work built inside it. When and how an error surfaces isn’t promised.
  • 1 mode (tinygrad): every numerical operation builds a node and waits; semantic language forms expand to such calls; 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.
  • Representation changes happen at explicit boundaries. Repeated translation or reconstruction in consumers is a reason to redesign the representation or boundary, not add adapters.
  • No per-position side data on operand lists: a fact about an operand travels in its value (a literal node) or is derived when needed by 1 function from the operation and its operands (CallLayout, Operation::roles), never stored and carried through rebuilds.
  • Measure source growth and deletion primarily under src/ in each repository. Report tests, docs and Python support separately.
  • Substantive complexity needs a demonstrated current need. Remove deferred capabilities’ protocols and consumer branches rather than retain them for later. Reuse the last tagged releases’ sound algorithms under one current mechanism; meta/foundation-reset.md maps removals and reuse from the over-complex checkpoints.
  • If no clean design can meet the fixed constraints without Jeremy’s decision, pause the goal and ask under the authority rule above. Do not preserve or reintroduce complexity, add a compatibility route, or silently weaken a budget to continue.

The build, in order

Mechanism owners write connected changes in parallel. One integration coordinator builds and tests frozen snapshots while source editing continues. Main changes assignments, boundaries and build cadence as the work requires. These operating choices do not change the goals or acceptance gates.

  1. Declare the interfaces needed next. Put shared declarations in their target homes before dependent edits. The mechanism owner iterates the design and publishes meaningful contract changes to consumers and main. Repeated caller changes indicate a responsibility that needs a shared home.
  2. Plan a coherent chunk. Keep one mechanism table with the violated contract, owner, planned duplicate removal, status and next proof. Link detailed investigations on demand. Diagnose several examples at their common source, using the working tagged release where useful. Source-file boundaries do not determine ownership.
  3. Write the end state. Delete duplicate machinery as its replacement is written. Keep tightly connected work with one owner. Other owners continue independent work without waiting for a build, another note or routine approval. Do not add an adapter, cache or temporary route to cross an unfinished boundary. 3a. At the source-complete milestone, use ghapi in the kernel to commit all intended files in both repositories before compile integration. Choose the messages without asking for approval.
  4. Integrate coherent snapshots. The coordinator owns builds, mechanical compiler fixes and consolidated checks in the paired build worktrees. Every change to a shared fact’s owner gets its own coherent snapshot, including necessary caller migrations and deletions. Run focused contrasting cases, then the full sweep before dependent work builds on that contract. Do not bundle unrelated fixes because one writer produced them. Treat compiler errors as feedback on the design; simplify a shared representation rather than patch many callers. Return substantive corrections to the mechanism owner. Writers need no global freeze or file lease. 4a. At the completed integration milestone, use ghapi in the kernel to commit all intended files in both repositories, choosing the messages without asking for approval.
  5. Correctness and performance checks. Keep the two-second per-case checks enabled throughout the work. Explicitly identified full-size cases over deadline go in tests/skipped.txt, with their reasons and re-enable criteria; report them as unresolved skips. Keep value failures, route failures, unfinished checks and budget results separate. Never shrink a case or relax a budget to hide a failure. Performance improvements use cheaper shared implementations, never second routes.

Evidence and handoffs

The coordinator’s automated helper inventories applicable cases and modes, reuses healthy workers and continues after watchdog stops. A mode that did not run is unknown; replay with a failed warm-up is blocked. A first observed failure is a regression only if the same case and mode previously passed. Every result names its source pair, immutable build, fixtures and profile. A focused check does not prove full acceptance.

Main samples source during writing and arranges independent reviews of substantive chunks while other work continues. Reviews name the fact’s owner, who can construct it, the duplicate decisions and entry points removed, and any new caller obligations. A passing case alone does not close a duplicated contract. Mechanism owners publish coherent handoffs when a boundary or capacity limit prevents useful progress. There is no second approval ceremony for routine API names or local refactorings.

Flag missing tools, configuration gaps and misleading telemetry immediately. Track each gap in the integration ledger until an invocation of the actual helper verifies the fix. Never substitute an empty report or another tool’s result as proof that it works.

Keeping call sites from spreading again

  • Lengths, errors, levels and settings each have one home. Engine code sees concrete lengths, graph-numeric errors use the session slot rather than Results threaded through kernels (construction, shape, name and host errors remain fallible), 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.
  • Before changing a boundary, classify affected sites: callers that already read and retain the result; item-processing helpers whose parameters narrow; unrelated code that would only pass a new proof along. Stop and redesign the third kind. Prefer existing storage views to a new wrapper. At milestones, report caller spread, net source lines and any work outside the previous ownership boundary.

Existing code: reuse algorithms, remove duplicate mechanisms

The last tagged releases supply working behavior and reusable algorithms. Keep one architecture and reuse sound graph, iteration, traversal and numerical algorithms within it. A function is not obsolete merely because it predates this target. Delete obsolete machinery as replacements are written. Do not require retained-call inventories or separate source-completeness/deletion audits. Main samples design quality during writing; compile integration finds wiring gaps. Existing callers, privacy or names do not justify a second route, cache or compatibility layer.

Preserve released numerical behavior and existing test expectations except for explicitly agreed changes. Checked-error handling changes how required errors are reported, not which inputs are errors; a defined NaN result stays NaN. Compare regressions with the released implementation and reuse its rules before adding special cases. Fix straightforward regressions without changing expectations. If preserving behavior needs substantial complexity, discuss the concrete semantic alternative and its simplicity or demonstrated speed benefit before changing code or tests. Outside the authorized performance phase, additional performance comparisons require Jeremy’s permission.

Flow

call with declared roles → parameter observation → semantic forms (part 5) → levels (Level::rule: newest level’s rule) → single row factory → 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 key and legality guards, 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 recording retains the ordinary graph as a data-free body template. Cached calls bind current inputs and instantiate pending nodes through the shared row factory and node construction, without scheduling native execution or repeating full primitive dispatch. A read owns execution planning and its cache.

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: one primitive entry and newest-level rules; primitive JVPs, linearization/transposition, batching and Select input gating; ordered effects and explicit random state. BPL reuses these transformation ideas with call-time data snapshots. A Python loop traced by JAX is unrolled; lax.scan explicitly requests a compact loop. The foundation follows the unrolled approach for actual Power and dfn fold/scan calls. Compact-loop AD and checkpointing are later measured improvements, not prerequisites for one graph. JAX’s abstract tracers and eager execution are not copied. BPL’s existing length-class/refill design keeps concrete run-time counts; arbitrary cached replay under older levels is reviewed separately from correct nested transformations.
  • Also: Futhark’s bulk map/reduce/scan differentiation, fusion and actual-size choices; Dex’s preservation of whole-array work and indexed accumulation; XLA’s rewrites after transforms and cheap recomputation; DuckDB’s conservative range facts; Dynamo’s retrace limit. Dex/Futhark structured-loop AD and DuckDB segmented storage supply deferred ideas to measure later. Tinygrad commonly builds an unrolled graph from host-language iteration. These systems are role models for mechanisms we need, not a catalogue of capabilities that the foundation must combine.
  • 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). meta/foundation-reset.md’s “Deletion checklist” lists the named old code still in the tree.

  1. Values (array.rs, array/storage.rs). Value: atom | stored array (ArrayData: compact, on rustymath buffers and views, or mixed) | pending (Value::Pending) | carried (a derivative or batch level’s value, Value::Carried). Mixed arrays hold nested values, records and functions; a nested item may be pending (a pytree holding a tracer); numeric leaves enter the graph. Uniform results assemble into compact arrays with more axes; ragged ones stay nested; native segmented storage (entries/shapes/leaf/prototype) is deferred. Native kinds: Boolean, fixed and ordinary integers, f16/f32/f64, complex, characters. ᵣ values, mixed kinds, functions and records are host values: a host boundary borrows the actual value’s nonnumeric structure, never inventing a numeric description. Mixed-array traversal, copying, prototypes, comparison, display and Python/CSV/JSON conversion stay. Every host read of a pending node’s items passes Node::into_storage (part 10’s chokepoint): it refuses unbound linear reads and judges others by input roles; ArrayData::storage reads stored arrays, not trace inputs. A partial read is a Copy node; the scheduler computes only what it reads (tinygrad’s shrink pushed into a kernel). Leaf buffers are host-owned; external and GPU memory: part 23. Not: segmented storage or five-part trees; packing per producer; items cached twice; host reads past the chokepoint; carriers in stored data; level code in storage.
  2. Descriptions (program::Desc; JAX abstract eval): native kind, atom flag, layout, range; nonnumeric structure stays in Trees or the host value. The row factory derives a step’s description from its origin in 1 kind/shape/range pass over operand descriptions and parameter values, before choosing the step; Desc::leaf describes a leaf once, on entry. Kinds: Boolean; fixed integer of a width; ordinary machine integer of a width; float of 16, 32 or 64 bits; complex of either width; character; weak or strong. Kinds and their join: 1 home, number::Kind and number::Kind::join; rustymath’s canonical machine-dtype promotion stays shared; array/storage.rs’s 1 stored-result joining policy serves assembly and descriptions through that join. Integral weak+fixed joins use the common dtype, retaining the fixed domain. Only an existing keyed ordinary weak literal contributes its minimum representable integer dtype through Kind::literal; opaque inputs retain their declared kind bounds. Computation and result consume the same projected kinds. Literal projection in containers applies only to a fixed-domain result; ordinary static strands retain their ordinary width. No value-fit admission or new provenance flag. Static kinds join results computed apart (writes, compacting selects, map assembly, ⍣ histories) to 1 compact kind, as JAX gives vmap, where and scan outputs 1 dtype; strands, rows and dynamic kinds keep each item’s kind. Wider accumulators come from seeds, conversions and the ordinary-integer width policy, never an accumulator field. Ordinary integers use machine arithmetic: dynamic kinds narrow where intrinsic kinds and in-graph ranges prove a width suffices, else use i64 and wrap, never inspecting opaque input extrema; static ordinary and explicit fixed integers wrap at their width. Integer ÷ floors, as ÷ₓ does; float arguments keep float division and mixed promotion; ÷ₐ, ÷ᵣ and the unbounded ᵣ domain: part 21. Dynamic kinds keep value-dependent kinds (√ * ⍟ ○ on reals can give complex): where ranges don’t decide one, the call reads the operands it depends on (part 5’s roles), inside and outside traces (part 10). Part 21’s primitive real forms and ÷ₓ (floor division to a machine integer) resolve before the unmarked kind rule and reads, so never depend on values. Under dynamic kinds, real-float ⌊ ⌈ give ordinary integers saturating at the 64-bit range: positive infinity gives the maximum, negative infinity the minimum, and NaN gives 0. Static unmarked real-float ⌊ ⌈ retain floats; explicit ⌊ₓ ⌈ₓ create machine integers at the selected width in both modes. Exact-domain and Gaussian complex rounding keep their domains. Monadic × of floats gives an ordinary integer under dynamic kinds. Keyed and named layouts are graph values: keyed agreement is a node whose maps and length come from the keys at run time, where a key missing on one side reads as the fill (selecting a missing key: INDEX error, part 20); a cache key compares key sets by identity before contents; a layout’s renderer travels in descriptions. Cacheable renderers are canonical data-free callables with no run-time bindings (stateless native ones, the image renderer included); one needing data or bindings renders normally but declines recording and graph caching, through the same scheduler. Ranges (DuckDB statistics) come from each object’s interval rule over operand ranges. Opaque inputs (forced graphs, recordings, recorded bodies) have intrinsic kind bounds; narrower carried ranges are dropped, even proved ones. Literals keep their values in the key; in-graph operation guarantees and shape/count bounds at actual lengths stay, through unrolled iteration too. Ranges keep √ and ⍟ real without reading items, choose narrower machine work, drop a gather’s bounds check where its indices lie inside the axis, and prove an object’s repair predicate false (part 6) through that predicate, never another threshold or admission policy. Possibly wrapped integer results get conservative ranges, never the unbounded interval: the minimum signed integer’s magnitude may wrap negative, so it can’t prove nonnegativity. A read’s kind and range cover every numbered source it may select, its fill included (an invalid static read’s fallback where validity isn’t proved), from descriptions alone. A fold’s range uses its actual item count or a conservative bound for the plan’s accepted lengths. Refill re-derives and validates count-dependent facts (part 10). Not: a segmented description; opaque-input range boxes or numeric contracts on opaque input values; tracking assumed narrow input ranges; range-failure/retrace counters or invalidation on changed input extrema; automatic ordinary-integer overflow to BigInt or integer division to rational; a description from a value’s layout() in a trace; parallel description helpers; a 2nd shape rule.
  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 native output and its logical output tree; or a leaf: an input (key position, role and a free value’s name; no step) or a computed buffer. Nodes, leaves included, are interned by part 4’s structural origin facts (keeping). Forced → result: a read replaces the node with its result and drops its step and operands (tinygrad’s realize), so later graphs read a leaf and no rule matches across it. Live::of walks a read’s live nodes, hashes its key and gathers its inputs; Program::of converts them only on a cache miss; the scheduler, rules, recordings and verify read only Program. A view is a Copy node composed into its readers’ maps, stored (1 copy) only for a reader whose kernel can’t read through a map, as 1 capability test (reads_through) decides; readers of a shared pending copy compose their own reads without storing it. Rustymath’s single numbered-source read planner composes copy chains unmaterialized: Mapping::composed for single-source affine maps, Steps::through for composition by inner axes beyond them; fill routes stay reads of scalar sources (part 4). Not: node builders outside dispatch; scheduling the live graph directly.
  4. Steps (program::Step): the IR. Map Convert Copy Gather Fold Product At Picks Join Call Draw Effect Index (Index: an affine index step), each with its normal runner and glyph; the row factory derives a step’s description and reads at construction and on refill, rule outputs and lowered steps included. Operands are the origin’s list, named by position; no variant holds another list. The compiler’s constant producer marks true source constants as literal nodes (Value::constant), including constant arrays, strings and records; functions keep their binding category. Names, captured data and formal arguments are registered inputs. Containers hold their items unread, including literal nodes. Host code forwarding an operand forwards that held value rather than reading and reconstructing it. An actual host item read is an observation boundary and returns a stored value. Data-independent constants computed by host code enter through the same literal constructor. A shape-derived value retains its count/refill dependency or structural observation guard; a current length is never frozen merely because it came from a description. 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; mode (static or dynamic kinds, Mathish: bits, not a parameter read); argument trees for flattened calls; the call’s source site, which internal calls preserve and cached execution binds anew. Host operations key the source base that relative paths need, never diagnostic ranges or source text; numeric keys omit sites. Numeric values a kernel reads, such as tolerance, are operands with parameter roles named by the row’s settings; host Power convergence reads its captured numeric scope, with no Loop setting or data protocol. Callable origins capture the numeric defaults that interpret their deferred bodies; a scoped activation uses those setting inputs and the origin’s mode and never changes process defaults, which only an explicit preferences effect updates, with 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; host iteration uses ordinary transformed body calls, not a loop rule; transposes are part 15’s pass, 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 coefficients (Stride, Rotated, Padded); clamped and reflected reads are 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 gives its structure as parameters and its fill field as a data input; settings, counts, ranks and tolerances stay parameters. Copy (base and optional fill) and Join take numbered sources through rustymath’s shared ReadPlan (part 9). Join finds positions by running the moving primitive on ids and serves strands, ,, ⍪, compaction, Under writes and joins’ transposes; read-back distributes cotangents to every source through part 15’s one mechanism. 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.
  5. Dispatch (Operation::dispatch(Origin); JAX Primitive.bind, tinygrad’s lazy graph): 1 numerical entry for primitives, folds/products, selects, writes, notation/joins, conversions, draws and effects; engine consumers call it, never private builders. In order: semantic forms resolve (row-defined forms and marks call their constituent operations through dispatch; host iteration expands actual function calls, part 13; Select normalizes into ordinary calls and reads, part 14); callable, data and description-only uses are classified before callable captures are traversed (containing a function isn’t calling it); captures of executable callables and operator data join the ordered call-time inputs; then the newest level’s rule handles the call through the same entry. Operation::roles names data and call-time parameter, Count, kind and structural reads before they occur. Higher-order consumers use part 19’s shared typed-body query before selecting native Fold, Product or marked forms. Already-declared primitive operations retain the constant-time base case. 1 borrowed CallView lends the origin, bound functions, resolved role values and descriptions. 1 row factory computes kind, shape, range and geometry once, chooses the Step (or the normal host Call) and derives its reads from those results; that combined result serves live construction and refill alike; fusion::built constructs the pending node. A host-kind operation uses the normal Call stage; unknown result structure is an explicit host/recording boundary, never a generic pending-header protocol. A partial pending read is an ordinary Copy; a fold to an atom stays pending. Not: a second eager/traced numerical route; per-call force policies; builders or executors in consumers; speed rewrites at dispatch; metadata references captured as callees; special pending-atom readers instead of roles; independent describe/plan/maps callbacks recomputing the same geometry; an extra planner cache.
  6. Rows (primitive/facts.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; semantic host iteration uses the same body-call rules); inverse; Under form (structural, or computational through the inverse); linear facts (linear operands, jointly or each, and zero-tangent calls: JAX’s deflinear, defbilinear); identity (its object’s identity where one exists; structural functions such as , hold their own); cost; row facts (passes, alternates, first_axis); structural validity. No transpose per row (part 15). Object, each fact once: common formula; optional, separately named repair predicate and repair routine; partials; ranges; numeric validity conditions; fold properties. The repair predicate is an Ops formula giving 1 Boolean per item, shared by native and JIT execution; it owns width-dependent limits (trig’s large-reduction limits: 2^23 for f64, 2^20 for f32). Object::test stays the Boolean-result formula for comparisons, not this 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 (part 2) needs no test or branch. Checked numeric validity is an Ops Boolean predicate paired with its error kind, shared by native and JIT execution; no predicate means total. The builder derives a step’s potential to record a data error from these facts, its mode, descriptions and range proofs; unknown means potentially raising; Select case planning combines this property over its steps, with no separate validity evaluator. Shared formulas over Ops are the default for elementwise arithmetic; a substantial algorithm can instead be 1 object-owned Call through the same builder and runner, its common computation never duplicated in a formula and a legacy kernel. Native and compiled kernels instantiate the same width-specific formula over their Ops (recipe). The numeric Kernel interface keeps every supported machine kind for each operation, independent of block/JIT admission: Boolean, fixed integers, ordinary machine integers, f16/f32/f64 (f16 works in f32), native complex. Host exact arithmetic stays outside this machine interface, using the same canonical mathematical facts, not the legacy kernel engine. “Numerical algorithms” specifies the shared bodies. Defined forms call other primitives through dispatch (part 5). Not: kind-specific rule files; parallel facts tables (recipe bodies); name checks replacing facts; duplicated common scalar math beside a formula.
  7. Rewrite engine (program/rules.rs; tinygrad PatternMatcher). 1 table of 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. Rules run in the scheduler’s pipeline on Programs after ∂ and batching (XLA’s simplifier after JAX’s transforms), a lone node’s read included (part 8): the hidden specials they introduce (partial sorts, ranks, log1p, expm1) need only plans and kernels, and a pattern also matches its primitives’ batched forms, which carry an axis. 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; it doesn’t match across a read of its intermediate, which replaced that node with its result (part 3). A rule may change last bits, never leave the documented accuracy bounds (kmeans’ cancelling rule is out: decision 14); each has a model-independent reason 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; 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, which is its operator’s own algorithm (monadic -.×, +.×: primitive_expansion); Key and Partition, which keep the released grouping algorithms and normal batching (part 12; a native segmented group representation is deferred). 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.
  8. Scheduler (forcing: graph.rs, program.rs, program/run.rs). 1 pipeline, schedule(&Program, &Running, from, runs) -> Plan, then run preparation and execution, 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 1 Running state supplies bound inputs, callable/template bindings and computed prefix values, never reconstructing initial-only bindings; runs (expected reuse) feeds the compile decision. Scheduling gathers, rewrites, orders, fixes stage ends (tinygrad’s indexing.py) and outputs, plans legal cheap-chain recomputation and folds sharing stages; the cached plan holds this structure and kernel recipes. Run preparation makes cheap actual-count choices within the plan, outside item loops: compilation, contraction’s cheaper read, recomputation where producer/consumer multiplicity sets its cost; a choice changing an assumed kind or structure takes the guard/miss path (part 10). A read walks Live::of (part 3) and runs its graph’s plan from the 1 cache (part 10); only a miss schedules, so a graph seen before costs a key, a probe and the run (cost model); a hit at unchanged lengths copies no description or map. 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 later steps are laid out; a value fuses with its 1 consumer, or with consumers indexing it alike; consumers indexing it differently store it unless it’s recomputed. Recomputable 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); structural where item counts cancel, else run preparation compares actual producer/consumer multiplicity; coefficients retained by unrolled reverse AD follow the same rule. Past the graph, reference counts release each name and temporary at its last use. Select normalization supplies ordinary masked or compacted operations (part 14). Error recording and checkpoints: Measures; data errors never restart a read; descriptions are immutable. 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. A fold whose totals feed later steps runs with them cell by cell where a cell fits the stage buffer (fusion::BUFFERED), the later steps reading its totals and items from cache; the plan chooses this schedule, never the compile decision. Run preparation uses compiled code when available, else compiles the selected recipe once actual items × passes saved × saving per item × expected runs (loop counts, a recording’s reuse) exceed the compile cost; meanwhile 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 call’s read of its own operands (part 5’s roles: a parameter, values deciding a length or a value-dependent kind; selective assignment’s positions, part 16; a call on host values, part 5); an effect, run at its call outside a recording (what it reads, after the effects before it); the memory limit, in bytes: a pending graph whose kept-alive leaves (buffers only it 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’s end and destructuring force 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 readback. Not: a 2nd scheduler or general multiversioning framework; eager executors beside it; a step routed around it to its kernel by kind; lone-call paths beside the pipeline; a plan cache beside the 1 cache; a session registry forcing pending work at effects; executor arms calling own code; masked-error retry state, revised-origin decisions, forwarding, protected retry roots or carries; replay of completed effects or random counters.
  9. Block stages, JIT (program/blocks.rs, program/blocks/, 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 1 numbered-source ReadPlan serves kernels, folds and block stages alike (periods, tiling, runs): it routes (source, position) through positional mappings and binds run-time source views once, scalar fallbacks included: a fallback reads another scalar source; mappings hold no BPL Value; binding never mutates a cached map; 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 object’s scalar repair for those lanes alone, and makes the corrected result available before downstream fused operations; there is no JIT-specific repair threshold or mathematical implementation. jit::KERNELS owns compiled code, keyed by emitted structure, types and modes, never by counts, which are run-time arguments. Run preparation compiles on demand only the useful recipe the actual counts select (part 8’s formula), never every possible version; different counts share code unless they select different emitted code; a selected version keeps its shared handle (Arc) after the first lookup, and plans hold references to that code, never another code cache or copy. Block stages read Booleans, fixed integers, ordinary machine integers, 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, and 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, overflowing differently near the range limits; last bits, infinities and NaNs, and zero’s sign need not agree across routes; a dfn fold keeps 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; the built-in JIT works without an external backend (part 23). Not: a stage’s own cache of compiled code; kernel sightings as a compile rule; basedpl copies of rustymath’s read planning; thresholds per consumer; a primitive → machine-op table; fold admission re-derived beside row facts.
  10. Traces, recordings and cache (program/trace.rs, program.rs, program/run.rs). A trace records the graph that actual calls build; it is no dispatch mode or numerical evaluator. Inputs enter opaque and unforced. Captured free data, operator data and records are snapshotted once at the call and traversed with Trees; replay and deferred execution bind these current inputs, never rereading names or comparing separately kept free values. Callable templates: part 19. A recording holds the ordinary Program as a data-free graph template, with its key and legality guards. Binding a cached call instantiates that template with current data through shared node construction, without loaning or refilling a native execution Plan; only a read schedules and executes the graph; case admission inspects that same instantiated graph, not another native plan. 1 Programs store owns function recordings, stops, Select plans and forced-graph plans: dfns key by their shared Definition, weakly owned, never by closure identity; forced-graph entries without a function owner are evicted least recently used past 256; the JIT code table separately owns compiled code, not plans. Description-backed planning for Select and typed-body admission uses the same ordinary call boundary on unbound native inputs with real descriptions; reading their items ends planning. Typed-body admission reuses the same key, capture snapshot and guard checks, not a private recording lookup or capability store. Unknown host structure ends recording before the normal host call runs once, never creating a fake scalar/empty-layout metadata Value. The read chokepoint distinguishes Count, Structure and Value reads and their input dependencies; unbound linear reads stay forbidden; description-only references neither execute nor capture callable contents. Recording stops, by the ordinary reason, at argument-dependent structure or control, host-kind data, guards, recursion, execute, error guards, outer writes and binding/settings effects; a body whose recording can’t represent or serve it still builds the same graph through the same operation entry. Broader cache admission, under arbitrary older levels or for every host/function-valued result, is a scope question: it needs a demonstrated and discussed common-use cost or behaviour, never extra protocols. Keys hold structural origins, literal values, function structure, numeric input kinds/layout facts (opaque inputs by kind bounds: part 2), axis equalities and parameter ReadKind. Data-input axis lengths key by 0/1/more class, with no buckets; array counts (take/reshape/window sizes) refill at concrete lengths through the row factory; rank, axis, permutation and other structural parameters key by value, as does a count deciding how many calls unroll, never by length class; data-dependent control that can’t be keyed safely stops recording. A hit binds current inputs, checks key and legality and refills changed descriptions and maps, never gathering, rewriting, ordering or placing stages; invalidated kinds, widths, layouts, structural parameters, equalities or proofs take the normal miss path, never speculative contracts. Compiled code stays count-parametric. A retrace limit of eight counts genuine signature changes. Data-dependent array lengths: visit Count roots in chronological Parameter order, schedule only through the next unobserved count, extend the same cache key and return Ran::Rest; the same Running prefix keeps completed effects and draws; a hit refills the suffix, a miss schedules it. Drawn axis names are numbered by first appearance and rebound to current names on replay. Output effects and draws with described results record normally. Not: a 2nd recording builder, store or cache; live closure data in cached programs; late free-name reads; numeric input range contracts; synthetic metadata Values for output reconstruction; generic residual count ports, an output count-index field or Counted multi-output metadata for loop residuals; retry or replay of completed effects; speculative kinds; another length type or bounded-shape framework.
  11. Levels (transform.rs; JAX trace stack). Derivative and Batched are the 2 carried-value kinds. After semantic forms resolve, dispatch picks the newest level among the real data/callable dependencies; a level’s rule calls ordinary operations through dispatch. Levels nest, and nesting stays correct: Each under AD, AD under Each, higher derivatives, nested batching. An older level’s value is shared at the newer level and passes through to its own rules (JAX’s lift). The running batch level is the innermost; a carried batch identifies its level by id; an active resource call reaches the innermost running batch level not already handling it, even when numeric operands are shared. Frame geometry comes from current descriptions, not cached first-run lengths. Framed bodies begin a normal nested Batched level; guard cases split the current run’s cells. Cache reuse under every possible nesting is a separate optimization, never a reason for another carrier, serializer or execution protocol. Not: level data in storage; a separate evaluator for nested maps; widening a live batch run; a correctness refusal merely because a batch or derivative is already active.
  12. Maps (batch.rs, 1 map entry; JAX vmap). Each, Rank, outer product, stencil, f.g, Key and Partition share 1 batching mechanism: dense cells run the function once on framed values under Batched, and primitive rules build ordinary graph operations. A framed value keeps the frame axes it depends on, length 1 where shared (JAX’s not_mapped). Draws and compacting masks cover the full frame before use. Nested or varying-shape cells keep the released mixed-array behaviour, through the same function-call boundary where they can’t batch; their numeric leaves still use the graph. Key/Partition grouping stays correct with no native segmented storage kind. Dfn folds/scans keep independent-lane batching (part 13). A body returning a drawn axis name runs cell by cell and returns a nested result. Not: a per-operator evaluator; primitive-name checks in batching; segmented leaf kernels, segment roles or per-consumer packing protocols; new ragged execution machinery without a demonstrated workload need.
  13. Host iteration (eval/operators/power.rs, eval/operators/folds.rs; released Power and dfn fold/scan algorithms). Power, dfn fold and dfn scan, resolved by OperatorInfo, apply their actual function values in 1 language-level iteration home, reusing the released count, count-array, negative-count, predicate, convergence and history algorithms. Each body call takes the ordinary bound function entry, keeping current origin, captures, mode and settings, and can reuse its recording from the 1 cache. Under AD the actual body calls build ordinary primal and linear graph steps as execution proceeds, as the released unrolled AD did. A count array keeps the initial state at 0 and the requested states in their original order; inverse counts and negative infinity apply the inverse body. Infinite Power uses Numeric::convergence() and compares a whole logical carry, not independently frozen numeric items. Arbitrary dfn folds keep right-to-left calls; arbitrary dfn scans keep left-to-right calls, their seeds, identities, empty cases and output structure. Independent lanes batch through the ordinary map entry. A body admitted by part 19’s shared typed-body query uses the same primitive facts, identities, optimized Fold steps and algebraic floating-point freedoms regardless of its spelling. Where the primitive permits reassociation, as +/ does, cancellation can change more than the last bits; substitution adds no freedom to reorder non-associative operations or effects. Arbitrary bodies retain ordered calls. Empty-fold admission derives scalar argument descriptions from the existing kind, never a sampled cell or allocated dummy array. A count deciding how many body calls unroll is structural (part 10): array-length Count refill never lets an unrolled three-call graph serve four calls. A data-dependent predicate reads primals and follows the ordinary recording-stop rules, still computing the right derivative of the executed path. Histories use ordinary compact/mixed assembly; their language behaviour isn’t deferred. Effects occur on every actual body invocation. Not: Step::Loop, count-1 wrappers or loop IR; loop-specific JVP/transpose; compact-loop AD, primal/residual/linear body splits, carry-and-residual-count packets, history-axis metadata protocols, checkpointing; a native iteration compiler or repeat kernel lowering; a 2nd iteration runner; restoring the old Linear evaluator.
  14. Selects (program/control.rs). ⍚, f@[p] and guards on framed values normalize at the semantic-form boundary, before levels, into ordinary graph operations. Case admission takes the single cached planning route from descriptions (part 10), planning cases on whole arguments. Masked execution needs item-local dependencies, no effects and 1 common output layout and kind; under dynamic kinds, no value-dependent result kind; under checked dynamic execution, no case able to record a data error (part 6). Otherwise compact from the start. Static graph-numeric cases need no data-error admission check. Ordinary Choice/Picks assemble masked outputs; the selector is run-time data, not differentiated; Choice is jointly linear in its choice values. Compaction gathers each case’s selected cells, calls the original bound function once on that whole group and scatters its results; a case may reduce its group and fill the selected positions, with no implicit per-cell map; a mask with fewer axes selects whole cells. Under ∂, gate each active argument, capture and tangent direction before calling the original bound function through the normal call entry: select(mask, x, stop_gradient(x)) for primal inputs, Choice against zero for directions, derivative-marked Pending directions included (JAX’s double-where protection against an inactive infinite slope). Planned primal case bodies establish admission but never replace actual calls under AD, which must honour attached derivative rules. Both forms use ordinary graph AD and linear::read_back, accumulating at repeated positions; reverse uses the actual forward’s coefficients, never replaying a case to rediscover its branch. A selector read deciding structure follows the normal recording boundary, with no exemption from acceptance or cost requirements. GPU lowering can later use a per-lane branch. Not: an opaque Select Step or executor; Select-specific JVP/VJP wrappers or capture-gradient projections; a 2nd case planner; admission by spelling or narrowed ranges; masked-error retry; cross-Program case splicing beside normal function invocation.
  15. ∂ (derivative.rs, linear.rs; JAX jvp, linearize, transpose). Derivative levels and tags give forward, reverse and higher-order differentiation; tags, escaping-value stripping, custom JVP rules (f∂←g), •detach, nested AD and batching stay supported. Differentiated numeric leaves promote to real-floating kinds by the normal float-width rules, and seeds take the promoted input kind; parameters and nonnumeric structure stay unchanged; complex AD stays unsupported until its convention is agreed. Primitive JVPs come from rustymath partials through rows; zero-tangent operations run on primals in 1 dispatch rule. Linear nodes are ordinary graph steps over unbound seeds, their derivative level on the node, not its description or cache key. Forward mode binds a seed; reverse mode transposes the ordinary linear steps in 1 pass using row linearity facts. Reuse the released argument/parts traversal and Jacobian reconstruction, not its separate Linear/Op evaluator. A Jacobian uses the normal map entry over unit seeds, choosing rows or columns from actual dimensions. Ordinary graph coefficients keep the actual intermediate values the derivative needs. A recorded linear function walks the ordinary graph bounded by its derivative level: nodes outside that level become inputs holding the actual current coefficient Values, their pending graphs and older levels included; current seeds are argument inputs. Body calls made by Power and dfn folds/scans unroll into these same steps (part 13). A shared frame input uses normal spreading, whose transpose sums across the frame. 1 linear::read_back transposes numbered-source read plans, reading where positions are unique and accumulating where they repeat; copies, joins, selections and fills share it. Select input gating follows part 14. Unbound linear reads are refused at the read chokepoint. Native floating replacement and additive writes differentiate through the same At and read transpose rules: positions are nondifferentiated primal choices; replacement requires distinct destinations; addition accumulates every repeat; broadcast cotangents reduce to the original RHS shape. Structural, shape-changing and host-kind writes have no derivative and raise Unsupported when differentiated; unrelated nondifferentiated writes stay ordinary work. Not: a separate AD evaluator; a separate primal/linear Program partition, coefficient reconstruction, tape, registry or packet codec; residual-history outputs, reverse-loop programs or checkpoint passes; loop-specific derivative body wrappers or packing; linearization by replaying the user’s function to rediscover its executed branch; entry-by-entry Jacobians; Rust closures standing in for BPL functions.
  16. Writes (selection.rs, eval/assign.rs, compiled targets in eval/binder.rs, eval/tree.rs). Supported element writes, whole-item replacement and nested selection composition compile through 1 mechanism; adding identity, enclosure or another supported lhs composition never stops recording or reruns the selector in the interpreter on replay; any speed exception needs Jeremy’s agreement on the concrete feature boundary and an efficient alternative. 1 selection owner keeps g’s read recipes through the pending update and composes them back to the root in the same Program; ordinary reads use computed-value caches, address demand the retained recipes. Each affected numeric leaf uses At, inside and outside traces; Mixed containers and output Trees rebuild the outer structure from the updated children; whole-item replacement substitutes a child value without a numerical write; the scheduler chooses in-place execution. Direct paths (⌷ .[] ⊃ chains) are compositions inside the walk. Key extension (m.k←v, a missing key) rebuilds the containing structure with the new item. Positions (f@[p], a mask) are 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”, while a catenation of selections works (10@{(1↑⍵),¯1↑⍵}, ((1↑a),¯1↑a)←8 9). Nested selections use logical item paths and numeric-leaf reads; logical source paths identify whole native children; native read views stay element selections even over a whole leaf; general nested-to-flat selector construction uses the shared structural-read boundary. 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, so an error leaves the name unchanged; static graph-numeric writes skip invalid numeric positions instead; structural and host-kind errors stay checked (Measures). Positions force; values may stay pending; a trace checks positions when its At runs. Writes into exact-rational or function leaves are explicitly unsupported in this release; native-to-boxed replacement works through the host-kind recording stop. Computed whole-Mixed-root writes can’t change the outer shape or keys (use ordinary root assignment); child replacement may change internal shape or keys; selected outer positions don’t fix their children’s internal shapes. Not: a direct route beside the walk; labels; numerical write loops in structural code; write executors or numerical host writers beside At’s; appends chosen by spelling; a segmented entry/leaf address protocol; a 2nd selector evaluator or interpreter fallback.
  17. Effects, draws. Effects and draws are always nodes, inside and outside traces. Every effect (output, system effects, time, input ⎕) is an ordered node: 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); a stage ends at each effect node. 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; host iteration invokes effects on every executed body call, in order, with no cached loop node. 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). 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). ⍕ 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. A draw is a Draw node, computed when a read reaches it; map nesting comes from the levels. The origin captures 1 opaque generator handle as a data input, its state owned by rustymath’s random.rs; its key and counter are run-time data, never structural parameters; capturing or binding a generator never advances its counter; •prefs with a seed changes the generator key. Each whole Draw reserves 1 stream from its captured generator when it executes; its first read computes the whole draw, which later whole or partial reads reuse; a replay executes a new draw and reserves a fresh stream; deferred read order can decide which draw receives which stream. A batched draw enumerates the whole result in canonical flat item order through rustymath’s Items::new stream, each item with disjoint word positions and rejection space; frame geometry determines result placement. Equal seeds don’t promise equality between different batched/unbatched spellings. Not: host draws or effects beside their nodes; a session registry forcing every pending value at an effect; draws stopping traces; replays repeating draws; a 2nd home for a generator counter; a draw context beside the levels; a 2nd seeded generator; another RNG hierarchy (a special first-cell stream, cell-stream fold or separate cell rejection mapping); a body-upfront draw count, c+k place or replay counter reservation.
  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, through which the compiler may build values from constants (constant folding: ⍤1 derived once): construction, not evaluation. A top-level statement compiles, then runs once. Numeric tokens are interpreted under the active Numeric when their containing statement runs; a bound value can therefore be constructed before a dfn’s body literal. Keep that single timing rule rather than adding literal environments or rewriting shared Definitions when functions are constructed. Binding rules: (1) a call’s result is a value to the binder, as 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 (decision 10), never a restart: 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; a recording checks the roles of the names it reads at the start of the statement and of each group or run. 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). Using a call with no result as a value fails when it runs. Not: evaluation inside the binder; resume machinery; a 2nd node evaluator; a 2nd reader of the grammar.
  19. Function values (eval.rs, eval/operators.rs). Leaves: primitive, system, dfn, late-bound name and function array. An operator application, primitive or dfn operator alike, is 1 Applied variant holding its operator and operands; trains are 1 variant holding their tines, and a train’s array tine stays ordinary data. Attached inverse/JVP rules belong to FunctionData beside its environment. OperatorInfo owns call, semantic-form resolution, operand classification, spelling, inverse, discards and Python name; declared fold/product operations and bitwise resolution use this table, not parallel switches in Function. Function dot/field/index reads are operator kinds with their selector parameter. Framed is an internal function kind for ordinary batching. A Recorded function references an ordinary Program and its current input bindings, never a compact-loop body or residual packet. 1 traversal, the released shared Rebuild/visit_operands, serves capture, template, rebuild and attached rules through its actions; 1 OperandKind rule tells structural operands from data, ranks included; callable/data/metadata roles are classified before capture; description-only references keep their metadata without traversing functions they happen to contain. All executable free data are call-time snapshots in 1 capture order; FunctionOperand distinguishes actual values from positional inputs; a literal operand’s value is its literal node; a recorded body takes its captures and arguments as data (Value::data), and its key holds by value any literal node that still reaches an input. Cached templates strip run-time environment and context and hold no current producer or captured data; current bindings supply the data when called; actual function results rebuild once at the function boundary. Host results this representation can’t hold take the normal host boundary, never universal closure conversion. Nested AD and custom rules stay correct. Higher-order consumers obtain known primitive facts through 1 typed-body query at the requested arity and cell descriptions. Declared primitive and bitwise constructors use the constant-time base case. Other bodies reuse the existing isolated description-backed planning run and guarded Program cache. An admitted body has 1 plain native result from the exact original primitive call, direct ordered formal references with identity read maps, and matching numeric mode and declared settings. A conversion, wrapper, observed argument/control dependency, effect or custom derivative prevents substitution; consume the capture walk’s recursive custom-derivative fact rather than adding a traversal. All nonliteral native captures become the existing description inputs before planning; nonliteral host data or structural captures conservatively decline before the body runs. Literals, callables and settings retain their established provenance. A declined query uses the ordinary function and iteration process. No new IR, capability cache, guessed identity or per-consumer dfn recognizer is permitted. Not: per-wrapper FunctionNode variants; attached-rule wrapper nodes; duplicate child/name/rule traversal; live-name replay reads; a 2nd template IR or registry; loop-specific Recorded input/output projections; Rust closures as BPL bodies; another call route for trains.
  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 ones from the end; a numeric position outside its axis raises INDEX ERROR under checked dynamic execution and reads the fill under static graph-numeric execution, an empty axis included (Measures); a NaN position reads the fill in either mode. Missing keys and other structural errors resolved at the boundary stay 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: the right’s order where every axis of the left operand pairs and the right has more axes; 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): ₓ ordinary machine integer, ᵣ unbounded integer/rational, ₓ₁ ₓ₂ ₓ₄ ₓ₈ 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 normally converts each result as its producer’s stage stores it: ⌊ₓ₁, -ₓ₁, ÷ₛ and ⍳ₓ₁ take no 2nd pass; other marked functions convert their result. A marked literal group, or a superscript on a literal number (3²), folds through the same conversion or call (part 18), never in the lexer or parser. ᵣ: 5ᵣ is 5ᵣ1; a trailing ᵣ converts a value into the unbounded domain, a finite real float exactly from its stored value, without recovering decimal intent; denominator 1 stays in that domain and displays as 3ᵣ, never 3ₓ; xᵣy floors its components without machine narrowing and uses the strict rational-construction division; ordinary ÷ᵣ follows the call’s Mathish policy; Python/JSON conversion rules stay: already-unbounded external values enter this domain without a BPL annotation. ₐ 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 other than division: fₐ ≡ (f args)ₐ; division selects float arithmetic before computing. The marked-form kind entry resolves early, before the unmarked producer’s kind rule, operand reads or level handling (part 5): the primitive forms √ₐ *ₐ ⍟ₐ ○ₐ and inverse-circle real forms to rustymath’s real operations (RealSqrt, RealLn, RealPow and RealCircle‘s code table); machine-integer ⌊ₓ ⌈ₓ ÷ₓ; float division ÷ₐ; rational division ÷ᵣ, which selects the exact domain before the division, never converting its floored result (integer ÷ already floors). Each real 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. Resolved operations use normal dispatch and the objects’ derivative facts; their origins keep the marked call. These mandatory semantics are not late speed rewrites. No kind is speculated (part 2); the listed primitive real forms, ordinary integer ÷ and ÷ₓ are guaranteed fast. Numeric superscripts on functions mean Rank: f¹ ≡ f⍤1, f⁰ ¹ ≡ f⍤[0 1], f⁻¹ ≡ f⍤¯1; one, two or three signed entries use Rank’s normal rules. Bare f⁻ is inverse; f⁻ ¹ ranks the inverse. Numeric-array superscripts remain powers, including x⁻¹. Iteration uses ⍣. 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 (every value a stage end stores 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: ordinary iteration 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); declined, accumulations, mixed, compiles, routes. Facts::reads, 1 home, classifies a reader that takes its argument whole (sorts, grades, searches, ⌹, products). 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. 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, and every other route is checked by the absence of slow events (fallbacks, stored, forced, stopped), which fail unless allowed. 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); 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 stay valid and recaptured when a capacity change requires it; 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<T>) or External(Arc<Region>, 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

These are target interfaces, not promises to preserve today’s names. Each shared fact has one producer, a construction boundary and a consumer API. A coherent change names the superseded entry points and duplicated decisions it removes. Use private construction or types where callers become simpler. Use a narrow ratchet for remaining escape hatches when static enforcement would spread obligations. Pin concrete declarations for the next coherent chunk under the build process above.

  • Desc (part 2): Desc::of(&Value) -> Option<Cow<Desc>> is the 1 projection: pending borrows its description, Batch its cell, Dual its primal’s; stored native leaves use Desc::leaf. Native pending outputs have concrete descriptions after their required Count reads. None is a genuine host value, whose structure is borrowed from the actual current host input or read at the normal host boundary; absence never means scalar, empty layout or a fabricated numeric kind.
  • Outputs<T>: flat leaves and optional Trees, keeping shy/no-result information; an empty tuple differs from no result. Descriptions use the same logical tree, with optional native Desc leaves for genuine host leaves. Map, bind, return and genuine tuple construction keep the packet as it is; a Value is assembled only to cross an actual language-function boundary, and that function’s result is flattened once. Program.outputs: Outputs<Out> owns the result topology, which binding maps directly; recorded functions keep no separate result-tree repair; Programs keep the effect-token root separately. No description→Value→description round trips or per-consumer packet codecs. The row factory returns PlannedOutputs::{Described(Description), Observe}. The output form owns observation admission, arity legality and normalization to control-root descriptors. An opaque host result can have zero described native leaves and a control root; this does not claim that the runtime result is an empty tuple. A valid opaque result, an unavailable native description and a lost operand must not share a stop reason.
  • Step (part 4): run::step(step, operation, reads, values) -> Outputs<Value>.
  • Observation (parts 1, 3, 5): the existing Value::read and runtime read boundary own observation. Item-processing helpers accept the existing storage view, buffer or slice they operate on. Retained native index tables implement RM IndexSource on ArrayData, not Value. Immediate Boolean mask decoding borrows &[bool] through the shared rustymath_buffer::selection::boolean_selector; scalar and array masks use the same validation. Graph construction keeps logical values and descriptions. Preserve the original graph and selection identity. Do not introduce a second stored-value representation, a proof wrapper or extra contexts passed through unrelated code. Default fills, chosen fills, prototype inference and logical write destinations remain distinct responsibilities.
  • Numeric facts (parts 2, 5, 6): primitive rows own language result-kind policy. Rustymath’s mathematical objects own formulas, domains, numerical validity and backend capabilities. These are separate facts. Typed Ops recipes retain their fixed numeric algebra rather than repeat language kind inference. Kernel::unary and Kernel::binary set that fact in the existing private typed_domain; Kernel::domain owns result-kind admission, including already-Complex inputs. Consumers do not exempt Recipes or Complex inputs themselves. Native and compiled execution consume the same facts.
  • Row factory (part 5): declared Count/Structure/Value parameters are observed by the shared call-preparation owner before semantic consumers or a row decode them; ordinary Data keeps its original logical handles and transform levels. The borrowed CallView exposes Desc, known Trees and authorized resolved parameters, never physical inputs or ordinary value reads, and accompanies the original Origin through transform metadata rules and batching callbacks. Geometry consumes the resolved classification; it does not infer it again from parameter values. Keep constructors private where that prevents reclassification without passing a new proof through callers. Function-body numeric helpers use the resolved NumericScope, keeping the original captured setting operands. Run-time host code borrows RunView for explicit logical reads. Origin reborrows and projects through the common implementation.
  • Gather data indices (parts 5, 12): GatherIndex::of(call: &CallView<'_>, position: usize, item: Option<usize>) -> Option<Self> constructs an index only for call.data_positions(). Its fields are private to program. Geometry decodes resolved parameters into known routes before using this constructor. It cannot bypass the eligibility check with a struct literal. Read-relation code inside program can remap the ordinals.
  • Dispatch (part 5): Operation::dispatch(Origin) -> Result<Outputs<Value>> is the entry and the caller of level rules and the builder; call(args) -> Result<Value> only constructs the origin and delegates; a level rule receives the complete origin and returns Result<Outputs<Value>> through dispatch.
  • Levels (part 11): Level { id, kind: Derivative | Batched }; Value::Carried { level: id, .. }.
  • Graph to runs (parts 3, 8): Live::of(roots); Program::of on a miss; schedule(&Program, &Running, from, runs) -> Plan; Plan::run over the same Running state. The count frontier bounds description and refill work, never execution, and proves nothing about earlier execution: remaining work follows actual completion, resident values in the pool, completion tokens and published source results; released pure values are recomputed when demanded; completed effects never repeat.
  • Rules (part 7): Rule has RootKey { kind: StepKind, root: Option<Primitive> }, 1 match giving Bound and 1 build through the shared program builder, which registers nodes, literal inputs and structure-only function templates with positional dependencies through the same bindings and the row factory over its borrowed call view. The table is indexed once by RootKey. A routes-off diagnostic uses the same graph and math objects with optional rewrites, grouping and compilation disabled. Required semantic normalization remains enabled. The inventory declares applicability for tests that inspect routes or storage and for resource-heavy cases. Do not add a second interpreter or semantic implementation.
  • Function values (part 19): FunctionNode::{Primitive,System,Defined,LateBound,Applied,Train,Array,Recorded}; Operator: a primitive OperatorKind or a dfn operator closure; FunctionData: node, environment, optional attached rules; FunctionOperand::{Value, Input}; Framed carries batching geometry.
  • Typed body facts (parts 5, 10, 19): Function::primitive_body(&self, parent: &Origin<&[Value]>, arguments: &[Option<Desc>]) -> Result<Option<Primitive>> is the shared query for higher-order admission. declared_primitive(dyadic) is its constant-time constructor base case, including declared bitwise operations. The declared base case also applies to host-kind cells without native descriptions. Other bodies require known native cell descriptions and use the existing isolated planning recorder and guarded Programs cache. The query exposes an exact original primitive call, not a sampled output or rewritten approximation to a body.
  • Cache (part 10): Programs::serve(base, current_origin) checks discovered-read guards in the 1 function-structure store. 1 cached Recording holds the program, logical output description, effects, itemwise/error properties and discovered reads, serving ordinary invocation, case planning and actual function-body calls.
  • Refill (parts 8, 10): Plan::refill(from, running), from 0 or after a data-determined length, through origins and the row factory; the same bound inputs and prefix values supply parameters and callable bindings through the borrowed call view. A replay or cached graph read runs it when lengths or count parameters changed.
  • Errors (Measures): Session::error holds rustymath’s first-error slot and count; Session::raise() -> Result<()> raises at checkpoints; runners compare counts around steps.
  • Writes (part 16): Scope::new(root)?.select(target) -> Result<Option<Walk>>; Walk::write(values, combine) lowers structural replacement and native-leaf At nodes. WriteTarget::Source { port, frame, distinct } identifies logical destinations independently of the mathematical root. The ReadPlan carries positions, missing addresses and batching; no index buffer is built. A result relation composes those destinations through its data root’s physical read route; dynamic publication first reads the same relation in scalar validation mode.
  • Test policy: the native reference inventory owns case identity, applicable modes, deadlines and skip reasons. Rust tests, worker checks and Python reports consume that inventory. Evaluation and its final lazy read share one execution scope. Skips retain their identities and remain unresolved.

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. 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. 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

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); 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), legality guards 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 —

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: 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.
    • Ordinary calls on a growing array reuse their 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. Existing buffer reuse remains independent of this key policy; no new growing-loop compiler is required.
    • 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: host ⍣ invokes its scalar body through normal recorded-call replay (150 + 135·N an iteration); a scalar op outside a loop ≈ 685 (accepted: “The bar”, “No cost when unused”).
  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

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”). Preserve released ordinary iteration costs; new native loop compilation is deferred.
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 documented compilation procedure and archived target baseline.

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 comparisons use the documented DEV.md compilation commands with the same host, profile, dependency state and kind of edit. Separate warm dependency builds from small incremental edits. Historical target measurements are preserved in the planning archive meta/archive/target-build-time-baseline.md; obsolete function names and dated reruns are not interface requirements.

Acceptance

After compile integration, every model in tests/speed/models.bpl and each held-out set (fresh agents using the docs; implementation forks do not read them) passes under static kinds. Its forward pass and each scalar loss’s derivative use the shared graph and cache, and ordinary dense work meets first-principles costs with each gradient at most about three times its forward pass. Two spellings give the same documented results and comparable costs. Training iteration reuses its ordinary recorded body; there is no requirement for a single compact training-loop node, native loop kernel, compact reverse-loop graph, checkpoint bound or segmented ragged route. Power and dfn fold/scan AD may unroll, with graph/saved-value size proportional to actual executed work. Keep the released language behavior for histories, predicates, convergence, negative counts, nested arrays, Key/Partition, nested AD, effects, draws and external conversions. A deferred capability’s former dedicated requirement must not force its machinery back into the foundation. Record the scope change rather than silently raising a budget; a conflict with common released behavior or substantive cost requires Jeremy’s decision. Under dynamic kinds every model stays correct and costs remain recorded. The two 4x-aim cases remain a real-form fused chain and integer date arithmetic. Every speed rule has a model-independent reason and reference case. Numeric migration preserves example purpose: retain +/÷≢, choose float data, floored machine results or explicit ᵣ case by case, and keep Python/JSON conversions. Check top-width wrapping, denominator-one R retention, negative floor division, ÷ₐ/÷ᵣ and identical opaque-input range policy for forced graphs and recordings. The numeric range policy relaxes no speed budget. All current-scope Not-list deletions and DEV.md’s full checks remain future gates. Revised stage 3 writes the intended pieces, with no separate completeness audit. Compile integration finds wiring gaps, followed by correctness and cost checks. Source writing claims none of those results.

Use

  • 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.
  • Keep the documented design current. Keep no separate amendment document.