Wire packed read path through ReadPlan (ADR-011 steps 2-4, plan phase 2)

SequentialReader now walks Arc<ReadPlan> instead of re-parsing the
BAST typed tree per field (the 400x read-path gap, review #004 H1);
materialize_packed walks the same plan, unifying the two packed
read-side consumers on one compiled form.

- SequentialReader::new(Arc<ReadPlan>) -> Self, infallible: the
  fallible BastDoc parse moved to ReadPlan::compile (phase 1). The
  reader holds the plan Arc + cursor state only; schema() returns the
  Arc<Value> retained on the plan (review #005 H2 — no
  self-referential struct); new plan() accessor exposes the shared
  plan.
- ReadPlan carries schema: Arc<Value> (set at compile; sub-plans hold
  a Null placeholder — only the root plan is handed out).
- materialize_packed(&ReadPlan, &[u8]): plan-walking packed
  materializer. The aligned path keeps walking BastDoc with the
  retained dummy_field_for/ty_source/materialize_typeref_packed
  helpers (phase 5 Scope Boundary: aligned structure walk is the
  permanent 0.3.0 design).
- Engine: Layout::Packed stores Arc<ReadPlan> alongside the builder;
  sequential_reader() is an Arc::clone (was a full-document Value
  clone); packed validate_bytes calls materialize_packed(&self.plan).
- Stride (deferred decision 4): FieldValue::Array now reports the
  true stride for fixed-size struct/nested-array elements (0.2.0
  returned 0); doc comment documents the behavioral change; no
  existing test asserted the 0, so none needed changing.
- Two parity subtleties found and preserved:
  (a) materialize_plan_composite unwraps the plan's anonymous
      single-field wrapper for primitive array elements/record values
      — without it, materialized records nest each leaf under a
      synthetic object (caught by the record parity test);
  (b) field-disc unions keep 0.2.0's materialized key order
      (__discriminator first), observable under preserve_order.
  Both are now covered by plan-phase tests or construction.

Bench (alktty wire_vs_bast, 1024 chunks/stream): packed read
2.27 us/chunk (review #004) -> 98 ns/chunk p64 / 100 ns/chunk p4k
(~23x; the 400x gap closes to ~17x vs hand-rolled 5.6 ns/chunk).
Residual gap is the per-field String allocation mandated by the
unchanged (String, FieldValue) read_next signature (2 allocs/chunk)
plus data_access bounds checks. sequential_reader() construction:
15.7 ns (was a whole-document clone).

Verification: 465 tests pass unchanged (the existing reader/
materialize/engine suites drive the rewrite through the public API —
only constructor call sites moved to ReadPlan::compile); clippy
-D warnings clean; cargo doc zero warnings; wasm32 release build
green.
This commit is contained in:
glm-5.3-flash committed 2026-09-02 07:52:46 +00:00
1 parent ff85258d03
commit e5f1b9d825
8 files changed
+1101 -644

No files matched your search

+45 -1
View File
@@ -214,7 +214,51 @@ Sync` so a future change can't break it silently — mirror the POC's
---
## Phase 2 — `SequentialReader` + `materialize_packed` consume `ReadPlan` (ADR-011 steps 2–4)
## Phase 2 — `SequentialReader` + `materialize_packed` consume `ReadPlan` (ADR-011 steps 2–4) — **DONE (2026-09-02)**
> **Status: implemented.** The packed read loop walks `Arc<ReadPlan>`:
> `SequentialReader::new(Arc<ReadPlan>) -> Self` (infallible; the old
> fallible constructor's work moved to `ReadPlan::compile`), the reader
> holds `plan: Arc<ReadPlan>` + cursor only, `schema()` returns the
> `Arc<Value>` retained on the plan (review #005 H2 closed — no
> self-referential struct), and a new `plan()` accessor exposes the
> shared plan. `materialize_packed(&ReadPlan, &[u8])` walks the same
> plan; the aligned materialize path keeps walking `BastDoc` with the
> retained `dummy_field_for`/`ty_source`/`materialize_typeref_packed`
> helpers (phase 5 Scope Boundary). Engine: `Layout::Packed` carries
> `Arc<ReadPlan>`; `sequential_reader()` is an `Arc::clone` (15.7 ns,
> was a whole-document `Value` clone); packed `validate_bytes` calls
> `materialize_packed(&self.plan, ...)`. The temporary validation
> bridge (reconstruct `BastDoc` for the validator) is still in place —
> phase 7 already retired it on `main`'s ValidationPlan; this phase's
> `validate_bytes` edit merged cleanly onto that state.
>
> **Stride (deferred decision 4):** fixed struct/nested-array elements
> now report their true stride through `FieldValue::Array`
> (0.2.0 returned `0`); doc comment updated; no existing test asserted
> the `0`, so no test needed changing — the plan-compile tests lock the
> new values.
>
> **Two parity subtleties found and preserved** (both invisible to the
> existing test suite, both now locked by tests or by construction):
> (a) the materializer unwraps the plan's anonymous single-field
> wrapper for primitive array elements/record values — without this,
> materialized records/arrays would nest each leaf under a synthetic
> object and `validate_bytes` would fail its own parity suite (caught
> by `materialize_record_packed_count_prefixed_pairs`); (b) the
> field-disc union's materialized key order keeps `__discriminator`
> first (matching 0.2.0's `Map` insertion order, observable under
> `preserve_order`).
>
> **Bench (alktty `wire_vs_bast`, 1024 chunks/stream):** read p64
> 2.27 µs/chunk (review #004) → **98 ns/chunk** (~23x; gap 400x →
> ~17x vs hand-rolled's 5.6 ns); read p4k → 100 ns/chunk. `engine.
> sequential_reader()` construction 15.7 ns (was a full `Value` clone).
> The residual gap is dominated by the per-field `String` allocation
> mandated by the unchanged `(String, FieldValue)` `read_next` return
> signature (2 allocs/chunk) plus `data_access` bounds checks — both
> outside this phase's scope (the signature is pinned by the Semver
> Contract).
**Goal:** Rewrite the packed read loop to walk `&ReadPlan` instead of
reconstructing `BastDoc`. `SequentialReader` stores `Arc<ReadPlan>` +
+21 -18
View File
@@ -82,6 +82,7 @@ pub(crate) fn validation_err(path: &str, reason: impl Into<String>) -> AlkTypeEr
mod tests {
use super::*;
use crate::materialize::materialize_packed;
use crate::read_plan::ReadPlan;
use serde_json::json;
fn doc_from<'a>(root: &'a Value, name: &'a str) -> BastDoc<'a> {
@@ -99,10 +100,12 @@ mod tests {
}
fn materialize_and_validate(
root: &Value,
doc: &BastDoc<'_>,
buffer: &[u8],
) -> Result<(), AlkTypeError> {
let value = materialize_packed(doc, buffer)?;
let plan = ReadPlan::compile(root, doc.root_name())?;
let value = materialize_packed(&plan, buffer)?;
validate_value(doc, &value)
}
@@ -121,8 +124,8 @@ mod tests {
] } }
});
let d = doc_from(&root, "S");
assert!(materialize_and_validate(&d, &u32_le(0)).is_ok());
assert!(materialize_and_validate(&d, &u32_le(0xFFFF_FFFF)).is_ok());
assert!(materialize_and_validate(&root, &d, &u32_le(0)).is_ok());
assert!(materialize_and_validate(&root, &d, &u32_le(0xFFFF_FFFF)).is_ok());
}
#[test]
@@ -133,8 +136,8 @@ mod tests {
] } }
});
let d = doc_from(&root, "S");
assert!(materialize_and_validate(&d, &[127u8]).is_ok());
assert!(materialize_and_validate(&d, &[128u8]).is_ok());
assert!(materialize_and_validate(&root, &d, &[127u8]).is_ok());
assert!(materialize_and_validate(&root, &d, &[128u8]).is_ok());
}
#[test]
@@ -190,8 +193,8 @@ mod tests {
] } }
});
let d = doc_from(&root, "S");
assert!(materialize_and_validate(&d, &prefixed_str_le("hi")).is_ok());
let err = materialize_and_validate(&d, &prefixed_str_le("hello")).unwrap_err();
assert!(materialize_and_validate(&root, &d, &prefixed_str_le("hi")).is_ok());
let err = materialize_and_validate(&root, &d, &prefixed_str_le("hello")).unwrap_err();
assert!(matches!(err, AlkTypeError::Validation(_)), "got {err:?}");
}
@@ -205,10 +208,10 @@ mod tests {
let d = doc_from(&root, "S");
let mut buf = u32_le(2);
buf.extend_from_slice(&[0xAA, 0xBB]);
assert!(materialize_and_validate(&d, &buf).is_ok());
assert!(materialize_and_validate(&root, &d, &buf).is_ok());
let mut buf = u32_le(3);
buf.extend_from_slice(&[0xAA, 0xBB, 0xCC]);
let err = materialize_and_validate(&d, &buf).unwrap_err();
let err = materialize_and_validate(&root, &d, &buf).unwrap_err();
assert!(matches!(err, AlkTypeError::Validation(_)), "got {err:?}");
}
@@ -238,8 +241,8 @@ mod tests {
}
});
let d = doc_from(&root, "S");
assert!(materialize_and_validate(&d, &u32_le(0)).is_ok());
assert!(materialize_and_validate(&d, &u32_le(2)).is_ok());
assert!(materialize_and_validate(&root, &d, &u32_le(0)).is_ok());
assert!(materialize_and_validate(&root, &d, &u32_le(2)).is_ok());
}
#[test]
@@ -253,7 +256,7 @@ mod tests {
}
});
let d = doc_from(&root, "S");
let err = materialize_and_validate(&d, &u32_le(5)).unwrap_err();
let err = materialize_and_validate(&root, &d, &u32_le(5)).unwrap_err();
assert!(matches!(err, AlkTypeError::Validation(_)), "got {err:?}");
}
@@ -284,16 +287,16 @@ mod tests {
let mut buf = vec![2u8];
buf.extend_from_slice(&u32_le(2));
buf.extend_from_slice(&[0xAA, 0xBB]);
assert!(materialize_and_validate(&d, &buf).is_ok());
assert!(materialize_and_validate(&root, &d, &buf).is_ok());
let mut buf = vec![2u8];
buf.extend_from_slice(&u32_le(3));
buf.extend_from_slice(&[0xAA, 0xBB, 0xCC]);
let err = materialize_and_validate(&d, &buf).unwrap_err();
let err = materialize_and_validate(&root, &d, &buf).unwrap_err();
assert!(matches!(err, AlkTypeError::Validation(_)), "got {err:?}");
let buf = vec![1u8, 7u8];
assert!(materialize_and_validate(&d, &buf).is_ok());
assert!(materialize_and_validate(&root, &d, &buf).is_ok());
}
#[test]
@@ -318,7 +321,7 @@ mod tests {
let mut buf = prefixed_str_le("data");
buf.extend_from_slice(&u32_le(2));
buf.extend_from_slice(&[0xFF, 0xFE]);
let err = materialize_and_validate(&d, &buf).unwrap_err();
let err = materialize_and_validate(&root, &d, &buf).unwrap_err();
assert!(matches!(err, AlkTypeError::Validation(_)), "got {err:?}");
}
@@ -383,7 +386,7 @@ mod tests {
buf.extend_from_slice(&2u16.to_le_bytes());
buf.extend_from_slice(&3u16.to_le_bytes());
buf.extend_from_slice(&4u16.to_le_bytes());
assert!(materialize_and_validate(&d, &buf).is_ok());
assert!(materialize_and_validate(&root, &d, &buf).is_ok());
}
#[test]
@@ -427,7 +430,7 @@ mod tests {
] } }
});
let d = doc_from(&root, "S");
let err = materialize_and_validate(&d, &[0u8; 2]).unwrap_err();
let err = materialize_and_validate(&root, &d, &[0u8; 2]).unwrap_err();
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
}
+19 -11
View File
@@ -22,6 +22,7 @@ use crate::error::AlkTypeError;
use crate::layout_builder::LayoutBuilder;
use crate::materialize;
use crate::offset_map::OffsetMap;
use crate::read_plan::ReadPlan;
use crate::schema::{AlkTypeKind, Endian, VariableEncoding};
use crate::sequential_reader::{FieldValue, SequentialReader};
use crate::validation;
@@ -47,12 +48,14 @@ pub enum LayoutMode {
/// APIs that make sense for that mode.
#[derive(Debug)]
enum Layout {
/// Packed sequential layout. The write-side is [`LayoutBuilder`]; the
/// read-side is a fresh [`SequentialReader`] constructed on demand
/// (ADR-007 — the reader has mutable cursor state that the consumer
/// owns, so the engine is a factory, not a holder).
/// Packed sequential layout. The write-side is [`LayoutBuilder`];
/// the read-side compiled form is the [`ReadPlan`] (ADR-011),
/// shared via `Arc` with every [`SequentialReader`] the factory
/// hands out (ADR-007 — the reader owns its cursor state, so the
/// engine is a factory, not a holder).
Packed {
builder: LayoutBuilder,
plan: Arc<ReadPlan>,
},
/// Aligned static layout. Field offsets are precomputed in an
/// [`OffsetMap`] for random access.
@@ -151,7 +154,8 @@ impl AlkTypeEngine {
let layout = match mode {
LayoutMode::Packed => {
let builder = LayoutBuilder::new(bast_doc, root_name)?;
Layout::Packed { builder }
let plan = Arc::new(ReadPlan::compile(bast_doc, root_name)?);
Layout::Packed { builder, plan }
}
LayoutMode::Aligned => {
let offset_map = OffsetMap::compute(&doc)?;
@@ -204,15 +208,17 @@ impl AlkTypeEngine {
}
/// Construct a fresh [`SequentialReader`] for packed-mode reads
/// (ADR-007). Each call returns a new reader with the cursor at
/// position 0. The consumer owns the reader and calls
/// `read_next`/`read_field`/`reset` on it directly.
/// (ADR-007, ADR-011). Each call returns a new reader with the
/// cursor at position 0, sharing the engine's `Arc<ReadPlan>` (a
/// refcount bump — no re-parse, no document clone). The consumer
/// owns the reader and calls `read_next`/`read_field`/`reset` on it
/// directly.
///
/// Returns `None` if compiled in aligned mode.
pub fn sequential_reader(&self) -> Option<SequentialReader> {
match &self.layout {
Layout::Packed { .. } => {
SequentialReader::new(&self.bast_doc, &self.root_name).ok()
Layout::Packed { plan, .. } => {
Some(SequentialReader::new(Arc::clone(plan)))
}
Layout::Aligned { .. } => None,
}
@@ -298,7 +304,9 @@ impl AlkTypeEngine {
pub fn validate_bytes(&self, buffer: &[u8]) -> Result<(), AlkTypeError> {
let doc = BastDoc::new(&self.bast_doc, &self.root_name)?;
let value = match &self.layout {
Layout::Packed { .. } => materialize::materialize_packed(&doc, buffer)?,
Layout::Packed { plan, .. } => {
materialize::materialize_packed(plan, buffer)?
}
Layout::Aligned { offset_map } => {
materialize::materialize_aligned(&doc, buffer, offset_map)?
}
+445 -50
View File
@@ -1,27 +1,25 @@
//! Materialize a `serde_json::Value` tree from a binary buffer by walking
//! the BAST typed tree. Used by
//! Materialize a `serde_json::Value` tree from a binary buffer. Used by
//! [`crate::engine::AlkTypeEngine::validate_bytes`] (ADR-010) to collapse
//! the two-step dance (read bytes -> `Value`, then validate `Value`) into
//! a single call.
//!
//! The materializer reuses the [`crate::data_access`] read functions for
//! leaf kinds and recurses into composites (`Struct`, `Array`, `Record`,
//! `Union`). For `Union`, it dispatches on the discriminator and recurses
//! into the variant. Errors carry the field path (ADR-004).
//!
//! Packed mode walks sequentially from offset 0; aligned mode reads at
//! offsets from the [`crate::offset_map::OffsetMap`]. Both produce the
//! same `Value` form; the validator is mode-agnostic.
//! Packed mode walks the compiled [`ReadPlan`]
//! (ADR-011) — the same compiled form [`crate::sequential_reader`] walks,
//! unifying the two packed read-side consumers on one plan. Aligned mode
//! walks the BAST typed tree, reading leaves at offsets from the
//! [`crate::offset_map::OffsetMap`] (the permanent 0.3.0 design — see the
//! phase 5 Scope Boundary in the 0.3.0 plan). Both produce the same
//! `Value` form; the validator is mode-agnostic.
//!
//! ## `$ref` resolution
//!
//! The BAST document ([`crate::bast::BastDoc`]) is threaded through every
//! recursive call so that `$ref` pointers (e.g. `"#/$defs/Read"`) in
//! composite variants can be resolved via
//! [`crate::bast::BastDoc::resolve_typeref`]. This is load-bearing for
//! unions and arrays whose variants/elements are `$ref`s into `$defs`
//! (the SFTP Packet shape: a struct wrapping a union with `$ref`
//! variants).
//! Packed mode: resolved eagerly at `ReadPlan::compile` — the per-buffer
//! walk never touches the BAST document. Aligned mode: the BAST document
//! ([`crate::bast::BastDoc`]) is threaded through every recursive call so
//! `$ref` pointers (e.g. `"#/$defs/Read"`) in composite variants resolve
//! via [`crate::bast::BastDoc::resolve_typeref`] at walk time —
//! load-bearing for unions and arrays whose variants/elements are `$ref`s
//! into `$defs`.
use crate::bast::{
BastArray, BastDefKind, BastDiscriminator, BastDoc, BastField, BastRecord, BastStruct,
@@ -29,35 +27,432 @@ use crate::bast::{
};
use crate::data_access;
use crate::error::AlkTypeError;
use crate::read_plan::{
CompositePlan, DiscriminatorPlan, FieldPlan, ReadKind, ReadPlan,
};
use crate::schema::{AlkTypeKind, Endian, VariableEncoding};
use serde_json::{Map, Value};
const U32_SIZE: usize = 4;
const DISCRIMINATOR_KEY: &str = "__discriminator";
/// Materialize a `Value` tree from `buffer` by walking the BAST root type
/// in packed mode (sequential, from offset 0).
/// Materialize a `Value` tree from `buffer` by walking the compiled
/// [`ReadPlan`] in packed mode (sequential, from offset 0).
///
/// The root type must be a struct. Endianness is read from the root
/// struct's `endian` annotation (defaults to little-endian).
/// The plan's root must be a struct (enforced at `ReadPlan::compile`).
/// Endianness is baked per node at compile time.
pub fn materialize_packed(
doc: &BastDoc<'_>,
plan: &ReadPlan,
buffer: &[u8],
) -> Result<Value, AlkTypeError> {
let root_def = doc.root_def();
let struct_node = match root_def.kind() {
BastDefKind::Struct(s) => s,
other => {
let mut offset = 0usize;
let mut obj = Map::new();
for field in plan.fields() {
let value = materialize_plan_field(plan, field, field.name(), buffer, &mut offset)?;
obj.insert(field.name().to_string(), value);
}
Ok(Value::Object(obj))
}
/// Materialize one planned field (packed mode): dispatch on the
/// field's [`ReadKind`], reading leaves via [`crate::data_access`] and
/// recursing into the compiled composite body. `endian` is the field's
/// effective endianness baked into the plan.
fn materialize_plan_field(
plan: &ReadPlan,
field: &FieldPlan,
field_path: &str,
buffer: &[u8],
offset: &mut usize,
) -> Result<Value, AlkTypeError> {
let _ = plan;
match field.kind() {
ReadKind::Primitive(kind) => {
materialize_plan_primitive(*kind, field.endian(), field_path, buffer, offset)
}
ReadKind::Enum => {
let v = data_access::read_enum(buffer, *offset, field_path, field.endian())?;
*offset += 4;
Ok(Value::from(v))
}
ReadKind::Struct => {
let body = match field.body() {
Some(CompositePlan::Struct(p)) => p,
_ => {
return Err(AlkTypeError::Schema(format!(
"internal: struct field {field_path} has no Struct body"
)))
}
};
materialize_plan_struct(body, field_path, buffer, offset)
}
ReadKind::Union => {
let body = field.body().ok_or_else(|| AlkTypeError::Schema(format!(
"internal: union field {field_path} has no body"
)))?;
materialize_plan_union(body, field_path, field.endian(), buffer, offset)
}
ReadKind::Array => {
let body = field.body().ok_or_else(|| AlkTypeError::Schema(format!(
"internal: array field {field_path} has no body"
)))?;
materialize_plan_array(body, field_path, field.endian(), buffer, offset)
}
ReadKind::Record => {
let body = field.body().ok_or_else(|| AlkTypeError::Schema(format!(
"internal: record field {field_path} has no body"
)))?;
materialize_plan_record(body, field_path, field.endian(), buffer, offset)
}
}
}
fn materialize_plan_primitive(
kind: AlkTypeKind,
endian: Endian,
field_path: &str,
buffer: &[u8],
offset: &mut usize,
) -> Result<Value, AlkTypeError> {
match kind {
AlkTypeKind::Int8 => {
let v = data_access::read_i8(buffer, *offset, field_path)?;
*offset += 1;
Ok(Value::from(v))
}
AlkTypeKind::Int16 => {
let v = data_access::read_i16(buffer, *offset, field_path, endian)?;
*offset += 2;
Ok(Value::from(v))
}
AlkTypeKind::Int32 => {
let v = data_access::read_i32(buffer, *offset, field_path, endian)?;
*offset += 4;
Ok(Value::from(v))
}
AlkTypeKind::Int64 => {
let v = data_access::read_i64(buffer, *offset, field_path, endian)?;
*offset += 8;
Ok(Value::from(v))
}
AlkTypeKind::Uint8 => {
let v = data_access::read_u8(buffer, *offset, field_path)?;
*offset += 1;
Ok(Value::from(v))
}
AlkTypeKind::Uint16 => {
let v = data_access::read_u16(buffer, *offset, field_path, endian)?;
*offset += 2;
Ok(Value::from(v))
}
AlkTypeKind::Uint32 => {
let v = data_access::read_u32(buffer, *offset, field_path, endian)?;
*offset += 4;
Ok(Value::from(v))
}
AlkTypeKind::Uint64 => {
let v = data_access::read_u64(buffer, *offset, field_path, endian)?;
*offset += 8;
Ok(Value::from(v))
}
AlkTypeKind::Float32 => {
let v = data_access::read_f32(buffer, *offset, field_path, endian)?;
*offset += 4;
Ok(number_from_f64(v as f64, field_path)?)
}
AlkTypeKind::Float64 => {
let v = data_access::read_f64(buffer, *offset, field_path, endian)?;
*offset += 8;
Ok(number_from_f64(v, field_path)?)
}
AlkTypeKind::Boolean => {
let v = data_access::read_bool(buffer, *offset, field_path)?;
*offset += 1;
Ok(Value::Bool(v))
}
AlkTypeKind::String => {
let s = data_access::read_string(buffer, *offset, field_path, endian)?;
let len = U32_SIZE
.checked_add(s.len())
.ok_or_else(|| AlkTypeError::Access {
field_path: field_path.to_string(),
reason: "string size 4 + len overflows usize".to_string(),
})?;
*offset = checked_add_at(*offset, len, field_path, "string")?;
Ok(Value::String(s.to_string()))
}
AlkTypeKind::Bytes => {
let b = data_access::read_bytes(buffer, *offset, field_path, endian)?;
let len = U32_SIZE
.checked_add(b.len())
.ok_or_else(|| AlkTypeError::Access {
field_path: field_path.to_string(),
reason: "bytes size 4 + len overflows usize".to_string(),
})?;
*offset = checked_add_at(*offset, len, field_path, "bytes")?;
let arr: Vec<Value> = b.iter().map(|&byte| Value::from(u32::from(byte))).collect();
Ok(Value::Array(arr))
}
other => Err(AlkTypeError::Schema(format!(
"materialize: unsupported primitive kind {other} at {field_path}"
))),
}
}
fn checked_add_at(
offset: usize,
size: usize,
field_path: &str,
what: &str,
) -> Result<usize, AlkTypeError> {
offset.checked_add(size).ok_or_else(|| AlkTypeError::Access {
field_path: field_path.to_string(),
reason: format!("{what} offset {offset} + {size} overflows usize"),
})
}
fn materialize_plan_struct(
plan: &ReadPlan,
field_path: &str,
buffer: &[u8],
offset: &mut usize,
) -> Result<Value, AlkTypeError> {
let mut obj = Map::new();
for field in plan.fields() {
let path = format!("{field_path}.{}", field.name());
let value = materialize_plan_field(plan, field, &path, buffer, offset)?;
obj.insert(field.name().to_string(), value);
}
Ok(Value::Object(obj))
}
fn materialize_plan_array(
body: &CompositePlan,
field_path: &str,
endian: Endian,
buffer: &[u8],
offset: &mut usize,
) -> Result<Value, AlkTypeError> {
let (element, count) = match body {
CompositePlan::Array {
element,
count,
..
} => (element, *count),
_ => {
return Err(AlkTypeError::Schema(format!(
"materialize_packed: expected a struct at the root, got {kind}",
kind = other.alk_kind()
)));
"internal: array body at {field_path} is not CompositePlan::Array"
)))
}
};
let effective_endian = struct_node.endian();
materialize_struct_packed(doc, struct_node, "", effective_endian, buffer, &mut 0)
let mut arr = Vec::with_capacity(count);
for i in 0..count {
let path = format!("{field_path}[{i}]");
let value = materialize_plan_composite(element, &path, endian, buffer, offset)?;
arr.push(value);
}
Ok(Value::Array(arr))
}
/// Materialize a composite body used as a TypeRef (array element,
/// record value, union variant).
///
/// Primitive/enum elements and values were wrapped at compile time in an
/// anonymous single-field struct plan (the wrap-leaf convention) so the
/// read loop has a uniform node; the materializer unwraps that wrapper
/// and emits the leaf value directly — the 0.2.0 materializer never
/// wrapped (real schema field names cannot be empty, so an empty name
/// uniquely identifies the wrapper).
fn materialize_plan_composite(
body: &CompositePlan,
field_path: &str,
endian: Endian,
buffer: &[u8],
offset: &mut usize,
) -> Result<Value, AlkTypeError> {
if let CompositePlan::Struct(plan) = body {
if let Some(only) = plan.fields().first() {
if plan.fields().len() == 1
&& only.name().is_empty()
&& matches!(only.kind(), ReadKind::Primitive(_) | ReadKind::Enum)
{
return materialize_plan_field(plan, only, field_path, buffer, offset);
}
}
}
match body {
CompositePlan::Struct(plan) => materialize_plan_struct(plan, field_path, buffer, offset),
CompositePlan::Union { .. } => {
materialize_plan_union(body, field_path, endian, buffer, offset)
}
CompositePlan::Array { .. } => {
materialize_plan_array(body, field_path, endian, buffer, offset)
}
CompositePlan::Record { .. } => {
materialize_plan_record(body, field_path, endian, buffer, offset)
}
}
}
fn materialize_plan_union(
body: &CompositePlan,
field_path: &str,
endian: Endian,
buffer: &[u8],
offset: &mut usize,
) -> Result<Value, AlkTypeError> {
let (disc, shared, variants) = match body {
CompositePlan::Union {
disc,
shared,
variants,
} => (disc, shared, variants),
_ => {
return Err(AlkTypeError::Schema(format!(
"internal: union body at {field_path} is not CompositePlan::Union"
)))
}
};
match disc {
DiscriminatorPlan::Byte {
offset: disc_rel,
disc_type,
} => {
let disc_abs = checked_add_at(*offset, *disc_rel, field_path, "discriminator")?;
let disc_value = match disc_type {
AlkTypeKind::Uint8 => {
data_access::read_u8(buffer, disc_abs, field_path)? as u32
}
AlkTypeKind::Uint16 => {
data_access::read_u16(buffer, disc_abs, field_path, endian)? as u32
}
AlkTypeKind::Uint32 => {
data_access::read_u32(buffer, disc_abs, field_path, endian)?
}
other => {
return Err(AlkTypeError::Schema(format!(
"internal: union discriminator type {other} is not a supported byte \
discriminator (parser should have rejected this)"
)));
}
};
let key = disc_value.to_string();
let variant = variants
.iter()
.find(|(k, _)| *k == key)
.map(|(_, v)| v)
.ok_or_else(|| AlkTypeError::Access {
field_path: field_path.to_string(),
reason: format!("union discriminator value {key} not in mapping"),
})?;
let disc_size = disc_type.type_size().unwrap_or(1);
let variant_offset =
checked_add_at(disc_abs, disc_size, field_path, "variant offset")?;
*offset = variant_offset;
let variant_value =
materialize_plan_composite(variant, field_path, endian, buffer, offset)?;
Ok(tag_union_value_number(disc_value, variant_value))
}
DiscriminatorPlan::Field { name, field_index } => {
let shared_plan = shared.as_ref().ok_or_else(|| AlkTypeError::Schema(format!(
"internal: field-disc union at {field_path} has no shared plan"
)))?;
let disc_field = shared_plan.fields().get(*field_index).ok_or_else(|| {
AlkTypeError::Schema(format!(
"internal: union {field_path} discriminator index {field_index} out of range"
))
})?;
let _ = disc_field;
// Walk the whole shared plan; capture the discriminator
// field's value for the mapping key as we pass it. The
// result object preserves the 0.2.0 key order:
// `__discriminator` first, then the shared fields, then the
// variant's fields.
let mut disc_value: Option<Value> = None;
let mut shared_values: Vec<(String, Value)> = Vec::new();
for field in shared_plan.fields() {
let path = format!("{field_path}.{}", field.name());
let value = materialize_plan_field(shared_plan, field, &path, buffer, offset)?;
if field.name() == name.as_str() {
disc_value = Some(value);
} else {
shared_values.push((field.name().to_string(), value));
}
}
let disc_value = disc_value.ok_or_else(|| AlkTypeError::Schema(format!(
"internal: union {field_path} discriminator field {name:?} not walked"
)))?;
let key = plan_value_discriminator_key(&disc_value, field_path)?;
let variant = variants
.iter()
.find(|(k, _)| *k == key)
.map(|(_, v)| v)
.ok_or_else(|| AlkTypeError::Access {
field_path: field_path.to_string(),
reason: format!("union discriminator value {key} not in mapping"),
})?;
let variant_value =
materialize_plan_composite(variant, field_path, endian, buffer, offset)?;
let mut obj = Map::new();
obj.insert(DISCRIMINATOR_KEY.to_string(), Value::String(key));
obj.insert((*name).to_string(), disc_value);
for (k, v) in shared_values {
obj.insert(k, v);
}
flatten_variant_into(&mut obj, variant_value);
Ok(Value::Object(obj))
}
}
}
/// Stringify a materialized discriminator field value for mapping
/// lookup. Mirrors the 0.2.0 `union_discriminator_key` for the Value form.
fn plan_value_discriminator_key(value: &Value, field_path: &str) -> Result<String, AlkTypeError> {
match value {
Value::String(s) => Ok(s.clone()),
Value::Number(n) => Ok(n.to_string()),
_ => Err(AlkTypeError::Schema(format!(
"union {field_path} has unsupported discriminator value kind: {value}"
))),
}
}
fn materialize_plan_record(
body: &CompositePlan,
field_path: &str,
endian: Endian,
buffer: &[u8],
offset: &mut usize,
) -> Result<Value, AlkTypeError> {
let value_body = match body {
CompositePlan::Record { value } => value,
_ => {
return Err(AlkTypeError::Schema(format!(
"internal: record body at {field_path} is not CompositePlan::Record"
)))
}
};
let count = data_access::read_u32(buffer, *offset, field_path, endian)? as usize;
*offset += U32_SIZE;
let mut obj = Map::new();
for i in 0..count {
let key_path = format!("{field_path}[{i}].key");
let key_str = data_access::read_string(buffer, *offset, &key_path, endian)?;
let key_len = U32_SIZE
.checked_add(key_str.len())
.ok_or_else(|| AlkTypeError::Access {
field_path: key_path.clone(),
reason: "key size 4 + len overflows usize".to_string(),
})?;
*offset = checked_add_at(*offset, key_len, &key_path, "key")?;
let val_path = format!("{field_path}[{i}].value");
let val = materialize_plan_composite(value_body, &val_path, endian, buffer, offset)?;
obj.insert(key_str.to_string(), val);
}
Ok(Value::Object(obj))
}
/// Materialize a `Value` tree from `buffer` by walking the BAST root type
/// in aligned mode (offsets from `offset_map`).
///
@@ -83,6 +478,7 @@ pub fn materialize_aligned(
materialize_struct_aligned(doc, struct_node, "", offset_map, effective_endian, buffer)
}
fn materialize_struct_packed(
doc: &BastDoc<'_>,
struct_node: &BastStruct<'_>,
@@ -399,10 +795,7 @@ fn materialize_record_packed(
Ok(Value::Object(obj))
}
/// Build the materialized `Value` for a byte-offset union: an object
/// `{ "__discriminator": <disc_value>, ...variant-fields }`. If the
/// variant materialized as a non-object (a leaf), it is nested under
/// `"__variant"`.
fn tag_union_value_number(disc_value: u32, variant_value: Value) -> Value {
let mut obj = Map::new();
obj.insert(DISCRIMINATOR_KEY.to_string(), Value::from(disc_value));
@@ -672,10 +1065,12 @@ mod tests {
}
fn materialize_packed_strict(
root: &Value,
doc: &BastDoc<'_>,
buffer: &[u8],
) -> Result<Value, AlkTypeError> {
materialize_packed(doc, buffer)
let plan = ReadPlan::compile(root, doc.root_name())?;
materialize_packed(&plan, buffer)
}
#[test]
@@ -694,7 +1089,7 @@ mod tests {
buf[4] = 0x41;
buf[5] = 0x42;
buf[6] = 0xC3;
let v = materialize_packed_strict(&doc, &buf).expect("materialize");
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
assert_eq!(v["blob"], json!([65, 66, 195]));
}
@@ -715,7 +1110,7 @@ mod tests {
buf[5] = 0xFE;
buf[6] = 0x00;
buf[7] = 0x80;
let v = materialize_packed_strict(&doc, &buf).expect("materialize");
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
assert_eq!(v["raw"], json!([255, 254, 0, 128]));
}
@@ -747,7 +1142,7 @@ mod tests {
buf[off] = b'a';
off += 1;
buf[off..off + 4].copy_from_slice(&20u32.to_le_bytes());
let v = materialize_packed_strict(&doc, &buf).expect("materialize");
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
assert_eq!(v["counts"], json!({ "b": 10, "a": 20 }));
}
@@ -766,7 +1161,7 @@ mod tests {
let doc = doc_from(&root, "S");
let mut buf = vec![0u8; 4];
buf[0..4].copy_from_slice(&0u32.to_le_bytes());
let v = materialize_packed_strict(&doc, &buf).expect("materialize");
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
assert_eq!(v["counts"], json!({}));
}
@@ -784,7 +1179,7 @@ mod tests {
});
let doc = doc_from(&root, "S");
let buf = [0u8; 2];
let err = materialize_packed_strict(&doc, &buf).unwrap_err();
let err = materialize_packed_strict(&root, &doc, &buf).unwrap_err();
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
}
@@ -818,7 +1213,7 @@ mod tests {
let mut buf = vec![0u8; 5];
buf[0] = 5;
buf[1..5].copy_from_slice(&42u32.to_le_bytes());
let v = materialize_packed_strict(&doc, &buf).expect("materialize");
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
let payload = &v["payload"];
assert_eq!(payload["__discriminator"], json!(5));
assert_eq!(payload["id"], json!(42));
@@ -856,7 +1251,7 @@ mod tests {
buf[0..4].copy_from_slice(&4u32.to_le_bytes());
buf[4..8].copy_from_slice(b"read");
buf[8..12].copy_from_slice(&7u32.to_le_bytes());
let v = materialize_packed_strict(&doc, &buf).expect("materialize");
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
let payload = &v["payload"];
assert_eq!(payload["__discriminator"], json!("read"));
assert_eq!(payload["type"], json!("read"));
@@ -897,7 +1292,7 @@ mod tests {
buf[4..8].copy_from_slice(b"read");
buf[8..12].copy_from_slice(&7u32.to_le_bytes());
buf[12..16].copy_from_slice(&99u32.to_le_bytes());
let v = materialize_packed_strict(&doc, &buf).expect("materialize");
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
assert_eq!(v["trailer"], json!(99));
assert_eq!(v["payload"]["n"], json!(7));
}
@@ -930,7 +1325,7 @@ mod tests {
});
let doc = doc_from(&root, "S");
let buf = [99u8];
let err = materialize_packed_strict(&doc, &buf).unwrap_err();
let err = materialize_packed_strict(&root, &doc, &buf).unwrap_err();
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
}
@@ -964,7 +1359,7 @@ mod tests {
let mut buf = vec![0u8; 5];
buf[0] = 5;
buf[1..5].copy_from_slice(&42u32.to_le_bytes());
let v = materialize_packed_strict(&doc, &buf).expect("materialize");
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
assert_eq!(v["payload"]["__discriminator"], json!(5));
assert_eq!(v["payload"]["id"], json!(42));
}
@@ -1001,7 +1396,7 @@ mod tests {
buf[2..4].copy_from_slice(&2u16.to_le_bytes());
buf[4..6].copy_from_slice(&3u16.to_le_bytes());
buf[6..8].copy_from_slice(&4u16.to_le_bytes());
let v = materialize_packed_strict(&doc, &buf).expect("materialize");
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
assert_eq!(v["items"], json!([{ "x": 1, "y": 2 }, { "x": 3, "y": 4 }]));
}
@@ -1032,7 +1427,7 @@ mod tests {
buf[0] = 1;
buf[1] = 2;
buf[2..6].copy_from_slice(&0x03030303u32.to_le_bytes());
let v = materialize_packed_strict(&doc, &buf).expect("materialize");
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
assert_eq!(v["header"]["a"], json!(1));
assert_eq!(v["header"]["b"], json!(2));
assert_eq!(v["after"], json!(0x03030303u32));
@@ -1054,7 +1449,7 @@ mod tests {
});
let doc = doc_from(&root, "S");
let buf = [0u8, 0u8, 0u8, 42u8, 0u8, 0u8, 0u8, 7u8];
let v = materialize_packed_strict(&doc, &buf).expect("materialize");
let v = materialize_packed_strict(&root, &doc, &buf).expect("materialize");
assert_eq!(v["channel_id"], json!(42));
assert_eq!(v["length"], json!(7));
}
@@ -1198,7 +1593,7 @@ mod tests {
});
let doc = doc_from(&root, "S");
let buf = f32::NAN.to_le_bytes();
let err = materialize_packed_strict(&doc, &buf).unwrap_err();
let err = materialize_packed_strict(&root, &doc, &buf).unwrap_err();
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
}
}
+16 -1
View File
@@ -44,6 +44,7 @@ use crate::error::AlkTypeError;
use crate::schema::{AlkTypeKind, Endian, VariableEncoding};
use serde_json::Value;
use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
/// Maximum recursion depth for the compile walk. Bounds deeply-nested
/// (or cyclically-referencing) adversarial schemas; pairs with the
@@ -65,6 +66,7 @@ pub struct ReadPlan {
endian: Endian,
fields: Vec<FieldPlan>,
by_name: BTreeMap<String, usize>,
schema: Arc<Value>,
}
/// One compiled struct field: the read dispatch kind, the effective
@@ -171,7 +173,10 @@ impl ReadPlan {
}
};
let mut seen = BTreeSet::new();
compile_struct(&doc, struct_node, struct_node.endian(), "", 0, &mut seen)
compile_struct(&doc, struct_node, struct_node.endian(), "", 0, &mut seen).map(|mut plan| {
plan.schema = Arc::new(bast_doc.clone());
plan
})
}
/// The root container default endianness.
@@ -188,6 +193,13 @@ impl ReadPlan {
pub fn field_index(&self, name: &str) -> Option<usize> {
self.by_name.get(name).copied()
}
/// The BAST document this plan was compiled from (retained as
/// `Arc<Value>` — the reader's `schema()` accessor returns it, and
/// phase 6's `Hash` derive covers it via `serde_json::Value: Hash`).
pub fn schema(&self) -> &Value {
&self.schema
}
}
impl FieldPlan {
@@ -264,6 +276,7 @@ fn compile_struct(
endian: container_endian,
fields,
by_name,
schema: Arc::new(Value::Null),
})
}
@@ -428,6 +441,7 @@ fn compile_union(
endian: container_endian,
fields: plan,
by_name,
schema: Arc::new(Value::Null),
}))
}
};
@@ -568,6 +582,7 @@ fn wrap_leaf(
endian: container_endian,
fields: vec![field],
by_name: BTreeMap::new(),
schema: Arc::new(Value::Null),
}))
}
_ => Err(AlkTypeError::Schema(format!(
+546 -558
View File
File diff suppressed because it is too large. Load diff
+5 -3
View File
@@ -376,7 +376,8 @@ fn sequential_reader_buffer_too_short_returns_access_error() {
}
});
let buffer = [0u8; 2];
let mut reader = SequentialReader::new(&root, "S").unwrap();
let plan = ReadPlan::compile(&root, "S").unwrap();
let mut reader = SequentialReader::new(std::sync::Arc::new(plan));
let err = reader.read_next(&buffer).unwrap_err();
assert!(matches!(err, AlkTypeError::Access { .. }), "got {err:?}");
}
@@ -392,7 +393,8 @@ fn sequential_reader_unknown_field_returns_schema_error() {
}
});
let buffer = [0u8; 4];
let mut reader = SequentialReader::new(&root, "S").unwrap();
let plan = ReadPlan::compile(&root, "S").unwrap();
let mut reader = SequentialReader::new(std::sync::Arc::new(plan));
let err = reader.read_field(&buffer, "missing").unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
}
@@ -409,7 +411,7 @@ fn sequential_reader_new_non_struct_returns_schema_error() {
"A": { "kind": "struct", "fields": [] }
}
});
let err = SequentialReader::new(&root, "U").unwrap_err();
let err = ReadPlan::compile(&root, "U").unwrap_err();
assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}");
}
+4 -2
View File
@@ -459,7 +459,8 @@ fn sequential_reader_round_trip_packed_buffer() -> Result<(), AlkTypeError> {
let after = 1 + 4 + payload.len();
data_access::write_u8(&mut buffer, after, 99, "tail")?;
let mut reader = SequentialReader::new(&root, "S")?;
let plan = ReadPlan::compile(&root, "S")?;
let mut reader = SequentialReader::new(std::sync::Arc::new(plan));
assert_eq!(reader.endian(), Endian::Little);
assert_eq!(reader.position(), 0);
@@ -502,7 +503,8 @@ fn sequential_reader_read_field_walks_preceding_fields() -> Result<(), AlkTypeEr
data_access::write_u32(&mut buffer, 1, 0xDEADBEEF, "b", Endian::Little)?;
data_access::write_u8(&mut buffer, 5, 9, "c")?;
let mut reader = SequentialReader::new(&root, "S")?;
let plan = ReadPlan::compile(&root, "S")?;
let mut reader = SequentialReader::new(std::sync::Arc::new(plan));
let value = reader.read_field(&buffer, "c")?;
assert_eq!(value, FieldValue::U8(9));
assert_eq!(reader.position(), 6);