Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Checkpoint 3: Build Shared Typed Evaluation

You now have owned nullable arrays and borrowed Array, Constant, Null, and Indexed views. This checkpoint turns them into one complete evaluation path: check the batch’s arity, types, and row counts, convert its inputs to typed views, read rows through ColumnView::get, call one scalar function when its inputs are present, and append a newly owned output array. Later optimizations will still fall back to this path.

Begin from your completed Checkpoint 2 workspace. Copy the cumulative tests, then run only the new Chapter 3 cases:

cargo x copy-test --chapter 3
cargo test -p type-exercise-starter-supplied-tests chapter_3 --locked

The focused test should fail because the shared evaluators and the three numeric facade functions do not exist yet. The inherited Chapter 1 and 2 APIs should still compile; keep the copied tests unchanged.

Validate before traversing rows

Enable the existing expression module and export it from type-exercise-starter/core/src/lib.rs. In core/src/expression.rs, implement:

pub fn validate_expression_inputs(
    inputs: &[ColumnViewImpl<'_>],
    expected_types: &[PhysicalType],
) -> anyhow::Result<usize>

Reject an arity mismatch first. Then compare each input’s physical type with its expected type and check that every input has the same length as the first. Return that common length; an empty input list has length zero.

This checks the whole batch before traversal; it does not construct typed views. Each later ColumnView::<S>::try_from still checks the physical type while constructing a typed Array, Constant, or Indexed view, because that conversion is a fallible API in its own right. The evaluator therefore compares physical type twice. For unary evaluation, the validator’s type check adds no independent safety beyond the conversion; the shared preflight keeps arity, type, and row-count checks together for all three evaluator shapes.

Lift scalar functions through typed views

Implement three public evaluators in the same core module:

evaluate_unary::<I, O, _>(input, scalar_function)
evaluate_binary::<L, R, O, _>(left, right, scalar_function)
evaluate_ternary::<A, B, C, O, _>(first, second, third, scalar_function)

Each evaluator follows one sequence:

  1. call validate_expression_inputs with the scalar families’ PHYSICAL_TYPE values;
  2. convert every erased input to ColumnView<S> once;
  3. allocate <O as Scalar>::ArrayType::Builder for the validated row count;
  4. read each row with typed get and call the scalar function only when every input is non-null;
  5. append the resulting value or null, finish the builder, and erase the owned array.

After validation and conversion succeed, the row loop has typed inputs of equal length and can read each input at every output row.

Use Option::map for unary input and Option::zip for binary and ternary inputs. That makes strict null propagation part of the shared traversal: a null input produces a null output without calling the scalar function.

Let ColumnView::get hide the Array, Constant, and Indexed variants. It is the representation-generic path that remains correct when later checkpoints place faster loops in front of it, and Indexed inputs can continue to use it unchanged.

Choose numeric meaning in the facade

Enable numeric in type-exercise-starter/expr/src/lib.rs. Core owns validation, traversal, null propagation, and output construction. The expr facade chooses concrete types and one scalar operation.

Expose these exact functions from expr/src/numeric.rs:

pub fn add_i16_i32(
    left: ColumnViewImpl<'_>,
    right: ColumnViewImpl<'_>,
) -> anyhow::Result<ArrayImpl>

pub fn negate_i32(input: ColumnViewImpl<'_>) -> anyhow::Result<ArrayImpl>

pub fn clamp_i32(
    value: ColumnViewImpl<'_>,
    lower: ColumnViewImpl<'_>,
    upper: ColumnViewImpl<'_>,
) -> anyhow::Result<ArrayImpl>

add_i16_i32 instantiates i16 + i32 -> i32, converting the left scalar with i32::from. negate_i32 uses wrapping negation. clamp_i32 instantiates the ternary evaluator with i32::clamp. Each function delegates the complete batch to one core evaluator. The facade chooses the numeric meaning without owning a row loop or knowing how the columns are represented.

Run the cumulative contract

Run the focused Chapter 3 cases, then every copied test:

cargo test -p type-exercise-starter-supplied-tests chapter_3 --locked
cargo test -p type-exercise-starter-supplied-tests --locked

All copied tests should pass. The new cases cover the public numeric facade, mixed numeric types, Array/Constant/Indexed inputs, strict null propagation, owned output, and arity/type/length validation.

You can also run the completed snapshot independently:

cargo test -p type-exercise-checkpoint-03-supplied-tests --locked
cargo test -p type-exercise-checkpoint-03-expr --lib --locked
cargo check -p type-exercise-checkpoint-03-core --locked

You are done when all three scalar arities share one typed-get path and the facade contains only the concrete numeric choices. The next checkpoint tackles the different publication rule needed by variable-width output.