Files
alktype/poc/readplan/FINDINGS.md
glm-5.2 f7c71da9e5 POC: ReadPlan shape derisking for ADR-011
Standalone workspace member at poc/readplan/ that depends on alktype
via path and exercises the ReadPlan/CompositePlan/ReadKind/
DiscriminatorPlan shape from ADR-011 against every BastType arm in the
current read loop.

Result: 28/30 tests pass. 2 deliberately ignored, both with documented
findings:

- Field-name-discriminator union read shape is a TODO (compile shape
  is correct; the read-side stub surfaces the work for implementation
  step 1 rather than hiding it).
- Existing SequentialReader returns element_stride=0 for fixed-size
  struct arrays (pre-existing limitation at sequential_reader.rs:567,
  not a plan-shape gap; the POC plan correctly computes the stride).

Coverage confirms every BastType arm compiles to the expected
ReadKind/CompositePlan. Equivalence tests confirm plan-driven read
produces identical (FieldValue, position) to the existing reader for
all covered cases. ReadPlan: Send + Sync confirmed.

Green light for ADR-011 implementation. See poc/readplan/FINDINGS.md
for the full writeup.

This branch is a derisking POC, not meant to merge to main (mirrors
the bast-validator-poc branch pattern). Cargo.toml gains a workspace
section that includes poc/readplan; that section is POC-only and would
be dropped if these files ever merged to main.
2026-08-18 09:37:33 +00:00

6.9 KiB

ReadPlan POC — findings

Branch: readplan-poc ADR: ADR-011 Status: ADR-011 accepted based on this POC. 28/30 tests pass; 2 deliberately ignored with documented findings.

Objective

Before accepting ADR-011 and starting implementation, derisk the ReadPlan/CompositePlan/ReadKind/DiscriminatorPlan shape by:

  1. Walking every BastType arm in the current read loop (sequential_reader.rs:303-381) and confirming ReadPlan::compile produces a plan that covers it.
  2. Driving both the existing SequentialReader and a plan-driven reader over the same buffers and asserting identical (FieldValue, position) results.

If both pass, the shape is confirmed and implementation can proceed by replacing the BastDoc walk in sequential_reader.rs and materialize.rs with a ReadPlan walk.

What the POC is

  • poc/readplan/ — a standalone workspace member (readplan-poc crate) that depends on alktype via path dep.
  • src/lib.rs — ReadPlan/FieldPlan/CompositePlan/ReadKind/ DiscriminatorPlan matching ADR-011's shape, ReadPlan::compile walking BastDoc once, and plan_read_field_at/plan_walk_struct_size mirroring the existing read loop arm-by-arm.
  • tests/coverage.rs — cov_* coverage tests (one per BastType arm)
    • eq_* equivalence tests (plan vs existing SequentialReader).

The POC is deliberately not production code: no doc comments on the plan types beyond the module header, no clippy-cleanliness gate, no bench. It exists to answer two questions and stop.

Result

28/30 tests pass. 2 ignored, both with documented findings.

Coverage — all BastType arms confirmed

Every BastType arm in the current read loop compiles to the expected ReadKind/CompositePlan shape:

BastType arm ReadKind CompositePlan cov test
Primitive (Int8..Float64, Bool, String, Bytes) Primitive(k) None cov_all_fixed_primitives, cov_string_and_bytes
Enum (via $ref) Enum None cov_enum_ref
Struct (inline) Struct Struct(ReadPlan) cov_struct_inline
Struct (via $ref) Struct Struct(ReadPlan) cov_struct_ref
Union (byte disc) Union Union { disc: Byte, variants } cov_union_byte_disc
Union (field disc) Union Union { disc: Field, variants } cov_union_field_disc
Array (fixed elem) Array Array { element, count, stride } cov_array_fixed_element
Array (variable elem, stride=0) Array Array { element, count, stride: 0 } cov_array_variable_element_stride_zero
Array ($ref elem) Array Array { element: Struct, count, stride } cov_array_ref_element
Record Record Record { value: Struct-wrapped leaf } cov_record

Field-level annotations (endian override, encoding, maxLength) are preserved by compile_field — covered by cov_field_level_endian_override and cov_maxlength_and_encoding_preserved.

Equivalence — plan == existing reader for every covered arm

eq_* tests drive both readers over the same buffer and assert identical (FieldValue, position) for every field. All pass except the two ignored ones below.

Send + Sync

ReadPlan: Send + Sync holds for the planned shape — confirmed by readplan_is_send_sync. Falls out naturally from the plan being immutable owned data with no lifetimes and no interior mutability.

Findings the POC surfaced

Finding 1 — Field-name-discriminator union read shape needs work

Status: POC TODO (ignored test eq_union_field_discriminator_todo).

compile_union correctly records DiscriminatorPlan::Field { name, field_index } and the variant plans, but plan_read_union's Field arm is a stub that returns an error. The field-name case is structurally different from the byte-offset case: the union declares its own fields array (the discriminator field + any shared fields), and the variant struct is laid out after those shared fields. The plan needs a sub-struct for the union's declared fields, separate from the variant plans.

This is not a plan-shape gap — DiscriminatorPlan::Field and the variant ReadPlans compile correctly. It's a read-shape TODO that the implementation step 1 must wire. The POC stubs it to surface the work explicitly rather than hide it.

Finding 2 — Existing reader returns stride=0 for fixed-size struct arrays

Status: POC FINDING (ignored test eq_array_ref_element).

The existing SequentialReader returns element_stride: 0 for an array of $ref-to-fixed-struct elements, even when the struct is fixed-size (e.g. Point { x: u16, y: u16 } is 4 bytes). See sequential_reader.rs:567:

let element_stride = if elem_kind.is_fixed_size() {
    elem_kind.type_size().unwrap_or(0)
} else {
    0
};

elem_kind is resolved_elem.alk_kind(), which returns AlkTypeKind::Struct for a $ref to a struct. Struct.is_fixed_size() is false, so the existing code returns 0. The POC's fixed_struct_size helper correctly computes 4.

The implementation step must decide:

  • (a) preserve the existing stride=0 behavior for back-compat (consumer walks sequentially), or
  • (b) fix the existing reader to return the true fixed-struct stride and let consumers index directly.

Either way, the ReadPlan shape is correct — this is a pre-existing reader limitation, not a plan-shape gap. Recording it so the implementation step makes a deliberate choice rather than inheriting the old behavior by accident.

What this POC does not cover (out of scope, by design)

  • Performance. No bench. The bench that matters lives in alktty's wire_vs_bast.rs; the implementation commit re-runs it. The POC only proves correctness/coverage.
  • Aligned mode, validation, write side. ADR-011 scopes these out.
  • materialize_packed equivalence. The POC covers SequentialReader equivalence; materialize_packed uses the same BastType arms via materialize_typeref_packed and will be covered by the implementation step's existing validate_bytes tests.
  • Nested unions (union variant is itself a union). The POC's compile_union rejects this with a clear error. The existing reader supports it via resolve_and_walk_variant's BastDefKind::Union arm; if a real schema needs it, the implementation step adds a VariantKind::Union read path. Not blocking — no current schema exercises it.

Conclusion

ADR-011's ReadPlan shape covers every BastType arm and produces identical results to the existing reader for all covered cases. The two ignored tests document deliberate scope boundaries (field-disc union read shape) and a pre-existing reader limitation (struct-array stride), neither of which is a plan-shape gap.

Green light for ADR-011 implementation. The implementation step 1 (ReadPlan type + compile) can proceed, with the field-disc union read shape and the struct-array stride decision as explicit sub-tasks.