perf: fixed-size struct fast path, integer union dispatch, zero-alloc read_next_borrowed
Targets the bench gaps from the 0.3.0 port review (commit dea96f0):
packet read was ~111-189x hand-rolled, chunk read ~18x.
- ReadPlan gains compile-time fixed_size (cached field-size sum).
Fixed structs skip the cursor size walk entirely (one bounds check
instead); fixed-size union variants skip the plan_walk_variant_size
pre-pass, eliminating the double walk of variant bytes for the
common SFTP-shaped case.
- CompositePlan::Union gains an int_keys dispatch table (pre-parsed
u64 mapping keys); byte-discriminator unions dispatch on the raw
integer instead of stringifying per read. Returned discriminator
String unchanged (public API). String-keyed fallback preserved.
- Additive SequentialReader::read_next_borrowed returns the field
name borrowed from the plan — zero allocs per field for hot loops.
read_next stays the owned-name form (single source of truth).
- plan_walk_struct_size / union shared walk: per-field format! moved
to the error path only.
- materialize: with_capacity for bytes arrays, arrays, and struct
objects.
Benches (1024 chunks/iter, criterion, pre-review baseline vs now):
- read_packet_stream: 600 -> 246 µs (~2.4x; gap to hand 189x -> ~74x)
- read_chunk_stream: 104 -> 67 µs (~1.6x; 18x -> ~11x)
- write/validate groups unchanged (within noise)
- engine_compile +8% (int_keys table + fixed-size precompute), still
one-shot
Verification: 566 tests pass, clippy -D warnings clean, wasm32 build
green. Bench baselines saved as pre-review/post-review.
This commit is contained in:
1 parent
dea96f0195
commit
d4635d28f0
4 files changed
+284
-72
No files matched your search
@@ -216,11 +216,11 @@ fn alktype_read_stream(buf: &[u8], n: usize, reader: &mut SequentialReader) -> u
|
||||
let mut total = 0usize;
|
||||
for _ in 0..n {
|
||||
reader.reset();
|
||||
let st = match reader.read_next(&buf[pos..]) {
|
||||
let st = match reader.read_next_borrowed(&buf[pos..]) {
|
||||
Ok(Some((_, FieldValue::U8(v)))) => v,
|
||||
_ => break,
|
||||
};
|
||||
let len = match reader.read_next(&buf[pos..]) {
|
||||
let len = match reader.read_next_borrowed(&buf[pos..]) {
|
||||
Ok(Some((_, FieldValue::U32(v)))) => v,
|
||||
_ => break,
|
||||
};
|
||||
@@ -372,10 +372,12 @@ fn hand_read_packet_stream(buf: &[u8], n: usize) -> usize {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Walk one variant's fields to exhaustion; returns bytes consumed.
|
||||
/// Uses `read_next_borrowed` — the zero-alloc hot-loop pattern for
|
||||
/// consumers that match or discard the field name.
|
||||
fn alktype_walk_variant(reader: &mut SequentialReader, buf: &[u8]) -> Option<usize> {
|
||||
reader.reset();
|
||||
loop {
|
||||
match reader.read_next(buf) {
|
||||
match reader.read_next_borrowed(buf) {
|
||||
Ok(Some((name, value))) => {
|
||||
black_box(name);
|
||||
black_box(&value);
|
||||
@@ -397,7 +399,7 @@ fn alktype_read_packet_stream(
|
||||
let mut total = 0usize;
|
||||
for _ in 0..n {
|
||||
packet.reset();
|
||||
let disc = match packet.read_next(&buf[pos..]) {
|
||||
let disc = match packet.read_next_borrowed(&buf[pos..]) {
|
||||
Ok(Some((_, FieldValue::Union {
|
||||
discriminator,
|
||||
variant_start,
|
||||
@@ -437,7 +439,7 @@ fn assert_packet_reader_parity(
|
||||
|
||||
let read_pkt = make_packet_bytes(5, &payload);
|
||||
packet.reset();
|
||||
match packet.read_next(&read_pkt) {
|
||||
match packet.read_next_borrowed(&read_pkt) {
|
||||
Ok(Some((_, FieldValue::Union {
|
||||
discriminator,
|
||||
variant_start,
|
||||
@@ -453,7 +455,7 @@ fn assert_packet_reader_parity(
|
||||
|
||||
let write_pkt = make_packet_bytes(6, &payload);
|
||||
packet.reset();
|
||||
match packet.read_next(&write_pkt) {
|
||||
match packet.read_next_borrowed(&write_pkt) {
|
||||
Ok(Some((_, FieldValue::Union {
|
||||
discriminator,
|
||||
variant_start,
|
||||
|
||||
+9
-7
@@ -46,7 +46,7 @@ pub fn materialize_packed(
|
||||
buffer: &[u8],
|
||||
) -> Result<Value, AlkTypeError> {
|
||||
let mut offset = 0usize;
|
||||
let mut obj = Map::new();
|
||||
let mut obj = Map::with_capacity(plan.fields().len());
|
||||
for field in plan.fields() {
|
||||
let value = materialize_plan_field(field, field.name(), buffer, &mut offset)?;
|
||||
obj.insert(field.name().to_string(), value);
|
||||
@@ -188,7 +188,8 @@ fn materialize_plan_primitive(
|
||||
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();
|
||||
let mut arr = Vec::with_capacity(b.len());
|
||||
arr.extend(b.iter().map(|&byte| Value::from(u32::from(byte))));
|
||||
Ok(Value::Array(arr))
|
||||
}
|
||||
other => Err(AlkTypeError::Schema(format!(
|
||||
@@ -215,7 +216,7 @@ fn materialize_plan_struct(
|
||||
buffer: &[u8],
|
||||
offset: &mut usize,
|
||||
) -> Result<Value, AlkTypeError> {
|
||||
let mut obj = Map::new();
|
||||
let mut obj = Map::with_capacity(plan.fields().len());
|
||||
for field in plan.fields() {
|
||||
let path = format!("{field_path}.{}", field.name());
|
||||
let value = materialize_plan_field(field, &path, buffer, offset)?;
|
||||
@@ -243,7 +244,7 @@ fn materialize_plan_array(
|
||||
)))
|
||||
}
|
||||
};
|
||||
let mut arr = Vec::new();
|
||||
let mut arr = Vec::with_capacity(count);
|
||||
for i in 0..count {
|
||||
let path = format!("{field_path}[{i}]");
|
||||
let before = *offset;
|
||||
@@ -309,12 +310,13 @@ fn materialize_plan_union(
|
||||
buffer: &[u8],
|
||||
offset: &mut usize,
|
||||
) -> Result<Value, AlkTypeError> {
|
||||
let (disc, shared, variants) = match body {
|
||||
let (disc, shared, variants, _) = match body {
|
||||
CompositePlan::Union {
|
||||
disc,
|
||||
shared,
|
||||
variants,
|
||||
} => (disc, shared, variants),
|
||||
int_keys: _,
|
||||
} => (disc, shared, variants, ()),
|
||||
_ => {
|
||||
return Err(AlkTypeError::Schema(format!(
|
||||
"internal: union body at {field_path} is not CompositePlan::Union"
|
||||
@@ -645,7 +647,7 @@ fn materialize_array_packed(
|
||||
) -> Result<Value, AlkTypeError> {
|
||||
let element_ty = array.element();
|
||||
let count = array.count();
|
||||
let mut arr = Vec::new();
|
||||
let mut arr = Vec::with_capacity(count);
|
||||
for i in 0..count {
|
||||
let path = format!("{field_path}[{i}]");
|
||||
let resolved_elem = doc.resolve_typeref(element_ty)?;
|
||||
|
||||
+98
-38
@@ -68,6 +68,7 @@ pub struct ReadPlan {
|
||||
endian: Endian,
|
||||
fields: Vec<FieldPlan>,
|
||||
by_name: BTreeMap<String, usize>,
|
||||
fixed_size: Option<usize>,
|
||||
schema: Arc<Value>,
|
||||
}
|
||||
|
||||
@@ -111,10 +112,18 @@ pub enum CompositePlan {
|
||||
/// variant starts immediately after the discriminator's bytes.
|
||||
/// A variant may itself be `CompositePlan::Union` (nested unions —
|
||||
/// ordinary recursion, no separate variant type).
|
||||
///
|
||||
/// `int_keys` is the byte-discriminator dispatch table: when every
|
||||
/// mapping key parses as a `u64`, it carries `(int_key,
|
||||
/// variant_index)` in `variants` order and the read loop dispatches
|
||||
/// on the raw discriminator integer without stringifying it.
|
||||
/// `None` when any key is non-numeric (or overflows `u64`) — the
|
||||
/// string-keyed fallback applies.
|
||||
Union {
|
||||
disc: DiscriminatorPlan,
|
||||
shared: Option<Box<ReadPlan>>,
|
||||
variants: Vec<(String, CompositePlan)>,
|
||||
int_keys: Option<Vec<(u64, usize)>>,
|
||||
},
|
||||
/// A fixed-count array. `element_stride` is the true stride for
|
||||
/// fixed-size elements; `0` signals variable-length elements (the
|
||||
@@ -209,6 +218,19 @@ impl ReadPlan {
|
||||
&self.schema
|
||||
}
|
||||
|
||||
/// The struct's compile-time-known byte size, or `None` when any
|
||||
/// field is variable-length (or a union/record). `Some` means a
|
||||
/// struct walk consumes exactly this many bytes regardless of
|
||||
/// buffer contents.
|
||||
///
|
||||
/// Additive in 0.3.0: computed once during [`ReadPlan::compile`],
|
||||
/// used by the packed read loop to skip size walks for fixed
|
||||
/// structs (and to skip union variant size pre-passes when the
|
||||
/// variant is fixed-size).
|
||||
pub fn fixed_size(&self) -> Option<usize> {
|
||||
self.fixed_size
|
||||
}
|
||||
|
||||
/// A stable-within-version hash of the plan (ADR-012 §1/§4).
|
||||
///
|
||||
/// Two plans with equal [`fingerprint`](Self::fingerprint)s (equal
|
||||
@@ -297,10 +319,12 @@ fn compile_struct(
|
||||
for (i, field) in fields.iter().enumerate() {
|
||||
by_name.entry(field.name().to_string()).or_insert(i);
|
||||
}
|
||||
let fixed_size = fixed_plan_size(&fields)?;
|
||||
Ok(ReadPlan {
|
||||
endian: container_endian,
|
||||
fields,
|
||||
by_name,
|
||||
fixed_size,
|
||||
schema: Arc::clone(schema),
|
||||
})
|
||||
}
|
||||
@@ -469,10 +493,12 @@ fn compile_union(
|
||||
for (i, field) in plan.iter().enumerate() {
|
||||
by_name.entry(field.name().to_string()).or_insert(i);
|
||||
}
|
||||
let fixed_size = fixed_plan_size(&plan)?;
|
||||
Some(Box::new(ReadPlan {
|
||||
endian: container_endian,
|
||||
fields: plan,
|
||||
by_name,
|
||||
fixed_size,
|
||||
schema: Arc::clone(schema),
|
||||
}))
|
||||
}
|
||||
@@ -484,13 +510,37 @@ fn compile_union(
|
||||
compile_variant(doc, variant_ty, container_endian, &variant_path, depth, seen, schema)?;
|
||||
variants.push(((*key).to_string(), body));
|
||||
}
|
||||
let int_keys = compile_int_keys(&variants, path)?;
|
||||
Ok(CompositePlan::Union {
|
||||
disc,
|
||||
shared,
|
||||
variants,
|
||||
int_keys,
|
||||
})
|
||||
}
|
||||
|
||||
/// Build the integer dispatch table for a byte-discriminator union:
|
||||
/// `(parsed_key, variant_index)` pairs in `variants` order, or `None`
|
||||
/// when any key is non-numeric (the string fallback applies). Keys are
|
||||
/// validated at parse time as stringified integers; a `u64` overflow
|
||||
/// means the key cannot equal a discriminator read from at most 4
|
||||
/// bytes, so such a table is simply not built (the string path also
|
||||
/// handles it correctly — it would never match, erroring at read time
|
||||
/// exactly as before).
|
||||
fn compile_int_keys(
|
||||
variants: &[(String, CompositePlan)],
|
||||
_path: &str,
|
||||
) -> Result<Option<Vec<(u64, usize)>>, AlkTypeError> {
|
||||
let mut out = Vec::with_capacity(variants.len());
|
||||
for (i, (key, _)) in variants.iter().enumerate() {
|
||||
match key.parse::<u64>() {
|
||||
Ok(v) => out.push((v, i)),
|
||||
Err(_) => return Ok(None),
|
||||
}
|
||||
}
|
||||
Ok(Some(out))
|
||||
}
|
||||
|
||||
/// Compile one union mapping entry. A variant must be a struct or a
|
||||
/// union (mirroring the 0.2.0 read loop's `resolve_and_walk_variant`);
|
||||
/// a `$ref` variant resolves through the cycle set.
|
||||
@@ -640,10 +690,12 @@ fn wrap_leaf(
|
||||
// is `Value::Null` by construction. It never escapes —
|
||||
// `materialize_plan_composite` unwraps it and the
|
||||
// public `schema()` accessor returns the root plan's.
|
||||
let fixed_size = fixed_plan_size(std::slice::from_ref(&field))?;
|
||||
Ok(CompositePlan::Struct(ReadPlan {
|
||||
endian: container_endian,
|
||||
fields: vec![field],
|
||||
by_name: BTreeMap::new(),
|
||||
fixed_size,
|
||||
schema: Arc::new(Value::Null),
|
||||
}))
|
||||
}
|
||||
@@ -654,45 +706,15 @@ fn wrap_leaf(
|
||||
}
|
||||
}
|
||||
|
||||
/// The compile-time-known byte size of a composite node, or `None` when
|
||||
/// variable-length. Used for array strides: primitives and enums
|
||||
/// contribute their fixed size; structs sum their fields; nested arrays
|
||||
/// with fixed elements contribute `count × stride`; unions, records,
|
||||
/// and variable-length primitives make the whole node variable.
|
||||
///
|
||||
/// Returns `Err` only on arithmetic overflow while summing struct field
|
||||
/// sizes or array products — after the array caps ([`MAX_ARRAY_BYTES`],
|
||||
/// [`MAX_ARRAY_ELEMENTS`]) no honest schema can reach those arms, and
|
||||
/// treating overflow as "variable-length" (the pre-0.3.1
|
||||
/// `unwrap_or_default` behavior) would silently mis-plan the wire.
|
||||
fn fixed_composite_size(body: &CompositePlan) -> Result<Option<usize>, AlkTypeError> {
|
||||
match body {
|
||||
CompositePlan::Struct(plan) => fixed_plan_size(plan),
|
||||
CompositePlan::Union { .. } | CompositePlan::Record { .. } => Ok(None),
|
||||
CompositePlan::Array {
|
||||
count,
|
||||
element_stride,
|
||||
..
|
||||
} => {
|
||||
if *element_stride == 0 {
|
||||
Ok(None)
|
||||
} else {
|
||||
match count.checked_mul(*element_stride) {
|
||||
Some(total) => Ok(Some(total)),
|
||||
None => Err(AlkTypeError::Schema(
|
||||
"internal: array size product overflowed while sizing a fixed-stride \
|
||||
composite (array caps should have rejected this schema earlier)"
|
||||
.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fixed_plan_size(plan: &ReadPlan) -> Result<Option<usize>, AlkTypeError> {
|
||||
/// The shared compile-time size computation behind
|
||||
/// [`ReadPlan::fixed_size`] and array-stride sizing: sum the fields'
|
||||
/// fixed byte sizes, returning `None` when any field is
|
||||
/// variable-length (or a union/record, which are variable by
|
||||
/// construction). `Err` only on arithmetic overflow, which the array
|
||||
/// compile-time caps make unreachable for honest schemas.
|
||||
fn fixed_plan_size(fields: &[FieldPlan]) -> Result<Option<usize>, AlkTypeError> {
|
||||
let mut total = 0usize;
|
||||
for field in plan.fields() {
|
||||
for field in fields {
|
||||
let size = match field.kind() {
|
||||
ReadKind::Primitive(k) if k.is_fixed_size() => {
|
||||
k.type_size().ok_or_else(|| {
|
||||
@@ -730,6 +752,42 @@ fn fixed_plan_size(plan: &ReadPlan) -> Result<Option<usize>, AlkTypeError> {
|
||||
Ok(Some(total))
|
||||
}
|
||||
|
||||
/// The compile-time-known byte size of a composite node, or `None` when
|
||||
/// variable-length. Used for array strides: primitives and enums
|
||||
/// contribute their fixed size; structs sum their fields; nested arrays
|
||||
/// with fixed elements contribute `count × stride`; unions, records,
|
||||
/// and variable-length primitives make the whole node variable.
|
||||
///
|
||||
/// Returns `Err` only on arithmetic overflow while summing struct field
|
||||
/// sizes or array products — after the array caps ([`MAX_ARRAY_BYTES`],
|
||||
/// [`MAX_ARRAY_ELEMENTS`]) no honest schema can reach those arms, and
|
||||
/// treating overflow as "variable-length" (the pre-0.3.1
|
||||
/// `unwrap_or_default` behavior) would silently mis-plan the wire.
|
||||
fn fixed_composite_size(body: &CompositePlan) -> Result<Option<usize>, AlkTypeError> {
|
||||
match body {
|
||||
CompositePlan::Struct(plan) => Ok(plan.fixed_size),
|
||||
CompositePlan::Union { .. } | CompositePlan::Record { .. } => Ok(None),
|
||||
CompositePlan::Array {
|
||||
count,
|
||||
element_stride,
|
||||
..
|
||||
} => {
|
||||
if *element_stride == 0 {
|
||||
Ok(None)
|
||||
} else {
|
||||
match count.checked_mul(*element_stride) {
|
||||
Some(total) => Ok(Some(total)),
|
||||
None => Err(AlkTypeError::Schema(
|
||||
"internal: array size product overflowed while sizing a fixed-stride \
|
||||
composite (array caps should have rejected this schema earlier)"
|
||||
.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -850,6 +908,7 @@ mod tests {
|
||||
disc,
|
||||
shared,
|
||||
variants,
|
||||
..
|
||||
} => {
|
||||
assert!(matches!(
|
||||
disc,
|
||||
@@ -886,6 +945,7 @@ mod tests {
|
||||
disc,
|
||||
shared,
|
||||
variants,
|
||||
..
|
||||
} => {
|
||||
assert!(matches!(
|
||||
disc,
|
||||
|
||||
+169
-21
@@ -179,10 +179,43 @@ impl SequentialReader {
|
||||
&mut self,
|
||||
buffer: &'a [u8],
|
||||
) -> Result<Option<(String, FieldValue<'a>)>, AlkTypeError> {
|
||||
match self.read_next_borrowed(buffer)? {
|
||||
Some((name, value)) => Ok(Some((name.to_string(), value))),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the next field from `buffer` at the current position,
|
||||
/// returning the field name **borrowed from the compiled plan**
|
||||
/// instead of a freshly allocated `String`.
|
||||
///
|
||||
/// Zero-allocation on the happy path — for hot stream-parsing loops
|
||||
/// where the field name is matched or discarded, this avoids one
|
||||
/// heap allocation and free per field. The name's lifetime is tied
|
||||
/// to this reader (the name lives in the shared [`ReadPlan`], which
|
||||
/// the reader holds via `Arc`), not to `buffer`.
|
||||
///
|
||||
/// The value's behavior is identical to [`Self::read_next`]:
|
||||
/// `Ok(Some((field_name, value)))` with the internal position
|
||||
/// advanced, or `Ok(None)` when all fields have been read.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Propagates [`AlkTypeError::Access`] from the underlying
|
||||
/// [`crate::data_access`] reads when `buffer` is too short or
|
||||
/// contains invalid data.
|
||||
///
|
||||
/// Additive in 0.3.0 (perf review): [`Self::read_next`] remains the
|
||||
/// stable owned-name form; this method is the allocation-free
|
||||
/// alternative for consumers that do not need an owned name.
|
||||
pub fn read_next_borrowed<'p, 'a>(
|
||||
&'p mut self,
|
||||
buffer: &'a [u8],
|
||||
) -> Result<Option<(&'p str, FieldValue<'a>)>, AlkTypeError> {
|
||||
if self.field_index >= self.plan.fields().len() {
|
||||
return Ok(None);
|
||||
}
|
||||
let name = self.plan.fields()[self.field_index].name().to_string();
|
||||
let name = self.plan.fields()[self.field_index].name();
|
||||
let (value, new_position) =
|
||||
self.read_field_at(buffer, self.field_index, self.position)?;
|
||||
self.position = new_position;
|
||||
@@ -297,7 +330,13 @@ fn plan_read_field_at<'a>(
|
||||
)))
|
||||
}
|
||||
};
|
||||
let size = plan_walk_struct_size(body, buffer, offset, field_path)?;
|
||||
let size = match body.fixed_size() {
|
||||
Some(size) => {
|
||||
plan_bounds_check(buffer, offset, size, field_path)?;
|
||||
size
|
||||
}
|
||||
None => plan_walk_struct_size(body, buffer, offset, field_path)?,
|
||||
};
|
||||
let end = checked_end(offset, size, field_path, "struct")?;
|
||||
Ok((FieldValue::Struct { start: offset, end }, end))
|
||||
}
|
||||
@@ -427,6 +466,12 @@ fn checked_end(
|
||||
/// Walk a struct plan's fields sequentially from `offset`, returning
|
||||
/// the total byte size. Only advances the cursor (reads length
|
||||
/// prefixes); does not collect values.
|
||||
///
|
||||
/// Field paths are assembled only on the error path: the per-field
|
||||
/// `format!` of the interpretive walk dominated hot loops, but the walk
|
||||
/// itself only needs a path when a check fails, so each field carries
|
||||
/// its plan-compiled name and the enclosing `field_path` is joined
|
||||
/// lazily.
|
||||
fn plan_walk_struct_size(
|
||||
plan: &ReadPlan,
|
||||
buffer: &[u8],
|
||||
@@ -435,10 +480,10 @@ fn plan_walk_struct_size(
|
||||
) -> Result<usize, AlkTypeError> {
|
||||
let mut position = offset;
|
||||
for field in plan.fields() {
|
||||
let sub_path = format!("{field_path}.{}", field.name());
|
||||
let (_, new_position) =
|
||||
plan_read_kind(field, buffer, position, &sub_path)?;
|
||||
plan_read_kind(field, buffer, position, field_path)?;
|
||||
if new_position < position {
|
||||
let sub_path = format!("{field_path}.{}", field.name());
|
||||
return Err(AlkTypeError::Access {
|
||||
field_path: sub_path,
|
||||
reason: format!("struct field walked backwards: {position} → {new_position}"),
|
||||
@@ -475,7 +520,13 @@ fn plan_read_kind<'a>(
|
||||
)))
|
||||
}
|
||||
};
|
||||
let size = plan_walk_struct_size(body, buffer, offset, field_path)?;
|
||||
let size = match body.fixed_size() {
|
||||
Some(size) => {
|
||||
plan_bounds_check(buffer, offset, size, field_path)?;
|
||||
size
|
||||
}
|
||||
None => plan_walk_struct_size(body, buffer, offset, field_path)?,
|
||||
};
|
||||
let end = checked_end(offset, size, field_path, "struct")?;
|
||||
Ok((FieldValue::Struct { start: offset, end }, end))
|
||||
}
|
||||
@@ -523,12 +574,13 @@ fn plan_read_union<'a>(
|
||||
field_path: &str,
|
||||
endian: Endian,
|
||||
) -> Result<(FieldValue<'a>, usize), AlkTypeError> {
|
||||
let (disc, shared, variants) = match body {
|
||||
let (disc, shared, variants, int_keys) = match body {
|
||||
CompositePlan::Union {
|
||||
disc,
|
||||
shared,
|
||||
variants,
|
||||
} => (disc, shared, variants),
|
||||
int_keys,
|
||||
} => (disc, shared, variants, int_keys),
|
||||
_ => {
|
||||
return Err(AlkTypeError::Schema(format!(
|
||||
"internal: union body at {field_path} is not CompositePlan::Union"
|
||||
@@ -543,17 +595,60 @@ fn plan_read_union<'a>(
|
||||
let abs_offset = checked_end(offset, *disc_offset, field_path, "discriminator")?;
|
||||
let (disc_value, disc_size) =
|
||||
plan_read_byte_discriminator(buffer, abs_offset, field_path, *disc_type, endian)?;
|
||||
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!("unknown union discriminator value: {key}"),
|
||||
})?;
|
||||
// Integer dispatch when the mapping is fully numeric: match
|
||||
// the raw discriminator integer against the pre-parsed
|
||||
// compile-time keys (no per-read stringify, no string
|
||||
// compares). The returned `FieldValue::Union` key stays a
|
||||
// `String` — cloned from the matched mapping key, which for
|
||||
// numeric keys is the same string the old
|
||||
// `disc_value.to_string()` produced.
|
||||
let (key, variant) = match int_keys {
|
||||
Some(table) => {
|
||||
let idx = table
|
||||
.iter()
|
||||
.find(|(k, _)| *k == u64::from(disc_value))
|
||||
.map(|&(_, i)| i)
|
||||
.ok_or_else(|| AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: format!("unknown union discriminator value: {disc_value}"),
|
||||
})?;
|
||||
(variants[idx].0.clone(), &variants[idx].1)
|
||||
}
|
||||
None => {
|
||||
// String-keyed fallback: find the variant first,
|
||||
// then clone the matched mapping key for the
|
||||
// returned value (no leak, no borrow tangle).
|
||||
let key_str = disc_value.to_string();
|
||||
let idx = variants
|
||||
.iter()
|
||||
.position(|(k, _)| *k == key_str)
|
||||
.ok_or_else(|| AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: format!("unknown union discriminator value: {key_str}"),
|
||||
})?;
|
||||
(variants[idx].0.clone(), &variants[idx].1)
|
||||
}
|
||||
};
|
||||
let variant_start = checked_end(abs_offset, disc_size, field_path, "variant")?;
|
||||
let variant_size = plan_walk_variant_size(variant, buffer, variant_start, field_path, endian)?;
|
||||
let variant_size = match plan_variant_fixed_size(variant) {
|
||||
Some(size) => {
|
||||
let end = checked_end(variant_start, size, field_path, "union")?;
|
||||
if buffer.len() < end {
|
||||
return Err(AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: format!(
|
||||
"union variant needs bytes [0..{size}) from variant start \
|
||||
{variant_start}, buffer has {} remaining",
|
||||
buffer.len().saturating_sub(variant_start),
|
||||
),
|
||||
});
|
||||
}
|
||||
size
|
||||
}
|
||||
None => {
|
||||
plan_walk_variant_size(variant, buffer, variant_start, field_path, endian)?
|
||||
}
|
||||
};
|
||||
let end = checked_end(variant_start, variant_size, field_path, "union")?;
|
||||
Ok((
|
||||
FieldValue::Union {
|
||||
@@ -589,9 +684,9 @@ fn plan_read_union<'a>(
|
||||
// shared fields); the variant starts after all of it.
|
||||
let mut position = offset;
|
||||
for field in shared_plan.fields() {
|
||||
let sub_path = format!("{field_path}.{}", field.name());
|
||||
let (_, new_position) = plan_read_kind(field, buffer, position, &sub_path)?;
|
||||
let (_, new_position) = plan_read_kind(field, buffer, position, field_path)?;
|
||||
if new_position < position {
|
||||
let sub_path = format!("{field_path}.{}", field.name());
|
||||
return Err(AlkTypeError::Access {
|
||||
field_path: sub_path,
|
||||
reason: format!(
|
||||
@@ -601,8 +696,25 @@ fn plan_read_union<'a>(
|
||||
}
|
||||
position = new_position;
|
||||
}
|
||||
let variant_size =
|
||||
plan_walk_variant_size(variant, buffer, position, field_path, endian)?;
|
||||
let variant_size = match plan_variant_fixed_size(variant) {
|
||||
Some(size) => {
|
||||
let end = checked_end(position, size, field_path, "union")?;
|
||||
if buffer.len() < end {
|
||||
return Err(AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: format!(
|
||||
"union variant needs bytes [0..{size}) from variant start \
|
||||
{position}, buffer has {} remaining",
|
||||
buffer.len().saturating_sub(position),
|
||||
),
|
||||
});
|
||||
}
|
||||
size
|
||||
}
|
||||
None => {
|
||||
plan_walk_variant_size(variant, buffer, position, field_path, endian)?
|
||||
}
|
||||
};
|
||||
let end = checked_end(position, variant_size, field_path, "union")?;
|
||||
Ok((
|
||||
FieldValue::Union {
|
||||
@@ -688,6 +800,42 @@ fn plan_read_byte_discriminator(
|
||||
}
|
||||
}
|
||||
|
||||
/// A compiled struct variant's compile-time-known byte size, if any.
|
||||
/// Union variants are variable by construction (their size depends on
|
||||
/// the nested discriminator); the compile step already rejects
|
||||
/// non-struct/union variant bodies, so the catch-all arm is
|
||||
/// unreachable in practice but keeps the match total.
|
||||
fn plan_variant_fixed_size(variant: &CompositePlan) -> Option<usize> {
|
||||
match variant {
|
||||
CompositePlan::Struct(p) => p.fixed_size(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_bounds_check(
|
||||
buffer: &[u8],
|
||||
offset: usize,
|
||||
size: usize,
|
||||
field_path: &str,
|
||||
) -> Result<(), AlkTypeError> {
|
||||
let end = offset
|
||||
.checked_add(size)
|
||||
.ok_or_else(|| AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: format!("struct end {offset} + {size} overflows usize"),
|
||||
})?;
|
||||
if buffer.len() < end {
|
||||
return Err(AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: format!(
|
||||
"struct at {offset} needs {size} bytes, buffer has {} remaining",
|
||||
buffer.len().saturating_sub(offset),
|
||||
),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolve a compiled variant body to its byte size starting at
|
||||
/// `variant_start`. Struct variants walk their field list; union
|
||||
/// variants (nested unions) read their discriminator and advance.
|
||||
|
||||
Reference in new issue
Block a user