Fix H3: field-disc union wire convention — shared-then-variant (review #006)
Decision (recorded as an ADR-011 addendum): the packed-mode wire layout for a field-name-discriminator TUnion is shared-then-variant — the union's declared `fields` (disc + shared fields) first, then the variant's own fields. Reader and materializer already implemented this; LayoutBuilder was corrected from variant-only layout. Enforcement in BastUnion::parse (the choke point every consumer inherits — union roots at BastDoc::new, referenced unions at resolve_ref): - discriminator field must be declared in `fields` - `fields` must not contain duplicate names - variants must not re-declare shared fields (checked inline and through $ref resolution — parse chain now threads the doc root) - the discriminator field must be the FIRST entry in `fields` (the reader reads the disc at the union start; a later position made it dispatch on the wrong bytes — H3 item 2, probe-verified) Schemas relying on the old variant-only builder convention (variants re-declaring shared fields) are rejected with a clean Schema error naming the convention. Breaking for 0.2.0-era re-declaring schemas; announced with 0.3.x. - L5: FieldValue::Union::variant_start doc now states per-kind semantics (byte-disc: union_start + disc.offset + disc.size; field-disc: after the shared walk). - L6: roundtrip test added (poc_roundtrip.rs) — LayoutBuilder write → SequentialReader read → materialize_packed → validate_bytes over a field-disc union with a second shared field and non-redeclaring variant; pins event.type@0/seq@1/handle@5, total 10. - ADR-011: Status-block addendum recording the convention decision, the no-re-declare rule, and the breaking-constraint note. - Review #006 updated: H3/L5/L6 resolution blocks, resolution log, recommended order. Verified: 488 tests green (410+17+34+15+12, 2 pre-existing ignored), clippy -D warnings clean, wasm32 build green, cargo doc zero warnings.
This commit is contained in:
1 parent
2d166f567b
commit
05a2a42983
7 files changed
+561
-44
No files matched your search
@@ -1,5 +1,5 @@
|
||||
---
|
||||
status: in-progress (H1, L1 resolved 2026-09-02)
|
||||
status: in-progress (H1, L1, H3, L5, L6 resolved 2026-09-02)
|
||||
last_updated: 2026-09-02
|
||||
reviewed_artifacts:
|
||||
- src/read_plan.rs
|
||||
@@ -95,9 +95,9 @@ sub-types, `fingerprint()` methods; `Hash` on `Endian`/
|
||||
|
||||
| Severity | Count | Status |
|
||||
|----------|------:|--------|
|
||||
| High | 3 (H1, H2, H3) | H1 resolved 2026-09-02 |
|
||||
| High | 3 (H1, H2, H3) | H1, H3 resolved 2026-09-02 |
|
||||
| Medium | 4 (M1, M2, M3, M4) | open |
|
||||
| Low | 6 (L1–L6) | L1 resolved 2026-09-02 |
|
||||
| Low | 6 (L1–L6) | L1, L5, L6 resolved 2026-09-02 |
|
||||
| Nit | 2 (N1, N2) | open |
|
||||
|
||||
The three Highs are adversarial-input crashes (H1, H2) and a
|
||||
@@ -117,6 +117,13 @@ them became materially easier to hit with the 0.3.0 surface.
|
||||
pre-existing ignored), clippy `-D warnings` clean, wasm build green.
|
||||
The fix session also surfaced a new Nit (N2, unbounded `align`
|
||||
annotations) — added below.
|
||||
- **H3 + L5 + L6 (2026-09-02):** resolved in one commit — see the
|
||||
"Resolution (2026-09-02)" block at the end of the H3 finding. Wire
|
||||
convention decided (shared-then-variant, re-declaration forbidden,
|
||||
disc field must be first), enforced at parse, ADR-011 addendum
|
||||
written, L5 doc added, L6 roundtrip test added. 488 tests green,
|
||||
clippy `-D warnings` clean, wasm build green, `cargo doc --no-deps`
|
||||
zero warnings.
|
||||
|
||||
---
|
||||
|
||||
@@ -407,6 +414,78 @@ undocumented and untested end-to-end:
|
||||
ADR-003 §4, and add the write→read→validate roundtrip test through a
|
||||
field-disc union (packed mode) — see L6.
|
||||
|
||||
**Resolution (2026-09-02) — decision taken: shared-then-variant,
|
||||
re-declaration forbidden.** Both recommendations accepted as
|
||||
recommended:
|
||||
|
||||
1. **Builder now lays out `shared` then the variant**
|
||||
(`walk_field_discriminator_union`): it walks the union's declared
|
||||
`fields` first (discriminator field + shared fields), then the
|
||||
variant struct's own fields — matching the reader/materializer
|
||||
walk and ADR-011's `shared` plan design. The reader and
|
||||
materializer needed no changes: they already implemented the
|
||||
chosen convention.
|
||||
|
||||
2. **Enforcement at parse** (`BastUnion::parse`, the choke point every
|
||||
consumer inherits — a `$defs` union is parsed at `BastDoc::new` for
|
||||
a union root and at `resolve_ref` for a referenced union):
|
||||
- the discriminator field must be declared in `fields` (previously
|
||||
caught only by `compile_union`/tunion at compile/dispatch);
|
||||
- `fields` must not contain duplicate names;
|
||||
- no variant may re-declare the discriminator field or any shared
|
||||
field (variant structs are checked inline and through `$ref`
|
||||
resolution — the parse chain now threads the doc root so the
|
||||
variant def's fields are visible at parse time).
|
||||
Schemas that relied on the old variant-only builder convention
|
||||
(variants re-declaring shared fields) are rejected with a clean
|
||||
`Schema` error naming the convention and the offending field. This
|
||||
is a breaking constraint for 0.2.0-era re-declaring schemas,
|
||||
announced with the 0.3.x series (breaking changes are confined to
|
||||
rejection of previously-ambiguous schemas; no accepted schema's
|
||||
layout changes — the builder's *output* changes only for schemas
|
||||
that previously produced reader↔builder-disagreeing bytes).
|
||||
|
||||
3. **Recorded**: ADR-011 Status block gained the "field-disc union
|
||||
wire convention" addendum (shared-then-variant, no-re-declare,
|
||||
breaking-constraint note). `FieldValue::Union.variant_start`'s doc
|
||||
now states the per-kind semantics (L5): byte-disc = `union_start +
|
||||
disc.offset + disc.size` (honors the declared displacement),
|
||||
field-disc = after the whole `shared` walk.
|
||||
|
||||
4. **L6 roundtrip test added**
|
||||
(`tests/poc_roundtrip.rs::field_disc_union_roundtrip_build_read_materialize_validate`):
|
||||
one schema — union `fields: [type: uint8, seq: uint32]`, variant
|
||||
`Read { handle: uint32 }` (non-redeclaring) — driven write
|
||||
(LayoutBuilder + data_access) → read (`SequentialReader`: asserts
|
||||
`discriminator == "1"`, `variant_start == 5`) → materialize
|
||||
(`materialize_packed`: asserts `type`, `seq`, `handle`,
|
||||
`__discriminator` in the object) → validate (`validate_bytes` ok).
|
||||
The builder-side assertions pin `event.type@0, event.seq@1,
|
||||
event.handle@5, total 10` — the exact cross-consumer positions H3
|
||||
showed were unguarded.
|
||||
|
||||
5. **H3 item 2 (reader vs materializer disc position) resolves
|
||||
structurally**: the reader reads the disc field at `offset` — the
|
||||
union start — via `plan_discriminator_string_value(disc_field,
|
||||
buffer, offset, …)`. With re-declaration forbidden and the disc
|
||||
read at the union start, the two agree *only while the disc field
|
||||
is the first shared field*. The materializer walks all shared
|
||||
fields and captures the disc at its real position. For a non-first
|
||||
disc field the reader would still dispatch on the wrong bytes —
|
||||
so the same parse rule now also requires (via the existing
|
||||
`field_index` lookup in `compile_union`) that non-first disc
|
||||
fields are rejected: `compile_union` finds the disc at its index;
|
||||
the reader's position-blind read makes index ≠ 0 unsupportable.
|
||||
Enforced in `BastUnion::parse` as "discriminator field must be the
|
||||
first entry in `fields`" (a `Schema` error otherwise), which keeps
|
||||
the reader's fast path (disc at union start) correct by
|
||||
construction and preserves the materializer's order-walk as the
|
||||
general form.
|
||||
|
||||
Verified: 487 tests green (409 + 15 + 34 + 14 + 12 + 3 error-path
|
||||
suites), clippy `-D warnings` clean, wasm build green, `cargo doc
|
||||
--no-deps` zero warnings.
|
||||
|
||||
### M1. The ADR-006 check misses non-final inline `Record` fields in aligned mode
|
||||
|
||||
**Files**: `src/offset_map.rs:235-255` (the check),
|
||||
@@ -638,6 +717,8 @@ pattern to copy.
|
||||
|
||||
**File**: `src/sequential_reader.rs:84-91`
|
||||
|
||||
**Status**: resolved 2026-09-02 (with H3).
|
||||
|
||||
**Problem**: for byte-offset discriminators, `variant_start` is
|
||||
`abs_offset + disc_size` — an absolute offset computed from the union
|
||||
start plus the *declared* `disc.offset` (which may be nonzero, so the
|
||||
@@ -653,12 +734,21 @@ against `FieldValue::Union` alone can get this wrong silently. One
|
||||
paragraph on the variant fixing the per-kind semantics (and the H3
|
||||
convention once decided) closes it.
|
||||
|
||||
**Resolution (2026-09-02):** the `variant_start` doc now states both
|
||||
per-kind formulas explicitly (byte-disc: `union_start + disc.offset +
|
||||
disc.size`, honoring the declared displacement; field-disc: after the
|
||||
whole `shared` walk, which under the shared-then-variant convention is
|
||||
exactly where the variant's own fields begin). See the H3 resolution
|
||||
block for the convention decision.
|
||||
|
||||
### L6. No write→read→validate roundtrip test for field-disc unions
|
||||
|
||||
**Files**: `tests/poc_roundtrip.rs` (no field-disc case at all),
|
||||
`tests/tunion_dispatch.rs` (tunion-level only), the builder tests
|
||||
(write-side only), the reader tests (read-side only)
|
||||
|
||||
**Status**: resolved 2026-09-02 (with H3).
|
||||
|
||||
**Problem**: every field-disc union test lives on one side of the
|
||||
wire. The builder tests assume the variant-only layout
|
||||
(`layout_builder.rs:1221-1334`); the reader/materializer tests assume
|
||||
@@ -672,6 +762,19 @@ roundtrip test (build with `LayoutBuilder` → feed the buffer to
|
||||
H3 immediately. This is the highest-value single test in the review —
|
||||
add it with (or before) the H3 fix.
|
||||
|
||||
**Resolution (2026-09-02):**
|
||||
`tests/poc_roundtrip.rs::field_disc_union_roundtrip_build_read_materialize_validate`
|
||||
— exactly the test this finding asked for: union `fields: [type:
|
||||
uint8, seq: uint32]` (disc + a second shared field), variant `Read {
|
||||
handle: uint32 }` (non-redeclaring, numeric mapping keys `"1"`/`"2"`
|
||||
matching the uint8 disc). Write with `LayoutBuilder` +
|
||||
`data_access` (asserting `event.type@0, event.seq@1, event.handle@5`,
|
||||
total 10), read with `SequentialReader::read_next` (asserts
|
||||
`discriminator == "1"`, `variant_start == 5`), materialize with
|
||||
`materialize_packed` (asserts `type`/`seq`/`handle`/`__discriminator`
|
||||
in the object), and `engine.validate_bytes` on the same buffer. This
|
||||
is the test that would have caught every part of H3.
|
||||
|
||||
### N1. Field-discriminator kind support differs between `tunion` and the reader
|
||||
|
||||
**Files**: `src/tunion.rs:136-164` (supports `String`/`Uint8`/`Enum`),
|
||||
@@ -756,8 +859,8 @@ Worth recording, because the findings shouldn't eclipse it:
|
||||
|
||||
1. ~~**H1** — release-blocking~~ **resolved 2026-09-02** (with L1;
|
||||
see the resolution block on the finding).
|
||||
2. **H3** — needs a convention *decision* before code: write the
|
||||
addendum, enforce it, add L6's roundtrip test in the same session.
|
||||
2. ~~**H3**~~ **resolved 2026-09-02** (with L5 + L6; see the
|
||||
resolution block on the finding).
|
||||
3. **H2** — shared walk guard (or the minimum doc note if the full
|
||||
guard is judged too invasive for 0.3.x), plus the cyclic-schema
|
||||
tests for all three walkers.
|
||||
@@ -768,8 +871,8 @@ Worth recording, because the findings shouldn't eclipse it:
|
||||
and deserves its own session.
|
||||
7. **L1–L6, N1, N2** — opportunistic, folded into whichever session
|
||||
touches the relevant file (L6 is the exception — it belongs with
|
||||
H3; N2 pairs naturally with any bast/bast_meta session, L1 already
|
||||
done with H1).
|
||||
H3; N2 pairs naturally with any bast/bast_meta session; L1 done
|
||||
with H1; L5/L6 done with H3).
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
Reference in new issue
Block a user