This issue tracks the private machinery that executes a RowFn over Vortex arrays.
Parent Epic: #9128
Related API tracking issue: #9129
Complete. The private executor is implemented on develop. Further API work and consumer migrations remain in #9129 and #9128.
Design
A RowFn provides a typed row kernel. The batch executor adds the columnar behavior that every strict row function needs:
- Validate every input length. Then plan the output dtype and nullable-row policy from the concrete
dispatch.
- Conjoin the input validities and short-circuit all-invalid or null-constant batches.
- Evaluate all-constant calls once and decode each batch constant into its constant representation.
- Select dense, dense-with-retry, or valid-only execution.
- Try direct valid-row execution before the filter-and-scatter fallback.
- Validate the output length, dtype, and valid-row values. Then apply strict validity.
This machinery is private. A function author selects typed elements and an output capability. The executor owns the batch strategy.
Nullable-row policies
Planning selects one of three policies for each concrete dispatch:
Dense evaluates every row and masks the output. It requires dense-safe, decode-infallible inputs and an infallible row result.
DenseWithRetry evaluates deferred, dense-safe, decode-infallible computations over every row. A partially valid batch retries only valid rows after reduced failure evidence reports an error.
ValidOnly does not evaluate the row closure for an invalid row. It handles other input and result combinations.
Decode infallibility currently participates in policy selection. Each kernel invocation decodes its inputs before preparation and row computation. A decode error returns immediately. Input validity does not defer or suppress it.
Executor results
Executors return VortexResult<ArrayRef> directly. Decode errors and immediate row errors return directly. Deferred dense execution can instead return reduced row-failure evidence, which batch execution resolves against input validity before deciding whether to preserve the error, suppress an all-null result, or retry valid rows.
An earlier design threaded a three-outcome RowExecution type through every executor. #9496 removed that shared result type. DenseWithRetry now exists only around deferred dense attempts, while ordinary dense and valid-only executors continue to return arrays directly.
Deferred failures
visit_deferred returns an owned output and a small FailureEvidence value for each row. The executor OR-reduces those values in a loop-local. It constructs one rich error after the loop.
The concrete failure evidence must not be wider than the output value. A wider reduction lowers the vector width and can make checked arithmetic much slower. Keeping the accumulator out of the output sink also avoids a loop-carried memory dependency.
Sink visits can report these result forms:
() for an infallible write into initialized storage.
InitializedElement for an infallible write into uninitialized storage.
VortexResult<()> for an immediate failure with initialized storage.
VortexResult<InitializedElement> for an immediate failure with uninitialized storage.
The result's WriteToken must match OutputSink::WriteToken. This constraint keeps the visitor methods safe. It places the per-row unsafe operation inside the uninitialized-output closure. Deferred evidence belongs to the owned visit_deferred forms, not sink execution.
Integer division returns VortexResult<InitializedElement> with UninitElementSink. Division is already scalar and expensive. An immediate check can stop at the first failure. Uninitialized dense output avoids filling every slot before the row loop.
Skip-invalid and filter-and-scatter execution
For a partially valid batch under ValidOnly, the executor can compute only valid row indices in the original inputs. This path requires two contracts:
- Every
InputElement must provide a null-tolerant decode for the concrete array.
OutputSink::skipped_rows_initializer must return an initializer for legal placeholders.
The executor masks those placeholders before it returns the output. If either contract declines, the executor filters every input to the valid rows. It runs the dense kernel and scatters the result into a full-length nullable array.
Owned output visits initialize skipped slots with Default. The executor masks those placeholders before returning the output. If null-tolerant decoding declines, the visitor uses filter-and-scatter. UninitElementSink supports skipped slots directly, which keeps nullable integer division on the original inputs.
ValidOnly first tries skip-invalid execution. If that attempt declines, it filters and scatters. There is no survivor threshold or FILTERED_DECODE_COST. #9521 adds this generic fallback.
Constants
Constant decoding and prepared computation are separate. InputElement::decode_constant returns a constant representation without constructing a one-row decoded column. A prepared visitor can derive shared state from that value once per batch.
The no-constant fast path borrows ElementTuple::Views. The tuple guard checks every decoded view length before unchecked row access. decoded_lens_match validates each varying column when the batch also contains constants.
Migration benchmark gate
Use pinned, alternating local x86 measurements and generated-code inspection before replacing a hand-written microkernel. CodSpeed CPU simulation measures a different cost model and does not replace native evidence. Keep a columnar fallback when RowFn produces slower native code, as the primitive comparison path does.
The executor has focused output, validity, constant, and deferred-failure benchmarks. #9517, #9620, #9626, and #9629 record the generated-code evidence for the landed paths. Consumer migrations remain part of #9128.
Steps
Decisions
Follow-ups
Implementation history
This issue tracks the private machinery that executes a
RowFnover Vortex arrays.Parent Epic: #9128
Related API tracking issue: #9129
Complete. The private executor is implemented on
develop. Further API work and consumer migrations remain in #9129 and #9128.Design
A
RowFnprovides a typed row kernel. The batch executor adds the columnar behavior that every strict row function needs:dispatch.This machinery is private. A function author selects typed elements and an output capability. The executor owns the batch strategy.
Nullable-row policies
Planning selects one of three policies for each concrete dispatch:
Denseevaluates every row and masks the output. It requires dense-safe, decode-infallible inputs and an infallible row result.DenseWithRetryevaluates deferred, dense-safe, decode-infallible computations over every row. A partially valid batch retries only valid rows after reduced failure evidence reports an error.ValidOnlydoes not evaluate the row closure for an invalid row. It handles other input and result combinations.Decode infallibility currently participates in policy selection. Each kernel invocation decodes its inputs before preparation and row computation. A decode error returns immediately. Input validity does not defer or suppress it.
Executor results
Executors return
VortexResult<ArrayRef>directly. Decode errors and immediate row errors return directly. Deferred dense execution can instead return reduced row-failure evidence, which batch execution resolves against input validity before deciding whether to preserve the error, suppress an all-null result, or retry valid rows.An earlier design threaded a three-outcome
RowExecutiontype through every executor. #9496 removed that shared result type.DenseWithRetrynow exists only around deferred dense attempts, while ordinary dense and valid-only executors continue to return arrays directly.Deferred failures
visit_deferredreturns an owned output and a smallFailureEvidencevalue for each row. The executor OR-reduces those values in a loop-local. It constructs one rich error after the loop.The concrete failure evidence must not be wider than the output value. A wider reduction lowers the vector width and can make checked arithmetic much slower. Keeping the accumulator out of the output sink also avoids a loop-carried memory dependency.
Sink visits can report these result forms:
()for an infallible write into initialized storage.InitializedElementfor an infallible write into uninitialized storage.VortexResult<()>for an immediate failure with initialized storage.VortexResult<InitializedElement>for an immediate failure with uninitialized storage.The result's
WriteTokenmust matchOutputSink::WriteToken. This constraint keeps the visitor methods safe. It places the per-row unsafe operation inside the uninitialized-output closure. Deferred evidence belongs to the ownedvisit_deferredforms, not sink execution.Integer division returns
VortexResult<InitializedElement>withUninitElementSink. Division is already scalar and expensive. An immediate check can stop at the first failure. Uninitialized dense output avoids filling every slot before the row loop.Skip-invalid and filter-and-scatter execution
For a partially valid batch under
ValidOnly, the executor can compute only valid row indices in the original inputs. This path requires two contracts:InputElementmust provide a null-tolerant decode for the concrete array.OutputSink::skipped_rows_initializermust return an initializer for legal placeholders.The executor masks those placeholders before it returns the output. If either contract declines, the executor filters every input to the valid rows. It runs the dense kernel and scatters the result into a full-length nullable array.
Owned output visits initialize skipped slots with
Default. The executor masks those placeholders before returning the output. If null-tolerant decoding declines, the visitor uses filter-and-scatter.UninitElementSinksupports skipped slots directly, which keeps nullable integer division on the original inputs.ValidOnlyfirst tries skip-invalid execution. If that attempt declines, it filters and scatters. There is no survivor threshold orFILTERED_DECODE_COST. #9521 adds this generic fallback.Constants
Constant decoding and prepared computation are separate.
InputElement::decode_constantreturns a constant representation without constructing a one-row decoded column. A prepared visitor can derive shared state from that value once per batch.The no-constant fast path borrows
ElementTuple::Views. The tuple guard checks every decoded view length before unchecked row access.decoded_lens_matchvalidates each varying column when the batch also contains constants.Migration benchmark gate
Use pinned, alternating local x86 measurements and generated-code inspection before replacing a hand-written microkernel. CodSpeed CPU simulation measures a different cost model and does not replace native evidence. Keep a columnar fallback when RowFn produces slower native code, as the primitive comparison path does.
The executor has focused output, validity, constant, and deferred-failure benchmarks. #9517, #9620, #9626, and #9629 record the generated-code evidence for the landed paths. Consumer migrations remain part of #9128.
Steps
RowFn.InputElementandOutputSinkimplementations invortex-spatial.Decisions
Defaultplaceholders for owned valid-row execution. Mask them before returning the output.Follow-ups
Implementation history
RowFnandRowVisitor#9386 defines the author-facing contracts used by the executor.ScalarFnVTableintegration.RowFnexecution contracts #9496 removes the sharedRowExecutionresult. ReplaceScalarFnVTable::is_falliblewithis_infallible#9511 establishes the positiveScalarFnVTable::is_infallibleterminology.RowFnbatch execution #9450 adds constant handling, dense execution, sink-based valid-row execution, and output validation.Defaultplaceholders (first landed on the stack as Execute owned RowFn outputs over valid rows #9468).DenseWithRetryand lands primitive arithmetic as the first production user.FixedSizeListSink.PolygonSink.