VM Functions
Every NSPL expression runs through one expression VM. That covers a route’s SET, WHERE, and
INVOKE, a source or processor filter, a key or ordering expression, an emitter mapping, and a
window aggregate’s arguments. This chapter is the implementation reference for that VM and its
function catalog. It covers:
- how an expression Model becomes a compiled program
- how a program executes over an Arrow batch
- which kernels compute each function family, and how far each one vectorizes
- how window aggregates and sketches keep branch-local state
- how to add a function without weakening any of these contracts
Expression Functions owns every contract a user relies on: signatures, exact types, nulls, errors, limits, and measured costs. This chapter explains how the implementation delivers those contracts, and links to the reference instead of restating them. Four other chapters own neighbouring contracts:
- Data Plane: payload, persistence, and acknowledgement semantics
- Data-Plane Concurrency: the concurrency of branch tasks and published state
- Domain Clock: the execution snapshot that supplies every expression’s time
- Shutdown And Recovery: what a stopping node does with windows it has not emitted
Five rules hold throughout:
- Arrow batches are the only payload. A program reads Arrow columns and writes Arrow columns. No row is ever held as a map of fields. A value every row shares is held once, not copied into each row.
- Types are exact. The compiler never inserts a conversion, and every operation’s result type is decided when the program is compiled.
- A failure belongs to a row whenever it can. A kernel records which rows failed and why. Only a failure of the batch as a whole, such as a UDF trap, fails the batch.
- Time is an input. No kernel reads a clock. The current time arrives in the execution context from the caller’s domain execution snapshot.
- The VM decides nothing about the graph. It runs one program against the bindings its caller hands it, and knows nothing of relays, branches, schedules, or connectors.
Ownership By Layer
The predicate capability and its execution entry point keep paired supported and rejection
Rustdoc examples beside their APIs. just test-capability-docs runs them in normal tests and CI,
separately from execution and concurrency evidence.
| Layer | Owner | What it owns |
|---|---|---|
| Vocabulary | Expression Models in nervix-models | Expression, RouteConstruction, Assignment, Invocation, and JsonPath. A builtin name is a validated BuiltinFunctionName identifier, not a closed set, and a UDF name is a UdfName. |
| Language | nervix-nspl | Parsing a call as a generic name(args) or udf::name(args) into those Models. The grammar has no function-name table; completion emits a typed builtin or UDF expectation at a call position. |
| Engines and infrastructure | nervix-vm | Lowering Models into VM programs, the semantic catalog of every operator, cast, and builtin, type and sensitivity checking, compilation into instructions over typed registers, every kernel, window aggregate lowering and route compilation, and where an execution runs: inline, or admitted through the node’s bounded executor onto its data or extension workers. |
| Engines and infrastructure | nervix-roto | Compiling a typed UDF program on the node’s bulk workers, and the FunctionInjector that answers the VM’s UDF calls over Arrow arrays under a watchdog. |
| Decisions | Registry validation and resource planning | Compiling every expression it can check with the same compiler the runtime uses when a statement is applied, then selecting scheduled UDF programs and lowering generator and emitter routes, source filters, HTTP fields, ordering groups and sink mappings against their declared scopes. |
| Data plane | Runtime plan binding and hosts | Binding runtime programs against installed schemas once per typed node revision, projecting carrier batches into VM input, supplying the execution context and injectors, turning row errors into structured message errors, and owning branch-local window accumulators. |
| Control plane | Subscriptions | Compiling a session subscription’s WHERE into a read-only predicate when the subscription is created. |
The VM compiler, batch constructors, runtime, and FunctionInjector return error-stack
reports with their semantic CompileError or RuntimeError context. A compile error retains a
typed code, its existing stable code spelling through code(), the operation span, and a safe
message. Registry validation adds the owning model and route while retaining the VM report.
Runtime plan binding likewise adds its operation above the original VM report: a
RuntimeVmCompileError names the program and its node, such as a filter, a FILTER-MAP route, an
output branch construction or a generator output, and its caller names the domain above that. A
lowering, LOOKUP_HASH_MAP or materialized-state binding failure stays beneath the same program
context instead of being rendered into its text. Roto UDF setup
returns UdfError reports; its injected calls return VM runtime reports, retaining an underlying
Arrow failure when one caused the call to fail. Jaq compilation, evaluation, and format conversion
return their own typed reports to the codec or signaling caller. These reports are batch or setup
failures; selected-row execution and SideError values remain the row-failure channel.
The compiler failure-contract gate reads source-owned outcome classifications on
SideErrorReason, CIDR NetworkDefect and JSON JsonTargetDefect. These are row or pure type
conversion outcomes; their callers establish the operation report when a whole batch or setup
fails. Errors And Diagnostics owns those compiler contracts.
For ordinary expression completion, the session resolver asks FunctionName for the VM’s sorted
builtin spellings, including datetime names and accepted aliases. That list excludes injected
calls and write_header; it is a candidate catalog, while VM lowering and registry validation
still decide whether a call is valid for its arguments. The parser carries the source or sink kind
with header-function expectations. The resolver adds read_header and read_headers only for a
source that can read headers, and write_header only for a sink that can write them. UDF candidates
come from the selected domain’s Models after the session’s queued transaction changes.
Registry planning converts the validated scheduled Models into typed processor specifications. Before a node revision is installed, runtime plan binding lowers every processor expression against the installed schemas, branch declarations, lookups and UDF catalog. The complete map of bound processor plans is published with the domain routing snapshot. Processor supervisors and concrete branch tasks consume those plans and never read the active graph or a semantic Model.
The Expression Pipeline
Each representation is produced once, at its boundary, and the next stage consumes it:
flowchart LR
Text["NSPL text"] -->|parsed once| Model["Expression Models"]
Model -->|lowered once| Program["VM Program"]
Program -->|"types, sensitivity, folding, sharing, registers"| Compiled["CompiledProgram"]
Compiled -->|"executed per batch"| Result["ExecutionResult"]
Carrier["Carrier Arrow batch"] -->|projected| Input["TypedBatch"]
Input --> Result
Result -->|"rows, selection, row errors"| Host["Routes, message errors, ACKs"]
Models are persisted; programs are not. A compiled program and everything prepared inside it, such
as a compiled regular expression or an IN set, live in memory beside the runtime node that runs
it. Nothing prepared at compile time ever enters a stored Model, and no executable NSPL text is
stored for a runtime to parse again.
Lowering
crates/nervix-vm/src/frontend.rs converts Models once into the VM’s program vocabulary in
program.rs. The result is a Program with an optional filter, ordered set assignments, and
ordered invoke calls, each built from Expr trees. Each route kind has one lowering, and each
lowering fixes the scopes a bare field name reads and writes:
| Lowering | Used for | Bare reads and writes |
|---|---|---|
lower_transforming_route | Transforming routes, which begin empty and may INHERIT | read and write output |
lower_set_only_route | Set-only constructions, such as a set-only processor route or a materialized-state DEFAULT | write output only |
lower_generated_route | Generator routes | read generated, write output |
lower_branch_construction | BRANCHED BY ... SET | read and write branch |
lower_finalized_output_filter | A route WHERE over finalized output | read output only |
lower_route_construction | A construction under a scope policy its caller chooses, such as a correlator route, an emitter VALUES mapping, or an error record | set by the caller |
lower_expression | One standalone expression, such as a subscription WHERE | set by the caller |
Window routes have their own lowering in window.rs, described under
Window Aggregates And Sketches.
SemanticScopePolicy decides what a bare read or write means. A scope the route kind does not
expose is a typed FrontendError, never a fabricated name that later fails name resolution.
Lowering also removes surface forms the compiler does not need to know about:
| Surface form | Lowered to |
|---|---|
IF | CASE |
NOT IN, NOT BETWEEN | negated membership and range tests |
TRY_CAST | a cast whose failure yields a typed null |
INHERIT | plain assignments; a field inherited with LEAK SENSITIVE is wrapped in leak_sensitive |
udf::name(...) | FunctionName::Udf |
Lowering resolves datetime builtins further than other calls, because their literal arguments
choose the kernel. It resolves literal units, time zones, formats, and bin widths once into a
DatetimeFunction, and the call keeps only its row-valued arguments. Every other name is parsed,
case-insensitively, into the closed FunctionName enum. An alias such as ceiling, power, or
substring maps to the same variant as ceil, pow, or substr, and messages always use the
canonical name. A name that matches nothing becomes FunctionName::Unknown, which the compiler
rejects.
A span in the VM is an operation ordinal, not a source offset: the i-th assignment, the route
WHERE, or the j-th INVOKE. Every node inside one operation shares that operation’s span. The
runtime maps a span back to the node, route, operation, operation index, and fields that a
structured message error reports.
The VM never parses NSPL in production code. nervix-nspl is a development dependency that only
its tests and benchmarks use to build Models from text.
The Semantic Catalog
crates/nervix-vm/src/semantics.rs is the one catalog of every operation’s meaning. Registration
is a set of exhaustive match expressions over closed enums, not a runtime table, so a builtin that
misses a decision does not compile. For each builtin the catalog and its neighbours decide:
| Decision | Where | What it records |
|---|---|---|
| Name | FunctionName::parse and as_str in program.rs | The canonical name and its aliases. |
| Lowering | builtin_descriptor, producing a BuiltinLowering | The operation the runtime executes. Some variants carry values prepared at compile time: a regular-expression call, an IN set, an Aho-Corasick matcher, a constant network, or a resolved datetime function. |
| Semantics | builtin_semantics_for_lowering, producing OperationSemantics | Volatility (Immutable, Stable, Volatile), dependency scope (Constant, ExecutionLocal, RowLocal), side effects, whether it can report a per-row error, and null propagation (NeverNull, Strict, Custom). |
| Signature | builtin_signature and builtin_output_type | Arity, exact argument types, and the result type. |
| Conditional arms | builtin_arm_execution, producing ArmExecution | Whether a conditional arm runs the kernel over the whole batch or only over the rows it selects. |
| Folding | fold_builtin_call in compiler.rs | A folding arm, or an explicit refusal. |
| Execution | execute_builtin in runtime.rs | The kernel call. It has no wildcard arm. |
| Optional result | Compiler::expr_may_be_null | Exceptions to “optional when any argument is”. |
ExpressionSemantics::from_operation combines an operation with its children bottom-up. It takes
the most volatile child, the widest dependency scope, and any child’s side effects or ability to
fail. Some calls have no descriptor: leak_sensitive, LOOKUP_HASH_MAP, the header functions,
window aggregates, and UDFs. The compiler handles those specially. For a tree that contains one,
expr_semantics returns no semantics, so the tree is neither folded nor shared through the builtin
path.
No builtin has side effects. The language’s one side effect, write_header, is an INVOKE
statement rather than an expression.
A value rule that both folding and execution apply is defined once in the catalog:
CaseMapping: full Unicode, locale-independentlowerandupperFloatClass: the floating-point classification behindis_nan,is_finite, andis_infiniteBitwiseOperationandIntegerBits: integer bit operations- the IP address and URL text checks
Execution may reach the same value by a faster route. lower and upper map a column whose
visible bytes are all ASCII in one pass over its buffer, for example, but must produce exactly the
value the contract defines. This is what keeps upper('grüßen') and upper(input.text) equal.
Types, Nullability, And Sensitivity
The compiler in compiler.rs infers every expression’s type from its input bindings and the
catalog. Argument types match by Arrow DataType equality. No numeric widening, parsing,
stringification, or datetime–text interchange happens unless the program spells it out as a cast,
TRY_CAST, or JSON_VALUE. Messages name types by their Arrow names, which the
reference maps back to NSPL names. A result is optional when
any argument is, unless the catalog or expr_may_be_null records an exception. The exceptions are
listed in Function Properties.
A program compiles against CompileBindings. Each binding has a namespace, a schema, the sensitive
fields of that schema, and whether it is readable and writable. With more than one binding, input
fields are qualified by namespace, such as left.id and right.id.
Sensitivity is a compile-time property:
- Propagation.
expr_is_sensitivemarks a result sensitive when any operand is sensitive. That includes aCASEcondition, because the arm a sensitive condition selects reveals it. - The only exemption.
leak_sensitive(...)is the only operation that removes sensitivity. It compiles to its argument and costs nothing at run time. - Where a leak is rejected. A
sensitive_leakerror rejects each of these:- a sensitive value assigned to a non-sensitive output field
- an inherited field that would pass sensitive data through
- a sensitive
write_headerargument
- The bypass.
CompileOptions::allow_sensitive_outputturns these checks off where the output never leaves Nervix. A predicate always compiles with it off. - Unbound sensitivity. A binding compiled without its sensitive fields enforces nothing. A caller whose output can leave Nervix must bind the sensitivity of every input it reads.
For direct emitter VALUES, the registry lowers each mapping through the same route frontend and
infers its type and sensitivity against the declared input schema. The inference result retains
sensitivity for each assigned field, including a field assigned more than once. The registry
rejects a sensitive mapping before activation unless its expression explicitly calls
leak_sensitive(...); the target column or attribute is identified without including its value.
A CompileError carries a stable code, a message, and its operation’s span. The codes fall into
these groups:
- binding and namespace errors, such as
unknown_identifierandwrong_stream - typing errors, such as
type_mismatchandunsupported_cast - function errors, such as
unknown_functionandinvalid_argument IN-set errors- output-contract errors, such as
missing_setandnull_for_required_field - invocation errors
sensitive_leak
Folding, Sharing, And Scalar Values
Three mechanisms avoid repeated work. None of them changes a result.
Constant folding. compile_expr folds an expression only when all of these hold:
- it is not volatile and its dependency scope is
Constant - it cannot fail and has no side effects
- the folder produces a non-null value
A folded value becomes one Literal instruction. The folder is deliberately small. It folds only:
- among builtins,
lower,upper,trim,length,coalesce,is_null,nullif,is_nan,is_finite,is_infinite, the bitwise functions,bit_count,contains,starts_with,ends_with,is_ip_address, andis_url - among binary operators, comparisons,
AND, andOR
Casts, arithmetic, membership, ranges, JSON, and every other builtin are never folded.
Folding applies the catalog’s value contracts rather than calling the runtime kernels, so a folded
call and an executed call agree by construction. trim and length are the exception: each has a
separate folding implementation beside its kernel. A new folding arm must reuse a catalog contract
rather than add a second implementation.
Three kinds of constant argument are prepared while the call is lowered: a regular-expression
pattern, a contains_any pattern list, and an ip_in_network network.
- An invalid constant pattern is kept, and every row that evaluates it reports the error.
- An invalid constant network is a compile error.
Sharing. Identical expressions share one register within one generation of the compiler’s
expression cache. The generation advances each time an assignment rebinds a writable field, and
each time an uninitialized field is materialized. So an expression is shared within one assignment,
and, after the last assignment, among the route WHERE and the invocations. The cache key ignores
spans. An expression is shared only when all of these hold:
- it has no side effects and cannot fail
- it is not volatile
- inside a conditional arm, it contains no injected call,
TRY_CAST, or JSON extraction, because those answer only the rows the arm selects and leave nulls on the others
A failing expression is compiled at each occurrence, so each occurrence reports its own error. A
UDF call follows its own rule: it is shared unless the UDF, or a UDF among its arguments, is
declared VOLATILE. Extractions from one JSON document share one scan for each document register
and selection, as the JSON entry of Function Families describes.
Scalar values. A register holds either a column or a scalar, which is a one-row array standing for every row:
- What is scalar. Literals are scalar. An instruction whose operands are all scalar and that
is not volatile is computed once, over one row. That covers an all-literal call that was not
folded, such as
clamp(5, 1, 3), andnow(), which has no operands. - Errors. When a scalar computation fails, as
1 / 0does, its row error is copied to every row that evaluates it. - Expansion. A scalar becomes a column only when a kernel or an output field needs one. It is expanded at most once per register and cached.
- Kernels that read scalars.
Operandimplements Arrow’sDatum, so Arrow comparison kernels, the checked numeric lanes, and most string and selection kernels take a scalar as it is.
Compiled Programs And Their Lifetime
CompiledProgram in ir.rs holds:
- its input and output schemas
- the bindings that load input columns into registers and read outputs from them
- the invocation bindings
- the instruction list
- the register layout of each register space
- the filter register
- an optional injector
Registers are typed. The types are:
- the thirteen scalar types: eight integer widths, two float widths,
Boolean,Utf8, andBinary Datetime, a nanosecond UTC timestampGeneric, for lists
The register spaces are Input, Temp, Condition, and Output. An Instruction carries its
kind, its span, and a selection register. It carries a selection only when its result depends on
which rows it runs for: an operation that can fail, an injected call, a JSON scan, or a cast that
reads or writes text.
Two optimization passes run after compilation:
- Dead instructions and moves. Instructions whose results are never read are removed, except volatile builtins and injected calls. Redundant moves are removed as well.
- Temporary registers. Temporaries are renumbered to reuse slots.
Input registers are never pruned: every readable field is bound, and binding one costs an Arc clone.
There is no separate constant pool. Literals are instructions, and prepared artifacts live inside
their BuiltinLowering behind shared pointers. Cloning a program therefore shares its prepared
patterns, sets, matchers, and pattern caches. CompiledPredicate wraps a program privately, so a
general construction program cannot be passed where only a read-only filter is allowed.
The VM has no notion of plan activation. The host holds each program as a shared
Arc<CompiledProgram>, the primitive boundary’s nervix_primitives::sync::Arc, inside a published
typed processor plan. A prepared artifact lives exactly as long as that plan. An unchanged scheduled
node reuses the exact plan allocation across a domain revision. A change to its topology, schema
fingerprint, resolved branch contract or processor specification binds a fresh plan before
publication. Existing branches adopt it by its typed revision identity between batches, while a
newly appearing branch starts from the same published allocation.
Where Programs Are Compiled
Registry validation compiles or type-infers expressions when a statement is applied and discards the result. The exceptions, which are first checked when their domain’s execution is built or a subscription is created, are listed in Where Expressions Run. The decision layer lowers the installed revision’s expressions into typed plans; node installation binds and compiles those programs against runtime schemas. Execution Plans describes the complete revision and its publication.
| Program | Compiled for execution |
|---|---|
Routes of junctions, deduplicators, reorderers, inferencers, correlators, window processors and WASM processors, and processor FROM ... WHERE and FILTER WHERE | Once when the installed typed processor revision is bound, before its domain routing snapshot is published. Every concrete branch shares the plan’s programs and prepared artifacts. A valid route that needs no VM program records that prepared absence and uses direct Arrow projection. |
DEDUPLICATE ON, reorderer BY, and CORRELATE WHERE | Once when the installed typed processor revision is bound, then shared by every concrete branch |
Ingestor FILTER WHERE, routes, and BRANCHED BY ... SET | Lowered once by the domain’s entrypoint plans, then bound when the ingestor starts, before its source opens. Every source instance and ingest group shares the bound programs. |
Reingestor FROM ... WHERE, FILTER WHERE, routes, and BRANCHED BY ... SET | Lowered once by the domain’s entrypoint plans, then bound for each input relay before that input’s task starts |
Emitter FROM ... WHERE, routes, HTTP request fields, SQS FIFO GROUP, VALUES, and OpenTelemetry mappings | Lowered once into the domain’s typed emitter execution plans, then bound against installed schemas and UDFs when the task starts or swaps. Reconnects reuse the bound programs. |
Window aggregate argument and output programs, inferencer INPUTS, and inferencer output routes | Once when the installed typed processor revision is bound, then shared by every concrete branch |
| Generator routes | Lowered into typed ordered route plans from the committed schedule, then compiled once when the domain’s execution is built or the generator is swapped; each concrete branch task retains those compiled programs and the exact materialized source branch. |
Materialized-state DEFAULT | Compiled and executed in one step when the default binds |
ON MESSAGE ERROR SEND TO ... SET | The registry lowers the ordered assignments before installation. The domain execution or its replacement binds the lowered program once. It reads the original eligible input, the exact captured state, the optional partial output and structured error fields for each failed record. A route update installs a new prepared program; delivery never compiles one. |
Subscription WHERE | When the subscription is created |
An HTTP emitter’s route compiles without its write_header invocations, which are request fields.
The emitter compiles its METHOD, PATH and header writes as one more program: METHOD and PATH
are assignments into an internal, write-only request namespace, so neither is an input of the
program, and the header writes are its invocations. It reads input as the original source record
and, with a codec, output and message as the finalized record; without a body message is the
source record. It runs once for each admitted batch over only the rows the route kept, with the
batch’s execution snapshot and materialized state, so a filtered record evaluates no request field,
and a volatile call such as uuid_v4() yields the value the retained request resends on every
retry.
The Runtime Bridge
The host builds a program’s input with project_vm_input_batch in src/runtime/vm_input.rs:
- Carrier columns. Each is an Arc clone of the carrier batch’s column, checked for its exact type. No buffer is copied.
- Built columns. Branch key fields, broadcast materialized-state values, lookup results, and
selected ingest metadata are built as new columns.
SharedVmInputColumnsbuilds each of them once per dispatched batch and shares it among every output route that reads it. - Uninitialized outputs. These stay
TypedArray::Uninitializeduntil finalization materializes them.
The host also builds an ExecutionContext. It carries the domain execution snapshot’s timestamp,
and optionally an injector for this execution alone, such as the header reader or a window’s
aggregate results.
execute_program_with_selection_in_context returns an ExecutionResult. It holds the output
batch, the selection that survived the program’s WHERE, and any write_header invocations. The
VM applies the WHERE itself, and always keeps a row that carries an error in the selection, so
the caller sees every failure. A caller must therefore check batch.errors() for each selected row
before treating that row as a result.
The kept rows are bitmap operations over the batch: the predicate’s values under its validity, where a null keeps no row, ORed with the bitmap of rows that carry an error when any row does. The selection is read from the set bits of that bitmap, and the same bitmap filters every output column and invocation argument.
src/runtime/filter_map.rs shows the complete handling:
- It acknowledges each row the
WHEREdropped. The kept rows are set in a bitmap directly from the selection, and the dropped rows are its clear bits. - It turns each row that carries an error into a structured message error. The error holds the stable reference, the code, the operation and its index, the fields, and the execution’s domain time, together with the partial output when the error route reads it.
- It rejects each row on which a required output field is uninitialized or null. The rows are found once per batch, as the OR of the inverted validity bitmaps of the required columns, and a batch whose required columns hold no null skips the bitmaps on their null counts. Only a rejected row names its fields.
- It passes every remaining row on as output. The rows it exports are set in a filter bitmap directly from their indices.
- A returned
RuntimeErrorfails the whole batch through the node’s general error handling. The host keeps its report beneath aPlannedGeneralErrorthat names the program by its clause and the step that failed, and hands back the acknowledgements of every message of the batch with it; the report is rendered only when the error policy reports the failure.
A branch construction program keeps the same split: a row whose branch SET recorded an error keeps
that SideError as its own typed outcome, which its route’s error handling formats only if it
reports the row, while a RuntimeError fails the whole batch.
Implementation Map
All paths are relative to the repository root.
| Path | Owns |
|---|---|
crates/nervix-vm/src/lib.rs | The public surface and the with_typed_registers! expansion over the scalar register types |
crates/nervix-vm/src/frontend.rs | Lowering Models into programs, scope policies, and frontend errors |
crates/nervix-vm/src/program.rs | Program, Expr, FunctionName, and resolved datetime calls |
crates/nervix-vm/src/semantics.rs | The semantic catalog: lowerings, semantics, signatures, arm execution, and shared value contracts |
crates/nervix-vm/src/compiler.rs | Type and sensitivity checking, folding, sharing, register allocation, instruction emission, and optimization |
crates/nervix-vm/src/ir.rs | CompiledProgram, CompiledPredicate, instructions, registers, and bindings |
crates/nervix-vm/src/runtime.rs | Execution, register banks, scalar sharing, conditional arms, the builtin dispatch, and the list, cast, and most string kernels |
crates/nervix-vm/src/batch.rs, operand.rs | Typed batches and arrays; column versus scalar operands and broadcasting |
crates/nervix-vm/src/error.rs | CompileError, RuntimeError, SideError, error codes, and sparse RowErrors |
crates/nervix-vm/src/numeric.rs and numeric/ | Checked numeric lanes, comparisons, math, bit operations, and decimal rounding |
crates/simd-kernels/src/flags.rs | Packing the checked lanes’ per-lane failure bytes into bitmap words at the selected SIMD level |
crates/simd-kernels/src/checked.rs, benches/checked_lanes.rs | Checked integer addition, subtraction and multiplication in vector registers at the selected SIMD level, returning each lane’s value together with the failure words, and their measurement beside the lane loop they replaced |
crates/simd-kernels/src/division.rs, division/lanes.rs, benches/constant_division.rs | Constant integer divisors prepared once per call, exact reciprocal quotient and remainder kernels, and comparisons with scalar reciprocal and checked division loops |
crates/nervix-vm/src/datetime.rs and datetime/ | Fixed-unit datetime kernels, calendar arithmetic, time zones, and formats |
crates/nervix-vm/src/text_column.rs | The bounded builder for STRING and BYTES values whose length an argument chooses |
crates/nervix-vm/src/text_search.rs, regexp.rs | Splitting, joining, LIKE, contains_any, NFC normalization, and regular expressions with their caches |
crates/nervix-vm/src/membership.rs, extremum.rs, count.rs | IN sets, greatest, least, and clamp, and integer counts read without narrowing |
crates/nervix-vm/src/bytes.rs | Encodings, hashes, and UTF-8 conversion of BYTES |
crates/nervix-vm/src/json.rs | Typed JSON extraction over one parse per document |
crates/nervix-vm/src/ip_address.rs, url_component.rs | IP addresses, CIDR networks, and URL components |
crates/nervix-vm/src/window.rs, window/route.rs | Lowering window assignments into aggregate demands, and compiling each route’s argument and output programs |
crates/nervix-vm/benches/ | The Criterion harness, workload shapes, and allocation probe |
crates/nervix-roto/src/lib.rs | The UDF injector and its watchdog |
src/registry/validation/ | Apply-time compilation, including window_route.rs and the sketch budget in processor/sketch.rs |
src/registry/entrypoint_plan.rs | Lowering ingestor and reingestor filters, routes, and branch constructions before a node binds them |
src/registry/message_error_plan.rs | Resolving the schemas, branch contracts and flush policies of DLQ routes and lowering their ordered assignments from scheduled Models |
src/runtime/entrypoint_routes.rs | Binding lowered ingestor and reingestor programs to a node’s schemas, state, lookups, and UDFs |
src/runtime/message_error_plan.rs | Binding each error-route VM program once to installed relay services and node-local capabilities |
src/runtime/vm_compile.rs | Runtime compilation and message-error sites |
src/runtime/vm_input.rs | Input projection, lookup key execution, and exporting selected output rows |
src/runtime/filter_map.rs | Program execution and result handling for routes and filters |
src/runtime/ingest_metadata.rs, lookup_hash_map.rs | The header injector and hash-map lookup calls |
src/runtime/message_error.rs | Structured message errors, the rows whose required outputs are missing, and execution of prepared error-record programs |
src/runtime/window_processor.rs, window_accumulator/, window_state.rs | Branch-local windows, their aggregate structures, and their snapshots |
benches/window_sketches.rs | The cost of admitting retained rows into pane sketches, from a new pane for every row to a thousand rows per pane |
src/runtime/window_archive.rs | A captured window as bounded archive Arrow groups, and the window snapshot rebuilt from them |
src/runtime/subscription_predicate.rs | Session subscription filters |
Window checkpoints nest two Arrow containers of the shared snapshot codec, the retained input rows and their argument columns, behind a header that counts the rows and both containers’ bytes. A window whose retained rows fit one 8 MiB snapshot section is encoded in memory under the window’s complete-payload admission; replica synchronization and ownership handoff use that resident encoding, which refuses a window whose rows exceed the whole bulk budget before it encodes a section. A larger window persists through the same container sealed in bounded pieces: each group of rows is sealed and staged on quota-owned disk as soon as it is encoded, the header is placed first once the groups are counted, and the pieces are concatenated one 64 KiB chunk at a time and published as a segmented checkpoint. Each bounded typed section of delayed histogram removals is encoded, staged and released before the next, so the removals never hold more than one section of the bulk budget. The two encodings are byte-identical for the same window. A backup restore seals an archived window the same way, one archived section at a time and a group’s input rows before its argument columns, keeping each archived Arrow section whenever its rows’ identities fit one identity record. A section whose sealing fails stages nothing and leaves the checkpoint as it was, so a restore the node refused only for room seals the same section again.
Retained input keys may repeat, and argument rows are unbranched. The relay restore boundary applies one-record-per-concrete-branch validation after this shared decoder; that relay invariant does not restrict retained window rows. A relay’s restored row views stay within its 8 MiB metadata ceiling, while a branch reopening its window charges the row views of every retained row to the relay memory class, so a window retaining more rows than that ceiling reopens after a restart or a restore. Relay snapshot transfers use quota-owned files independently of the window’s encoding.
Worked Example: clamp
This example traces one assignment from its declaration to its execution:
SET bounded = clamp(input.latency_ms, 0, input.budget_ms)
Here input.latency_ms and input.budget_ms are I64 fields and bounded is an I64 output
field.
-
Parse. The parser produces
Expression::Callwith the nameclampand three argument Models. It knows nothing aboutclamp. -
Lower.
clampis not a datetime name, soFunctionName::parseyieldsFunctionName::Clamp. The assignment is the route’s first operation, and every node in it carries that operation’s span. -
Catalog.
builtin_descriptormaps it toBuiltinLowering::Clamp. Its semantics:- immutable, with a constant dependency scope
- strict nulls
- it can fail, because a row’s low bound can exceed its high bound or be NaN
Its arm execution is
WholeBatch, because two comparison bitmaps and two selections cost less than narrowing a batch to the selected rows. -
Type check.
builtin_output_typerequires three arguments of one identical, ordered type (numeric,STRING, orDATETIME) and returns that type:- an
I32bound beside anI64value is atype_mismatch - a
BOOLvalue isunsupported_function
The result is optional when any argument is, and sensitive when any argument is.
- an
-
Compile. Because
clampcan fail, it is neither folded nor shared. The compiler emitsInstructionKind::Builtin { lowering: Clamp, inputs }. It writes the assignment with a fallback, so a row that fails leavesboundednull. Inside aCASEarm, the instruction also carries the arm’s selection register. The literal0stays a scalar register. -
Execute.
execute_builtincallsextremum::clamp:- Comparisons. For integers and floats it compares the value with each bound in one pass
over the value buffers, with IEEE 754 rules, so a NaN value is neither below nor above
anything. For
STRINGandDATETIMEit calls Arrow’sltandgt. - Selection. Two
zipselections pick the bound or the value for each row.nullifthen nulls every row that has a null argument or invalid bounds. - Errors. It returns bitmaps of the rows whose bounds are invalid. The runtime builds a
SideErrorwithInvalidClampBoundsonly for the set bits of those bitmaps, so a clean batch allocates no error at all.
- Comparisons. For integers and floats it compares the value with each bound in one pass
over the value buffers, with IEEE 754 rules, so a NaN value is neither below nor above
anything. For
-
Report. A failed row reaches the route’s
ON MESSAGE ERRORpolicy. Its error has the codeevaluation, the kindinvalid_argument, the operationset, and the operation index0. The runtime built its field set when it compiled the route: the assigned field and every field the expression reads, sorted asinput.budget_ms,input.latency_ms, andoutput.bounded.
The public contract is in Extrema. Unit tests are in
extremum_tests.rs, and the public qualification is the Greatest, least and clamp outline in
tests/features/runtime/membership_ranges_extrema.feature.
Batch Execution
Typed Batches And Registers
TypedBatch carries:
- its schema, held rather than rebuilt for each batch
- one
TypedArrayper field - its row count
- the row errors it arrived with
A TypedArray has one variant per scalar register type, plus these:
Datetime, a nanosecond UTC timestamp arrayGeneric, for listsUninitialized, for an output no assignment has written yet
Each NSPL type has one Arrow representation, and the VM executes over all of them:
| NSPL type | Arrow type |
|---|---|
U8, I8, U16, I16, U32, I32, U64, I64 | The matching unsigned or signed integer type |
F32, F64 | Float32, Float64 |
BOOL | Boolean |
STRING | Utf8 |
BYTES | Binary. Its values may be empty or hold any octets, and are never read as UTF-8 without an explicit conversion. |
DATETIME | Timestamp(Nanosecond, "+00:00"). RFC 3339 is only a wire representation of it. |
ARRAY<T, n>, VEC<T> | FixedSizeList and List of the element type |
Execution checks the schema once and then works through these steps:
- It allocates one register bank for the batch.
- It loads each input register with an Arc clone of its column. A sliced array keeps its offset, and kernels read only its visible span of values.
- It carries the batch’s incoming row errors forward.
- It runs the instructions in order.
- It reads each output register, expanding a scalar into a column at this point.
- For a program with a
WHERE, it filters the output columns, the invocation arguments, and the row errors together.
Route construction is one ordered program, and it executes as follows:
- Inherited fields. An inherited field that no assignment changes is bound straight to its input register, so the output column is the input column itself.
- Assignments. Assignments run in written order. A repeated target rebinds the field to the later value’s register, so it replaces the current output column. A later assignment that reads the field reads the value it holds at that point.
- Finalization. Each field is then finalized:
- Uninitialized optional outputs materialize as typed nulls.
- A required output that is still uninitialized, or holds a null, fails. The compiler rejects both cases statically wherever it can prove them.
- Route
WHERE. The route’sWHEREthen reads the finalizedoutput.
This is the implementation of The Working Message, which owns the scopes and their edge cases.
Conditional Arms And Selected Rows
The compiler compiles each CASE arm under a selection:
- An arm’s condition is compiled as
coalesce(condition, false), over the rows that no earlier arm answered. - Its result is compiled over the rows its condition selected.
- A nested arm narrows its parent’s selection with
AND.
An instruction observes its arm’s selection only when its result on a selected row depends on which rows it runs for:
- an operation that can report a per-row error
- an injected call
- a JSON scan
- a cast that reads or writes text
Every other instruction runs over the whole batch, even inside an arm. Its value on an unselected
row is discarded, and nothing else it does is observable. This covers every function that cannot
fail, such as lower, replace, date_part, or format_datetime.
For an instruction that does observe the selection, an ArmSelection reads the selection mask
once. It classifies the mask as all rows, no rows, or some rows, and every instruction under the
same mask reuses that classification. The runtime then chooses how to execute each instruction:
| Case | Plan |
|---|---|
| The arm selects no row | Write a typed null without running the kernel. |
| Every operand is scalar and the instruction is not volatile | Compute one row, and copy its errors to the selected rows. |
WholeBatch | Run the kernel over the whole batch, then discard the errors it reported for unselected rows by truncating each row’s error list back to its earlier length. |
SelectedRows | Narrow each column operand to the selected rows with one Arrow filter, run the kernel over those rows only, and scatter the result back with take. Unselected rows get nulls, and errors are re-indexed onto the batch rows. |
execute_select then merges the arms from last to first with Arrow zip. It skips an arm that
selected no row and takes an arm that selected every row as it is.
The catalog’s ArmExecution makes the cost decision for each fallible builtin. Narrowing costs a
few nanoseconds for each row of the batch, so it pays only for kernels that cost more per row:
-
SelectedRows:- regular expressions,
LIKE,contains_any,split,join, andconcat_ws - NFC normalization
- the encodings and decodings,
sha256, and UTF-8 conversion - the transcendental functions
uuid_v7- reading and writing IP address text, and URL parsing
date_trunc,date_add,date_diff, andparse_datetime- casts that read or write text
repeat,lpad, andrpadmust useSelectedRows, and not only for cost. Every row’s text shares one column’s size limit, so text built for an unselected row could exhaust that limit and fail a selected row. - regular expressions,
-
WholeBatch: checked arithmetic,clamp, bit shifts,date_bin,from_unix, and non-text casts, which are vectorized kernels.
An injected call, whether a header read, a window aggregate, or a UDF, receives only the narrowed arguments, the batch indices of the selected rows, and the errors those rows already carry. It is not called at all when its arm selects no row. A JSON scan parses only the selected documents.
AND, OR, and coalesce compile every operand under the same selection. They run Arrow’s
Kleene boolean and zip kernels over whole columns. Only conditional arms narrow evaluation.
Row Errors And Batch Errors
A per-row failure is a SideError: a typed SideErrorReason and the operation’s span. Each reason
maps to one stable ErrorCode:
division_by_zerooverflowcast_failedinvalid_argument
The reason stays typed until the host reports it, so its text is formatted only for a row that
actually failed. RowErrors is sparse. A batch without failures allocates nothing, and
push_failures builds reasons only for the set bits of a kernel’s failure bitmap. Reasons name the
operation, the target type, or a position or length, never the value.
There are two exceptions:
- A regular-expression syntax error carries the regex crate’s message, which quotes the pattern.
A pattern read from a field can therefore appear in
error.message, as Regular Expressions states. - An injected call’s error text is chosen by the injector.
A failure that belongs to the batch is a RuntimeError, returned as Err:
- a schema that does not match the program
- an Arrow kernel error
- a collection larger than Arrow can address
- an execution the bounded executor did not admit, or that panicked on its worker
- a formatted datetime column larger than its offsets allow
- an invalid injected result, such as the wrong type or row count
Not every caller of the VM has an error route. The Errors section of the reference says what each context does with a row error.
Prepared Patterns, Sets, And Matchers
| Artifact | Prepared | Held | Bound |
|---|---|---|---|
| Constant regular expression | Once, when the call is lowered (ConstantPattern) | Inside the program; one search cache is borrowed from the regex’s pool for each batch, so no row takes a lock | 10 MiB compiled size, 2 MiB lazy-DFA search cache |
| Pattern read from a field | On a miss, on the executing thread | DynamicPatterns, one cache per call site per program, shared by clones and across batches. Each batch loads one ArcSwap snapshot and publishes a miss copy-on-write. | 64 patterns, oldest compiled evicted first; rows that repeat the previous row’s pattern skip the lookup |
Constant contains_any list | Once, as an Aho-Corasick matcher | Inside the program | 128 patterns and 64 KiB; over that, each non-null row fails |
Per-row contains_any set | On a miss, for each batch | A per-call cache for one batch; a hit borrows the Arrow strings without allocating | 64 sets per batch |
IN set | Once, from evaluated constant elements | MembershipSet | Up to 8 fixed-width values compared in turn; larger fixed-width sets and every STRING set hashed |
| Constant CIDR network | Once, when the call is lowered | Inside the program | A per-row network is reparsed only when its text differs from the previous row’s |
| Datetime zone and format | Once, when the call is lowered | Inside the resolved call | A 256-byte longest formatted value, checked at compile time |
LOOKUP_HASH_MAP is not a VM function at run time. The runtime rewrites each lookup into an
internal input field, and runs the key expression as its own program before the route’s program.
Identical lookups of one program are answered once, and the answer enters the program as an input
column. A key expression that fails fails the batch. The map itself is the resource version the
graph pins; see Lookups and Resource Versions And
Bindings.
Allocation And Result Bounds
Registers are allocated for each execution and are not reused across batches. Kernels reserve their output from the input where the size is known:
- string builders sized from the input’s offsets and values
- 32 bytes per row for
md5 - numeric lanes sized up front
- one scratch string reused across the rows of a batch
The per-row allocations that remain belong to functions whose output is irregular:
- the Unicode path of
lowerandupper repeatuuid_v4’s text- list
containsandoverlap, which slice each row - the error copied to each row of a failed shared value
The limits users see are enforced in these places:
- Argument-sized text. A value whose length an argument chooses is built through
ByteColumnBuilderintext_column.rs. It sizes each value before allocating it and refuses one that would take the column past 2,147,483,647 bytes. A shared value is charged once for each row it stands for. A refused value fails its row withoverflow. - Text built without that check.
concat,replace,regexp_replace,translate,lower,upper, andinitcapuse Arrow’s ordinary builders, so no row reportsoverflowfor them. Arrow’s builder does not return an error when its 32-bit offsets overflow: it panics. The reference’s Result Size asks users to keep these results within the column limit for that reason. - Other bounds.
splitstops at 65,536 parts,LIKEpatterns at 4 KiB, JSON documents at 16 MiB and 128 levels, andformat_datetimeat its preallocated column. Lists that Arrow cannot address fail the batch withCollectionTooLarge.
Scheduling And Execution Bounds
The VM’s entry point alone decides where a program runs:
| Condition | Where it runs |
|---|---|
At most INLINE_ROW_LIMIT (1,024) rows, and no injected function whose FunctionExecutionPolicy is Extension | Inline, on the caller’s task |
| More than 1,024 rows, and no such function | On the node’s data workers, admitted through the bounded executor |
Any Inject instruction whose injector’s FunctionExecutionPolicy is Extension | On the node’s extension workers, admitted through the bounded executor, whatever the batch size. Every UDF call does this. |
A caller hands the entry point the node’s Executor and awaits the result; it chooses neither
the class nor the charge. An admitted execution is charged to the relay memory class for the bytes
the batch’s columns hold, the usual order of what the program builds from them, and never more
than one relay batch may decode into. A batch Arrow cannot measure is charged that bound. A UDF
runs operator-supplied native code the node cannot bound, so its executions take the extension
class: one that never returns holds an extension worker and leaves the data workers, which relay
bodies are encoded and decoded on, their whole capacity.
Admission can refuse. A class whose wait queue is full refuses the execution with
RuntimeError::ExecutionNotAdmitted, and a job that panics on its worker returns
RuntimeError::ExecutionPanicked. Both are batch errors, which reach the node’s error policy as
the report of the planned step that ran the program, like any other batch failure.
An inline execution never yields and has no cancellation point: it runs every instruction over
its batch to completion, and the batch’s size and the limits above bound that work. An admitted
execution checks between its instructions whether its caller stopped waiting, and returns
RuntimeError::ExecutionCancelled at the next instruction if it did, so it costs at most the
instruction already running. It keeps its relay charge until it actually exits. Stopping a node
or a processor otherwise takes effect between batches. Host loops call consume_budget once per
batch iteration, not the VM. A UDF adds its own watchdog, described
below.
Kernels
SIMD Kernels owns the primitives boundary, dispatch and build targets, caller catalog, exact buffer results, scalar/level qualification, and measured evidence. This chapter owns how the VM selects those operations, masks nulls and failures, and reports errors.
Kernel Classes
Every kernel falls into one of five classes. The class decides how its cost scales and which claim about vector instructions it supports.
| Class | Examples | Claim |
|---|---|---|
| Arrow compute kernel | Boolean logic, STRING/BYTES/DATETIME comparisons, LIKE/ILIKE, contains/starts_with/ends_with, casts, CASE/coalesce/nullif selection, filter/take/zip/interleave, UTC date_part, bitwise_and/or/xor, list sum per row | Whatever Arrow 58.4’s kernels do; Nervix adds none of its own |
| Explicit SIMD lanes | Checked + and - over I8, U8, I16, U16, I32, U32, I64, and U64, checked * below 64 bits, integer column-by-scalar / and % below 64 bits, and I64 column-by-scalar / | nervix-simd-kernels selects the vector instructions once per process, with a scalar fallback, and every level computes the same lanes; the SIMD kernels 07 report and 08 report inspect the generated code |
| One pass over value buffers | Integer division by a column, U64 column-by-scalar / and % and I64 column-by-scalar % with a prepared reciprocal, checked * over I64 and U64, checked float arithmetic, numeric comparisons, fixed-width IN, abs/sign/negation, ceil/floor/round/trunc, sqrt, shifts, classification, fixed-unit date_trunc/date_bin/date_add/date_diff, to_unix/from_unix, length/octet_length/bit_length, ASCII lower/upper, list count | Written so LLVM may auto-vectorize the loop for the target CPU; no claim that it does |
| Library with runtime SIMD dispatch | JSON structure (simd-json), base64 (base64-simd), hexadecimal (faster-hex), sha256 (sha2 with SHA-NI detection) | The library selects instructions at run time; xxh3_64 selects them when the binary is built |
| Irregular, per row | Transcendental math, round(value, digits), zoned and calendar datetimes, datetime formatting and parsing, Unicode case mapping and NFC, regular expressions, Aho-Corasick, substring and padding functions, md5, IP and URL parsing, JSON path walk and conversion, most list functions, UUIDs | Batch API with optimized substeps; scalar work per row |
Arrow Kernel Reuse
The VM reuses Arrow kernels from arrow-arith, arrow-ord, arrow-select, arrow-string, and
arrow-cast wherever their semantics are exactly NSPL’s. It deliberately avoids them in these
cases:
- Numeric comparisons. Arrow’s comparison kernels order floats by the IEEE 754 total order.
NSPL comparison is IEEE 754 equality and ordering, in which NaN equals nothing and the two zeros
are equal.
numeric::Comparisonevaluates every numeric comparison, integers included, over the value buffers.IS DISTINCT FROM,nullif, listcontains,IN,greatest,least, andclampapply the same float rules. - Arithmetic. Arrow’s wrapping or erroring arithmetic cannot report which rows failed. The checked lanes described below compute arithmetic instead.
- List
sumand fixed-width floatdot. These keep ordered accumulation, checked integer overflow, and a finiteness check, rather than Arrow’s wrapping sum. - Three casts. A float-to-
STRINGcast uses Rust formatting. ASTRING-to-DATETIMEcast parses RFC 3339 through chrono.BOOL–DATETIMEcasts always yield null. - UTC
date_part. It reads the column as a zoneless timestamp, so Arrow does not convert every lane through an offset.
List min and max are the one place that intentionally keeps Arrow’s total order. The reference
documents how that differs from greatest,
least, and the window aggregates.
Checked Buffer Kernels
numeric.rs computes every lane of a checked operation in one branch-free loop:
- Failure bitmap. Each lane returns its value and whether it failed. A block of up to 1,024
lanes runs in one loop that stores each lane’s failure as one byte, and one call to
nervix-simd-kernelspacks the block’s bytes into its failure words with vector compares and bitmask extraction at the SIMD level the process selected. No lane shifts its failure into a word, so packing does not decide whether the lane loop vectorizes. - Explicit integer lanes. Integer
+and-, and*below 64 bits, run no lane loop of their own.CheckedIntegerhands the operands’ value buffers, and a scalar operand as the value every lane shares, toCheckedArithmeticinnervix-simd-kernels, which computes a register of lanes at a time and returns each lane’s value together with the failure words, so no failure byte is stored or packed. A sum or a difference wraps in its operands’ own lanes, and the signs of its operands and result, or the carry out of unsigned lanes, give its failure bit at every width. An 8-, 16-, or 32-bit product widens into two registers of lanes twice as wide, where every product is exact: comparing it against the narrow type’s bounds gives the failure bit, and narrowing it back by truncation gives the lane’s value. Every lane holds the wrapped value thatoverflowing_add,overflowing_sub, oroverflowing_mulreturns. A 64-bit product has no wider lane to be exact in, so it stays on the lane loop. - Failed lanes. A failed lane becomes null and its value is zeroed, so a wrapped result never escapes.
- Validity. A result lane is null wherever any operand lane is null.
- No rerun. The kernel never reruns a batch. The failure bitmap, restricted to lanes whose operands are valid, is the only record of a failure.
- Scalar operands. A scalar operand is folded into the lane function, or handed to an explicit kernel as the value every lane shares.
- Inlining. Each operator passes its own lane function or kernel operation, so the call inlines.
The datetime kernels share these lanes. Fixed-unit truncation and binning and to_unix prepare
one reciprocal divisor at entry. For a non-power-of-two divisor d, the prepared unsigned reciprocal is
floor(2^64 / d). Discarding its lower 64-w bits gives floor(2^w / d) at a narrower
operand width w; the high half of its product with a dividend gives a
quotient at most one below the exact quotient. One remainder comparison corrects it. Powers of
two use a shift and mask. Signs are restored after division of unsigned magnitudes, which keeps
MIN representable and distinguishes truncation toward zero from Euclidean rounding. Binning
normalizes both its value and origin, then corrects their phase difference by at most one stride.
It fails a lane if subtracting the distance to the bin start overflows.
Fixed-unit date_diff subtracts in i64 and uses its prepared truncating divisor when the
difference fits. Otherwise it computes the difference and division in i128, then checks the
result against i64. Calendar differences retain their calendar rules. date_add and
from_unix use checked i128 multiplication and addition; from_unix performs no division.
Integer column-by-scalar / and % use the same reciprocal primitive. Division by a column
retains the checked scalar lane loop. A quotient by zero or MIN / -1 fails; a remainder fails
only for zero, so MIN % -1 remains zero. Operand validity still masks failures, and a null
scalar produces an all-null column without running division. The constant-lane benchmark
compares each width’s selected kernel, scalar reciprocal and checked lane loop on identical runs;
U64 and I64 remainder retain scalar reciprocal loops because their vector forms measured slower
on x86-64-v3. The
retained paths and generated instructions are recorded in the
SIMD kernels 08 report.
Floating-point operations fail a lane that produces NaN or infinity. The transcendental functions
come from the platform math library as opaque calls per lane that no loop vectorizes, so those
kernels compute valid lanes only, as the decimal rounding and calendar kernels do. They read
validity one 64-lane word at a time: a word without a valid lane computes nothing and fails no
lane, a fully valid word runs the ordinary lane loop over its 64 lanes, and any other word computes
its valid lanes alone with the failure bytes of its null lanes cleared. The words’ failure bytes
fill the same blocks the other kernels pack, so a null lane never fails, no lane writes a failure
word, and a word with few valid lanes pays for no packing call of its own. round(value, digits) rounds exactly in integer arithmetic
on the value’s significand, and a digit count beyond ±400 rounds as ±400 does. A shift reads its
count from any integer type and fails a negative one. A count at or beyond the value’s width moves
every bit out, and the lanes stay branch-free.
Batch Execution, Compiler Vectorization, And Explicit SIMD
These are three different claims, and the implementation makes them separately:
- Batch execution. Every function computes a whole column in one call, and the cost of dispatch, type checks, and registers is paid once per batch. Every function in the catalog executes this way, including the irregular ones.
- Compiler vectorization. A buffer loop is written so that LLVM can widen it to the vector
instructions of the CPU the binary targets, and its result is the same whether or not it does.
The effective compiler flags determine the baseline: local
target-cpu=nativebuilds use the compiling host’s features, while untuned builds use the compiler target’s baseline. The Debian image builds its x86-64 payloads forx86-64-v3through cargo-sonic. A compiler-vectorized loop needs inspection of the generated instructions for the particular build before claiming a specific instruction set. - Explicit SIMD. The VM hands typed buffers to
nervix-simd-kernelsfor failure packing and checked integer operations. The caller catalog also covers JSON, delivery latency, domain/window admission, selections, and text codecs. Library-dispatched functions retain the library’s selection; xxhash chooses when the binary is built. - Dispatch. The kernel crate caches one
fearless_simdlevel per process and enters it once per buffer operation. The build-target matrix defines SSE2, SSE4.2, AVX2, Ice Lake class AVX-512, NEON, WASM SIMD, and forced fallback, including the ambient baseline that can coalesce lower test tokens into a higher backend. Every integer lane and failure bit is exact. The VM names no instruction set and uses nostd::archortarget_featureof its own.
The VM functions measurement report records what the measurements establish, and Measured Performance turns them into user guidance:
- Fixed-width work. Checked arithmetic ran at about 127 million rows per second on a clean 1,024-row batch.
- Variable-length work. Ragged lists, text search, and JSON ran at single-digit millions.
- Failures. Dense failures made checked arithmetic about six times slower, because each failed row builds an error.
- The executor hop. Crossing the 1,024-row threshold admits the execution onto the data workers, which costs more than executing a small batch.
- Conditional arms. A regular expression in an arm that selects half the batch costs about twelve times one that selects none.
- JSON sharing. Four extractions from one document cost less than a third of four extractions from four documents.
The timings were taken on one development machine with other builds running, so they are diagnostic. Without hardware counters or generated-instruction inspection, they do not establish a SIMD speedup. The SIMD kernels 07 report records the generated instructions and interleaved Criterion rounds of the explicit integer lanes.
Nulls, NaN, Overflow, And Unicode
- Nulls. A strict kernel’s result is null wherever any operand is null. Membership
and length results reuse the input’s null buffer as it is.
IS [NOT] DISTINCT FROMcombines both validity bitmaps into a never-null result. - Cast failures. A strict cast runs Arrow’s safe cast, which nulls a value it cannot convert. It
then reports the rows that became null without being null in the input, and skips that check
when the null counts are equal.
TRY_CASTkeeps those nulls silently. - NaN. Comparisons are IEEE 754 everywhere except list
minandmax. AnINset stores floats by bit pattern after canonicalizing-0.0, and never matches NaN. - Overflow. Integer overflow, division by zero, and
MIN / -1fail their lane. Datetime results outside the nanosecond range fail their lane. A window’s integerSUMaccumulates in i128 and is checked only when the window emits. - Unicode.
STRINGvalues are valid UTF-8 by Arrow’s contract. Case mapping is full Unicode without a locale.lengthcounts code points by skipping continuation bytes.STRINGordering is bytewise, which for UTF-8 equals code-point order. Nervix has no collation.
Operations That Do Not Reduce To SIMD
Several widely used operations cannot be expressed as a fixed-width loop over value buffers. For the functions this catalog delivers, the implementation optimizes the substeps that can be batched and leaves the irregular remainder per row, still inside one batch call:
| Operation | Optimized substeps | Remaining scalar or irregular work |
|---|---|---|
| Unicode case mapping and NFC normalization | A column whose visible text is all ASCII is case-mapped in one pass over its value buffer, reusing its offsets and nulls. NFC writes into one bounded builder. | A column with any non-ASCII byte is mapped row by row with allocation. Normalization composes each row. Nervix has no collation to optimize: strings compare bytewise. |
| Regular expressions and captures | Constant patterns compile once. A batch borrows one search cache per pattern. Field-supplied patterns are cached across batches and deduplicated within one. The regex engine runs its own vectorized literal prefilters. Inside a conditional arm, only selected rows are searched. | Each row runs its own search. Captures allocate their slots once per pattern per batch, then each match is extracted and interpolated per row. |
| JSON extraction | simd-json finds a document’s structure with instructions chosen at run time. Every extraction from one document column shares one parse. A literal document is parsed once per batch. | The path walk over each tape, conversion to the declared type, and the depth check are per row. |
| Formatted datetimes | A format compiles once. Writing lays the format out into fixed-width templates and fills one preallocated buffer. | Parsing is a strict per-row reader. |
| Calendar and time-zone logic | Fixed-length units in UTC or at a fixed offset use the vectorizable fixed-unit lanes. A time zone resolves once from the bundled database. Consecutive lanes inside one offset span reuse one zone lookup. | Months, quarters, years, and local days or weeks under an IANA zone compute per row through Jiff’s civil calendar, including disambiguation and month-end clamping. |
contains_any and IN | Constant pattern lists build one Aho-Corasick matcher. Small fixed-width sets compare in turn and larger ones hash, with the threshold chosen by benchmark. | Each row is scanned or hashed individually. Per-row pattern sets build a matcher per distinct set per batch. |
| Lists | Counts read offsets. First, last, and nth are index arithmetic plus one take. min and max over fixed-width lists without null elements compare whole columns one element position at a time. | Ragged-list extrema, mean, distance, dot, contains, and overlap loop over each row’s elements. |
| Sketches and window structures | Aggregate arguments are evaluated once per batch as Arrow columns. Typed runs fold validity by popcount, exact integer sums by split SIMD lanes, floating sums and centered moments by per-lane folds, and histogram indexes before scattering. Non-finite arguments are classified by column bitmaps. | Candidate deque updates, bucket scatter, BLAKE3 per distinct input, t-digest insertion, and Misra-Gries updates remain irregular. Sketch keys reuse a run buffer. |
| Encodings and hashes | Base64, hex, and SHA-256 use library SIMD dispatch per value. Output lengths are checked before encoding. | Each value is encoded or hashed separately. md5 is scalar. |
| IP addresses and URLs | Constant networks parse once. A containment test is one mask and compare on fixed-width integers. A scalar URL parses once per batch. | Address text and URLs parse per row under the URL Standard. |
| Roto UDFs | One vectorized call per batch or selection, with column methods over Arrow arrays. | Whatever the UDF body does. Its get and builder methods are an explicit per-row path. |
Function Families
The reference owns each family’s contract. These notes describe its implementation.
- Numeric. Checked lanes cover arithmetic, comparison,
abs,sign, and rounding.round(value, digits)rounds exactly, with halves away from zero, using integer arithmetic on the significand. Integer rounding to negative digits computes in 128 bits.- Bit operations reuse Arrow’s
binaryandunaryforand,or,xor, andnot, and branch-free lanes for shifts. - Classification never fails.
- Temporal. A
DATETIMEis signed nanoseconds since the epoch in an Arrow UTC timestamp.- Fixed units compute in checked lanes.
- Calendar units and zones use Jiff with the IANA database it bundles, and never read the host’s zone or locale.
- Time zones, formats, units, and widths resolve once when the call is lowered.
now()and every execution-local instant come from the execution context.
- Strings.
length,bit_length,octet_length, andtrimread or copy contiguous slices.LIKEandILIKEare Arrow kernels behind a pattern-length bound.splitreturns an Arrow list, bounded in parts and bytes.- Search, capture, and normalization are described above.
- Only NFC normalization exists.
- Arrays and vectors.
ListColumnreads a variable list’s offsets or a fixed list’s width.- Construction and
concatuse Arrowinterleave.slicebuilds indices for onetake. ListElementsin the catalog decides, when the call is compiled, which functions reject nested elements, so no batch reaches a kernel with an element type it cannot read.
- Construction and
- Bytes, encodings, and hashes.
BYTESis an ArrowBinarycolumn.- Encoders check each output’s length against the column before encoding.
bytes_from_utf8reuses the text’s buffers without copying.- Hashing preserves sensitivity like any other function.
- JSON. One
JsonScaninstruction per document register and selection parses each document once, into a tape. EveryJsonScanOutputthen walks its path over that tape and appends directly to a typed column of its declared type, truncating back to a mark when a row fails.- A document is never held as an untyped value, and no row becomes a map.
JSON_VALUEandTRY_JSON_VALUEare one extraction with differentCastFailurechoices. Even the tolerant form still reports a result too large for its column.
- Network. An IP address is a 4- or 16-byte
BYTESvalue, held internally as a 32- or 128-bit integer.- A network is a network and mask pair, so containment is one mask and compare.
- URLs parse with the
urlcrate, and the functions never resolve a host or fetch anything.
- Conversions.
ASandTRY_CASTshare one cast path through Arrow’s safe cast. ASTRINGread as a number follows Arrow’s parser.
Context, Determinism, And Extension Boundaries
Domain Time
The one timestamp an execution sees is ExecutionContext::now. The host takes it from the
domain execution snapshot of the unit of work. All of
these use it:
now(), and every datetime function applied to ituuid_v7(), whose timestamp field it encodeserror.occurred_atin a structured message error
now() is Stable with an ExecutionLocal dependency scope. It is therefore never folded, which
requires a constant scope, but it is shared and computed once per execution. A paced domain’s time
before the epoch reaches uuid_v7() as it is, and each row that evaluates the call fails rather
than encoding a different instant. No VM or datetime code calls SystemTime or Instant.
Processor and materialized relay tasks retain the installed domain clock that supplies each unit’s
snapshot. Filtered subscriptions retain its lifecycle owner so a subscription opened before
START DOMAIN follows the subsequently installed generation. Each subscription generation also
retains the node’s executor and supplies it directly to predicate evaluation, preserving bounded
admission for extension calls. Batch evaluation reads these publications directly; it does not
discover its clock through the runtime registry.
Deterministic And Volatile Functions
Only uuid_v4 and uuid_v7 are Volatile. A volatile builtin:
- is never shared by the compiler
- is never computed once for all rows
- is never removed as dead code
Window aggregates and header reads are Stable, because they answer from their context. A UDF is
deterministic unless it is declared VOLATILE. Only a volatile UDF can call now, rand_f64, or
uuid_v4, and UDF code never runs during constant folding.
Extension Boundaries
The FunctionInjector trait is the VM’s only extension point. The compiler emits an Inject
instruction for three kinds of call: read_header and read_headers, window aggregates, and UDFs.
At run time the VM looks for an injector in two places: the injector in the execution context first, then the one compiled into the program. Every answer is validated. A result of the wrong type or row count, or a side error that names a row outside the call, is a batch error.
The injectors:
- Header reads.
IngestHeaderFunctionInjectoranswers header reads from the ingest metadata of one execution. - Window aggregates.
WindowAggregateResultsanswers window aggregates from the accumulators at emission. - UDFs.
nervix-roto’sUdfExecutoranswers UDF calls:- It receives one call per batch or selection, and masks rows whose required arguments are null or already failed.
- Every execution that calls a UDF runs on the node’s extension workers, and a panic there is a batch error.
- It reports per-row errors as side errors, and fails the batch for a trap, a wrong type or row count, an unexplained null, or a call that returns after its 5-second watchdog.
- The watchdog is checked only after the call returns. It cannot reclaim a worker from native code that never returns, as User-Defined Functions explains.
WASM processors are not VM functions. nervix-wasm does not depend on the VM. A WASM processor’s
routes are ordinary set-only VM programs over the guest’s output, and the host gathers guest
columns with Arrow identity, slice, take, and concatenate. Guest execution, isolation, and state
belong to WASM Processor Guests and
WASM State And Recovery.
FunctionInjector and retained window accumulator traits declare recurring compiler contracts,
so their implementations and dynamic callers retain the VM callback frequency across crate
metadata. UDF result builders document the retained column and admitted execution bound; external
Arrow, formatting and operator callback effects are declared at their owning callable. Paired
Rust API doctests verify that injection accepts the domain timestamp with its selected-row error
mask and rejects an unrelated scalar time. The annotations do not replace executor admission,
selected-row validation or the concurrency checks.
Data-Plane Concurrency owns compiler contract syntax
and diagnostics.
Window Aggregates And Sketches
Planning
window.rs lowers a window route’s assignments:
- What is rejected. It rejects
INHERITandINVOKE, and targets other than a bare field oroutput.<field>.inputmay be read only inside an aggregate argument, and an aggregate argument may read onlyinput. Nested aggregates, and configuration arguments that are not constants, are rejected too. - Demands. It rewrites each aggregate call into a demand. A demand names its function, its
shared storage kind, and its per-row arguments. Two demands merge when their storage, arguments,
histogram configuration, and sketch configuration all match. That merging is what lets
AVGandSTDDEV_POPof one argument share onemomentsstructure.
window/route.rs then compiles two kinds of program for each route:
- The argument program. One program evaluates every demand’s per-row arguments over an input
batch, writing internal
window_argument.demand_<id>_<position>fields. - The output programs. For each assignment, a one-row output program turns aggregate results
into the output field. Each aggregate call becomes an
Injectinstruction. Aggregate calls may therefore sit inside arithmetic, a conditional, or a list, because they compile like any other scalar operand.
Registry validation compiles the same route with the same compiler when the processor is applied.
It also rejects BYTES arguments and checks the sketch budget.
Branch Ownership And Accumulation
A window and every accumulator in it are fields of the live window of exactly one concrete branch.
Only that branch’s task changes them, through &mut self, without a lock. What leaves the task is
an immutable publication. See Window and WASM
state.
For each batch, the task works through these steps:
- It evaluates the argument program once. A whole-batch failure fails every message in the batch.
- It refuses a row whose argument failed, or whose float argument is not finite where the structure requires finite values. Each floating argument column supplies a non-finite bitmap.
- It checks the state budget, then admits the rest in runs of consecutive rows. A run ends where the window’s width fills.
- For each run, every structure reads one typed value slice and validity bitmap from the shared argument columns. Counts use bitmap popcounts; integer sums use exact high and low lane halves; float sums use per-lane TwoSum; moments use a compensated mean pass and a centered second pass, normalizing by the largest contributing magnitude when summing finite inputs overflows before division, then one Chan merge into the branch’s aggregate. Histogram indexes are computed for the run and scattered into bucket counts. Each retained row holds its sequence, its timestamp, and a row view of the Arc’d input and argument batches. The normalized mean is clamped to its operand range before rescaling; a constant finite run keeps its exact mean and zero M2 and cross moment even when its sum overflows.
- It emits while the width is met, and then steps the window. Stepped rows are acknowledged.
Tumbling and sliding windows use this same path, with different WIDTH and STEP.
| Structure | Functions | Algorithm | Retraction when the window steps |
|---|---|---|---|
counter | COUNT | Exact 64-bit count | Subtract |
truth_counter | COUNT_IF, BOOL_AND, BOOL_OR | Exact true and false counts | Subtract |
sum over integers | SUM | Exact 128-bit sum from high and low 32-bit SIMD halves, checked against the argument type at emission | Subtract the same run kernel’s result |
sum over floats | SUM | Per-lane Knuth two-sum in a two-stack window; F32 accumulates in F64 | Refold typed surviving runs, never subtract |
moments | AVG, variances, standard deviations | Two-pass centered count, mean, and M2 with one Chan merge per run, in a two-stack window | Refold typed surviving runs |
co_moments | Covariances, CORR | Two-pass centered co-moments with one Chan merge per run; CORR clamped to [-1, 1] | Refold typed surviving runs |
extremes, sequence, arg_extremes | MIN/MAX, FIRST/LAST, ARG_MIN/ARG_MAX | Monotonic candidate deque ordered by value, arrival, or key; ties keep the earliest row | Drop front candidates older than the first survivor |
linear_histogram | PERCENTILE_LINEAR_HISTOGRAM | Fixed-range buckets, answered with a bucket midpoint | Immediately, or after the configured delay on the domain clock |
hll | APPROX_COUNT_DISTINCT | HyperLogLog over the first 8 bytes of a BLAKE3 hash of a type-tagged key, with linear counting at small cardinalities | Rebuilt from survivors |
quantile_sketch | APPROX_QUANTILE | t-digest capped at its capacity, merging the least-cost adjacent centroids; exact while it holds every value | Rebuilt from survivors |
frequent_items | APPROX_TOP_K | Misra-Gries candidates, ordered by count and then key bytes | Rebuilt from survivors |
The two-stack window keeps its floating-point statistics exact under sliding:
- Front. A stack whose entries aggregate the oldest retained rows. Its top entry covers the oldest front row together with every newer front row, so dropping that row pops one entry.
- Back. One aggregate of every retained row after the front, extended as rows are admitted.
- Refold. A retraction that reaches past the front groups survivors by typed argument run and folds each run from newest to oldest into a fresh front.
Nothing is ever subtracted, so a value that left the window leaves no rounding behind, at amortized constant cost per row.
Run boundaries and SIMD lane order determine the association of floating-point additions and Chan merges. Their results are associative up to floating-point rounding and can differ slightly when batching changes. Integer sums and counts remain bit-identical regardless of run boundaries.
Sketch Panes, Merge, And Expiry
A sketch window keeps one sketch per pane. A pane is a span of the greatest common divisor of
WIDTH and STEP, aligned to the Unix epoch, and a window spans at most WIDTH / pane + 2 panes.
At emission the window merges every live pane into a fresh sketch:
- HyperLogLog takes the register-wise maximum.
- The t-digest inserts every centroid and compresses.
- Misra-Gries adds weighted counts.
Each top-k value is sliced from the earliest retained row with that key.
The first row admitted into a pane creates its sketch, found or inserted by one search of the
window’s ordered pane map; every later row of the pane updates that sketch in place. A row whose
argument is absent admits nothing, so a pane holding only such rows keeps no sketch.
just bench-window-sketches measures admission at one, 16 and 1,024 rows per pane for each
sketch.
A sketch cannot subtract a row. When a window steps, every pane is cleared, and every surviving row is admitted again. This rebuilds the whole window rather than one pane, so a row that left the window never contributes to a later estimate. Mergeability is what makes the pane merge cheap; it never implies that a sketch can retract a row.
Bounded State
A sketch’s reservation per pane is fixed by its configuration:
2^precision + 128bytes for HyperLogLog32 * capacity + 128bytes for the t-digest160 * capacity + 128bytes for Misra-Gries
When the processor is applied, processor/sketch.rs requires these declarations:
MAX STATE SIZE- duration
WIDTHandSTEP - for a branched window, a branch that declares
MAX INSTANCES
It rejects a route when 1024 + reservations * (maximum panes + 1) bytes exceed that size. The
extra pane is the merged sketch built at emission.
At run time, check_admission charges a branch before any state changes. The charge covers:
- a fixed base
- per-row bookkeeping
- each shared input and argument batch, charged once at the greater of its actual and its estimated size
- the branch key
- every sketch in every active pane, plus the merged sketch
A run that does not fit is retried one row at a time. A single row that still does not fit is
refused through the node’s message-error policy, which logs it and leaves it unacknowledged. More
active panes than the maximum is refused the same way. Restoring a snapshot passes the same check.
Only a window that declares MAX STATE SIZE is charged, and every sketch window must declare it.
Histogram buckets are not charged, as the reference
states.
Publication, Handoff, And Recovery
A branch task marks its window dirty after every admission and every emission that changes it. It
publishes an immutable generation through an ArcSwap on these occasions:
- at each replication poll, while the window is dirty
- before an ownership-handoff checkpoint
- when the branch stops
The snapshot task persists publications on its own interval. See Data Plane for the persistence guarantees.
A published window carries its retained rows, each with its sequence, timestamp, key, and the Arrow row views of its input and argument columns. It does not carry the accumulators themselves. The snapshot codec seals those views as bounded Arrow sections on the bulk executor. Only the histogram’s delayed removals ride beside them in a typed section, because the retained rows cannot reproduce them.
Each delayed removal archives its bucket index as a fixed-width 64-bit count, with checked native
decoding. The current snapshot frame begins with NVXWIN64, and window state uses runtime-state
kind tag 8. These identify the current stored and transferred shape before decoding; an
unrecognized frame fails validation, and stored state outside the current namespace must be
recreated. The count contract is defined in Archived Counts.
A snapshot from an earlier incarnation of the branch restores an empty window and marks it for publication, so a late checkpoint of a previous lifetime cannot restore its panes. Otherwise, restoring checks that the snapshot’s sequences are consecutive, that its input schema matches, and that every row belongs to this branch. It then re-admits every row in order. That rebuilds every exact structure and every sketch pane from the rows themselves, then reapplies any delayed removals.
The argument columns of a snapshot have one schema, which the window compiler derives from the route’s aggregate demands: one nullable column per argument of every demand, in demand order. Native snapshots and backup archives both use it, and a reader opens a sealed snapshot only against that schema and the window’s input relay schema.
A backup archives the same publication in a public form. Each window
branch becomes an rkyv descriptor holding the retained rows’ admission sequences and watermarks,
the branch incarnation, the window model digest and every histogram’s delayed removals, together
with bounded groups of the retained input rows and their argument columns as Arrow IPC. A restore
converts those groups back into a native snapshot, which the branch task restores as it restores
any other: every row is re-admitted in order, and the histograms take their delayed removals back.
The window model digest is the BLAKE3 hash of the window processor’s canonical NSPL, so a window
whose WIDTH, STEP, state limit or aggregates changed starts empty instead.
How each ending treats the window:
| Ending | Effect on the window |
|---|---|
| Handoff or detach | Flushes a window whose width is met, then publishes what remains. |
| Eviction | Resets the window first, so its final publication is empty and a later branch with the same key starts fresh. |
| Quiesced backup cut | Publishes any change since the window’s last publication when the cut’s lifecycle checkpoint asks. The retained rows’ acknowledgement shares stay parked, so they do not hold the cut open; the cut captures them as window state. |
| A task aborted after its shutdown grace period | Loses the changes made after its last publication. |
Snapshot transfer between nodes follows the bulk transfer rules of the Cluster Interconnect. Drain and forced flush at shutdown follow Shutdown And Recovery.
A result that does not fit its type fails the window when it emits:
- a
SUMoutside its argument type - a statistic that overflows
F64 - a sketch estimate outside
I64
That failure fails every retained row’s acknowledgement and clears the window. The routes the window emitted before the failing one keep their output.
Adding A Function
Treat this as the checklist for a new builtin, or for a new family.
-
Decide the contract first. Write down these, and check that the function respects the reference’s Function Properties:
- exact argument and result types, and which results are optional
- null handling, and every error kind and message
- volatility and dependency scope
- sensitivity and eligible contexts
A new type shape, such as a general decimal or a map payload, is a product change that needs its own contract, not a scalar function name.
-
Register it. Add the following:
- a
FunctionNamevariant with itsparseandas_strarms, plus aliases only when they are part of the contract - a
BuiltinLoweringvariant - its arms in
builtin_descriptor,builtin_semantics_for_lowering,builtin_output_type, andbuiltin_arm_execution - any
expr_may_be_nullexception
A datetime function resolves its literal arguments at lowering instead.
- a
-
Decide folding. Either add a folding arm that applies a shared catalog contract, or list the function in
fold_builtin_call’s refusals. Never fold anything that can fail, is volatile, or reads execution-local state. -
Decide
can_errorhonestly. A builtin that pushes a row error must declarecan_error. Otherwise the compiler shares it and gives it no selection, so the errors of unselectedCASEarms would leak into error handling. -
Choose the kernel.
- Reuse an Arrow kernel when its semantics are exactly NSPL’s, including float ordering, overflow, and null rules.
- Otherwise write one pass over value buffers with the checked lanes, pushing failures from a bitmap.
- Keep a dependency with runtime SIMD dispatch only when it preserves the semantic contract.
- Irregular work stays a batch API with per-row substeps.
Build text or bytes whose length an argument chooses through the bounded column builder. Read integer counts through
count.rs. Take scalars asOperands instead of expanding them. -
Choose arm execution. Use
SelectedRowsfor work costing well above a few nanoseconds per row, and for any text sized by an argument. UseWholeBatchfor vectorized kernels. -
Keep errors typed. Add a
SideErrorReasonvariant with typed fields and itsErrorCode. Format nothing per row, and never include an argument’s value in a reason. A failure of the batch as a whole extendsRuntimeErrorand propagates througherror-stack. -
Preserve the invariants.
- no implicit conversion
- sensitivity from every operand
- no clock read
- no row-oriented carrier
- no lock per row
- no prepared object outside the compiled program
-
Test it.
- kernel and compiler unit tests next to the code, through
just test-vm, covering nulls, NaN, overflow, sliced arrays, scalar operands, and conditional arms - a public Cucumber scenario outline on one and three nodes that proves values and failures
through a graph and a session subscription, run with
just test-scenarios --input <feature> - a row in the qualification ledger
- for a kernel with an independent reference, a registered Bolero property over its column
kernel, as
round(value, digits)has invm-decimal-rounding-floatsandvm-decimal-rounding-integers; see Property Testing And Fuzzing - window structures also need interleaved branches, eviction, and snapshot restore
- kernel and compiler unit tests next to the code, through
-
Measure it. Add a Criterion case to
crates/nervix-vm/benches/vm.rsor its workload shapes, and run it withjust bench-vm. Usejust bench-vm-allocfor allocation evidence, and the serializedjust benchmark-abfor any end-to-end claim.- Claim vectorization only after inspecting the generated instructions for the target.
- Claim a speedup only from a same-host A/B.
- Record the result beside the existing report.
-
Document it.
- Add its contract to Expression Functions, including a catalog row.
- Update the NSPL skill only where its routing or checks change, then regenerate the book.
- Update this chapter when the function changes a mechanism rather than adding an instance of one.
Guarantees And Limits
Guarantees:
- An expression’s value never depends on the other rows in its batch, on the batch’s size, or on whether a kernel was vectorized.
- A folded, shared, or scalar computation produces the value that per-row evaluation would.
- Only a conditional expression evaluates an operand for some rows and not others. An unselected arm never reports an error.
- Every execution reads one domain timestamp, supplied by its caller.
- Window state belongs to one branch task. It is published only as immutable generations, and after a handoff or recovery it is rebuilt from its retained rows.
Limits:
- Column limits: 2,147,483,647 bytes of text or bytes in one column; 1,024 rows before execution moves to the node’s data workers.
- Regular expressions: 10 MiB compiled and a 2 MiB search cache per pattern, and 64 field-supplied patterns cached per call.
- Pattern sets: 128 patterns and 64 KiB per constant
contains_anylist, and 64 per-row pattern sets per batch. - Text functions: 4 KiB
LIKEpatterns and 65,536splitparts. - JSON: 16 MiB, 128-level documents, and 64-step paths.
- Datetimes: 256-byte formatted values.
- Sketches: precision 4–16, capacity 32–4096, and
kat most the capacity.
The complete user-facing limits are in Limits.
Current boundaries:
- Validation compiles to reject invalid statements, while installation compiles the programs selected by the typed revision against local schemas and capabilities.
- Branched processor routes compile before publication and share their prepared patterns and sets across concrete branch tasks of the same installed node revision.
- A message-error route binds once per installed revision; each failed record executes that bound program with its captured inputs.
- The text functions listed under Allocation And Result Bounds build their columns without the size check.
- A statement that makes a batch’s program slow is bounded only by the input and the limits above. The VM has no instruction budget and no preemption.