Compare commits
1
Commits
main
...
readplan-poc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7c71da9e5 |
No files matched your search
Generated
+8
@@ -547,6 +547,14 @@ version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "readplan-poc"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"alktype",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
|
||||
+4
-1
@@ -19,4 +19,7 @@ default = []
|
||||
|
||||
[dependencies]
|
||||
jsonschema = { version = "0.46", default-features = false }
|
||||
serde_json = { version = "1", features = ["preserve_order"] }
|
||||
serde_json = { version = "1", features = ["preserve_order"] }
|
||||
|
||||
[workspace]
|
||||
members = ["poc/readplan"]
|
||||
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "readplan-poc"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
authors = ["poc"]
|
||||
|
||||
[dependencies]
|
||||
alktype = { path = "../.." }
|
||||
serde_json = { version = "1", features = ["preserve_order"] }
|
||||
|
||||
[lib]
|
||||
name = "readplan_poc"
|
||||
@@ -0,0 +1,156 @@
|
||||
# ReadPlan POC — findings
|
||||
|
||||
Branch: `readplan-poc`
|
||||
ADR: [ADR-011](../../docs/architecture/decisions/011-compiled-read-plan-for-packed-mode.md)
|
||||
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 `ReadPlan`s 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`:
|
||||
|
||||
```rust
|
||||
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.
|
||||
@@ -0,0 +1,733 @@
|
||||
//! ReadPlan POC — derisking ADR-011 before implementation.
|
||||
//!
|
||||
//! Lives on branch `readplan-poc`. Not production code. The objective is
|
||||
//! to answer two questions, not to be pretty:
|
||||
//!
|
||||
//! 1. Does the `ReadPlan`/`CompositePlan`/`ReadKind`/`DiscriminatorPlan`
|
||||
//! shape from ADR-011 cover *every* `BastType` arm in the current read
|
||||
//! loop (`sequential_reader.rs:303-381`) and the parallel arms in
|
||||
//! `materialize.rs`?
|
||||
//! 2. Does a plan-driven read loop produce identical `(FieldValue, position)`
|
||||
//! results to the existing `SequentialReader::read_field_at` across a
|
||||
//! battery of schemas covering every arm?
|
||||
//!
|
||||
//! If both answers are yes, ADR-011's shape is confirmed and the
|
||||
//! implementation can proceed by replacing the `BastDoc` walk in
|
||||
//! `sequential_reader.rs` and `materialize.rs` with the plan walk.
|
||||
//!
|
||||
//! What this POC is *not*:
|
||||
//! - It is not the production `ReadPlan`. The production version lives
|
||||
//! in `src/` and is unit-tested against the BAST fixtures there.
|
||||
//! - It is not performance-tuned. The bench that matters lives in
|
||||
//! alktty's `wire_vs_bast.rs`; this POC only proves correctness/coverage.
|
||||
//! - It does not handle aligned mode, validation, or the write side —
|
||||
//! ADR-011 scopes those out.
|
||||
|
||||
use alktype::bast::{
|
||||
BastArray, BastDefKind, BastDiscriminator, BastDoc, BastField, BastRecord, BastStruct,
|
||||
BastType, BastUnion,
|
||||
};
|
||||
use alktype::data_access;
|
||||
use alktype::error::AlkTypeError;
|
||||
use alktype::schema::{AlkTypeKind, Endian, VariableEncoding};
|
||||
use alktype::sequential_reader::FieldValue;
|
||||
use serde_json::Value;
|
||||
|
||||
const U32_SIZE: usize = 4;
|
||||
|
||||
pub struct ReadPlan {
|
||||
endian: Endian,
|
||||
fields: Vec<FieldPlan>,
|
||||
by_name: std::collections::HashMap<String, usize>,
|
||||
}
|
||||
|
||||
pub struct FieldPlan {
|
||||
name: String,
|
||||
kind: ReadKind,
|
||||
endian: Endian,
|
||||
encoding: VariableEncoding,
|
||||
max_length: Option<usize>,
|
||||
body: Option<CompositePlan>,
|
||||
}
|
||||
|
||||
pub enum ReadKind {
|
||||
Primitive(AlkTypeKind),
|
||||
Enum,
|
||||
Struct,
|
||||
Union,
|
||||
Array,
|
||||
Record,
|
||||
}
|
||||
|
||||
pub enum CompositePlan {
|
||||
Struct(ReadPlan),
|
||||
Union {
|
||||
disc: DiscriminatorPlan,
|
||||
variants: Vec<(String, VariantPlan)>,
|
||||
},
|
||||
Array {
|
||||
element: Box<CompositePlan>,
|
||||
count: usize,
|
||||
element_stride: usize,
|
||||
},
|
||||
Record {
|
||||
value: Box<CompositePlan>,
|
||||
},
|
||||
}
|
||||
|
||||
pub enum DiscriminatorPlan {
|
||||
Byte { offset: usize, disc_type: AlkTypeKind },
|
||||
Field { name: String, field_index: usize },
|
||||
}
|
||||
|
||||
pub struct VariantPlan {
|
||||
kind: VariantKind,
|
||||
plan: ReadPlan,
|
||||
}
|
||||
|
||||
pub enum VariantKind {
|
||||
Struct,
|
||||
Union,
|
||||
}
|
||||
|
||||
impl ReadPlan {
|
||||
pub fn compile(bast_doc: &Value, root_name: &str) -> Result<Self, AlkTypeError> {
|
||||
let doc = BastDoc::new(bast_doc, root_name)?;
|
||||
let root_def = doc.root_def();
|
||||
let struct_node = match root_def.kind() {
|
||||
BastDefKind::Struct(s) => s,
|
||||
other => {
|
||||
return Err(AlkTypeError::Schema(format!(
|
||||
"ReadPlan root must be a struct, got {kind}",
|
||||
kind = other.alk_kind()
|
||||
)));
|
||||
}
|
||||
};
|
||||
let plan = Self::compile_struct(&doc, struct_node, struct_node.endian())?;
|
||||
Ok(plan)
|
||||
}
|
||||
|
||||
fn compile_struct(
|
||||
doc: &BastDoc<'_>,
|
||||
struct_node: &BastStruct<'_>,
|
||||
container_endian: Endian,
|
||||
) -> Result<Self, AlkTypeError> {
|
||||
let mut fields = Vec::new();
|
||||
let mut by_name = std::collections::HashMap::new();
|
||||
for (i, field) in struct_node.fields().iter().enumerate() {
|
||||
let field_plan = Self::compile_field(doc, field, container_endian)?;
|
||||
by_name.insert(field_plan.name.clone(), i);
|
||||
fields.push(field_plan);
|
||||
}
|
||||
Ok(Self {
|
||||
endian: container_endian,
|
||||
fields,
|
||||
by_name,
|
||||
})
|
||||
}
|
||||
|
||||
fn compile_field(
|
||||
doc: &BastDoc<'_>,
|
||||
field: &BastField<'_>,
|
||||
container_endian: Endian,
|
||||
) -> Result<FieldPlan, AlkTypeError> {
|
||||
let endian = field.effective_endian(container_endian);
|
||||
let ty = field.ty();
|
||||
let resolved = doc.resolve_typeref(ty)?;
|
||||
let (kind, body) = Self::compile_kind(doc, &resolved, endian)?;
|
||||
Ok(FieldPlan {
|
||||
name: field.name().to_string(),
|
||||
kind,
|
||||
endian,
|
||||
encoding: field.encoding(),
|
||||
max_length: field.max_length(),
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
fn compile_kind(
|
||||
doc: &BastDoc<'_>,
|
||||
ty: &BastType<'_>,
|
||||
container_endian: Endian,
|
||||
) -> Result<(ReadKind, Option<CompositePlan>), AlkTypeError> {
|
||||
match ty {
|
||||
BastType::Primitive(k) => Ok((ReadKind::Primitive(*k), None)),
|
||||
BastType::Enum(_) => Ok((ReadKind::Enum, None)),
|
||||
BastType::Struct(s) => {
|
||||
let plan = Self::compile_struct(doc, s, s.endian())?;
|
||||
Ok((ReadKind::Struct, Some(CompositePlan::Struct(plan))))
|
||||
}
|
||||
BastType::Union(u) => {
|
||||
let plan = Self::compile_union(doc, u)?;
|
||||
Ok((ReadKind::Union, Some(plan)))
|
||||
}
|
||||
BastType::Array(a) => {
|
||||
let plan = Self::compile_array(doc, a, container_endian)?;
|
||||
Ok((ReadKind::Array, Some(plan)))
|
||||
}
|
||||
BastType::Record(r) => {
|
||||
let plan = Self::compile_record(doc, r, container_endian)?;
|
||||
Ok((ReadKind::Record, Some(plan)))
|
||||
}
|
||||
BastType::Ref(_) => Err(AlkTypeError::Schema(
|
||||
"internal: compile_kind saw an unresolved $ref".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn compile_union(doc: &BastDoc<'_>, u: &BastUnion<'_>) -> Result<CompositePlan, AlkTypeError> {
|
||||
let disc = match u.discriminator() {
|
||||
BastDiscriminator::Byte { offset, disc_type } => DiscriminatorPlan::Byte {
|
||||
offset: *offset,
|
||||
disc_type: *disc_type,
|
||||
},
|
||||
BastDiscriminator::Field { name } => {
|
||||
let idx = u
|
||||
.fields()
|
||||
.iter()
|
||||
.position(|f| f.name() == *name)
|
||||
.ok_or_else(|| {
|
||||
AlkTypeError::Schema(format!(
|
||||
"union has no discriminator field '{name}'"
|
||||
))
|
||||
})?;
|
||||
DiscriminatorPlan::Field {
|
||||
name: name.to_string(),
|
||||
field_index: idx,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut variants = Vec::new();
|
||||
for (key, variant_ty) in u.mapping() {
|
||||
let variant_def = doc.resolve_typeref_as_def(variant_ty, "")?;
|
||||
let kind = match variant_def.kind() {
|
||||
BastDefKind::Struct(s) => {
|
||||
let plan = Self::compile_struct(doc, s, s.endian())?;
|
||||
(VariantKind::Struct, plan)
|
||||
}
|
||||
BastDefKind::Union(inner_u) => {
|
||||
let _ = inner_u;
|
||||
return Err(AlkTypeError::Schema(format!(
|
||||
"union variant '{key}' is itself a union — nested unions not yet supported by ReadPlan compile"
|
||||
)));
|
||||
}
|
||||
BastDefKind::Enum(_) => {
|
||||
return Err(AlkTypeError::Schema(format!(
|
||||
"union variant '{key}' is an enum — variants must be struct or union"
|
||||
)));
|
||||
}
|
||||
};
|
||||
variants.push((key.to_string(), VariantPlan { kind: kind.0, plan: kind.1 }));
|
||||
}
|
||||
|
||||
Ok(CompositePlan::Union { disc, variants })
|
||||
}
|
||||
|
||||
fn compile_array(
|
||||
doc: &BastDoc<'_>,
|
||||
a: &BastArray<'_>,
|
||||
container_endian: Endian,
|
||||
) -> Result<CompositePlan, AlkTypeError> {
|
||||
let element_ty = a.element();
|
||||
let resolved_elem = doc.resolve_typeref(element_ty)?;
|
||||
let count = a.count();
|
||||
|
||||
let element_stride = match &resolved_elem {
|
||||
BastType::Struct(s) => Self::fixed_struct_size(doc, s)?,
|
||||
BastType::Array(inner_a) => {
|
||||
let inner_stride = match inner_a.element() {
|
||||
e if doc.resolve_typeref(e)?.alk_kind().is_fixed_size() => {
|
||||
doc.resolve_typeref(e)?.alk_kind().type_size().unwrap_or(0)
|
||||
}
|
||||
_ => 0,
|
||||
};
|
||||
if inner_stride == 0 {
|
||||
0
|
||||
} else {
|
||||
inner_a
|
||||
.count()
|
||||
.checked_mul(inner_stride)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
}
|
||||
BastType::Primitive(k) if k.is_fixed_size() => k.type_size().unwrap_or(0),
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
let element = Self::wrap_kind_as_composite_body(
|
||||
Self::compile_kind(doc, &resolved_elem, container_endian)?,
|
||||
container_endian,
|
||||
)?;
|
||||
|
||||
Ok(CompositePlan::Array {
|
||||
element: Box::new(element),
|
||||
count,
|
||||
element_stride,
|
||||
})
|
||||
}
|
||||
|
||||
fn fixed_struct_size(doc: &BastDoc<'_>, s: &BastStruct<'_>) -> Result<usize, AlkTypeError> {
|
||||
let mut total = 0usize;
|
||||
for f in s.fields() {
|
||||
let ty = f.ty();
|
||||
let resolved = doc.resolve_typeref(ty)?;
|
||||
let k = resolved.alk_kind();
|
||||
if k.is_fixed_size() {
|
||||
total = total
|
||||
.checked_add(k.type_size().unwrap_or(0))
|
||||
.ok_or_else(|| AlkTypeError::Schema(format!("fixed struct size overflow")))?;
|
||||
} else {
|
||||
return Ok(0);
|
||||
}
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn wrap_kind_as_composite_body(
|
||||
compiled: (ReadKind, Option<CompositePlan>),
|
||||
container_endian: Endian,
|
||||
) -> Result<CompositePlan, AlkTypeError> {
|
||||
match compiled {
|
||||
(_, Some(body)) => Ok(body),
|
||||
(kind @ (ReadKind::Primitive(_) | ReadKind::Enum), None) => {
|
||||
let field = FieldPlan {
|
||||
name: String::new(),
|
||||
kind,
|
||||
endian: container_endian,
|
||||
encoding: VariableEncoding::LengthPrefixed,
|
||||
max_length: None,
|
||||
body: None,
|
||||
};
|
||||
Ok(CompositePlan::Struct(ReadPlan {
|
||||
endian: container_endian,
|
||||
fields: vec![field],
|
||||
by_name: std::collections::HashMap::new(),
|
||||
}))
|
||||
}
|
||||
(ReadKind::Struct | ReadKind::Union | ReadKind::Array | ReadKind::Record, None) => {
|
||||
Err(AlkTypeError::Schema(
|
||||
"composite ReadKind should have produced a body".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn compile_record(
|
||||
doc: &BastDoc<'_>,
|
||||
r: &BastRecord<'_>,
|
||||
container_endian: Endian,
|
||||
) -> Result<CompositePlan, AlkTypeError> {
|
||||
let values_ty = r.values();
|
||||
let resolved = doc.resolve_typeref(values_ty)?;
|
||||
let compiled = Self::compile_kind(doc, &resolved, container_endian)?;
|
||||
let value = Box::new(Self::wrap_kind_as_composite_body(compiled, container_endian)?);
|
||||
Ok(CompositePlan::Record { value })
|
||||
}
|
||||
|
||||
pub fn endian(&self) -> Endian {
|
||||
self.endian
|
||||
}
|
||||
|
||||
pub fn fields(&self) -> &[FieldPlan] {
|
||||
&self.fields
|
||||
}
|
||||
|
||||
pub fn field_index(&self, name: &str) -> Option<usize> {
|
||||
self.by_name.get(name).copied()
|
||||
}
|
||||
}
|
||||
|
||||
impl FieldPlan {
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
pub fn kind(&self) -> &ReadKind {
|
||||
&self.kind
|
||||
}
|
||||
pub fn endian(&self) -> Endian {
|
||||
self.endian
|
||||
}
|
||||
pub fn body(&self) -> Option<&CompositePlan> {
|
||||
self.body.as_ref()
|
||||
}
|
||||
pub fn encoding(&self) -> VariableEncoding {
|
||||
self.encoding
|
||||
}
|
||||
pub fn max_length(&self) -> Option<usize> {
|
||||
self.max_length
|
||||
}
|
||||
}
|
||||
|
||||
impl VariantPlan {
|
||||
pub fn kind(&self) -> &VariantKind {
|
||||
&self.kind
|
||||
}
|
||||
pub fn plan(&self) -> &ReadPlan {
|
||||
&self.plan
|
||||
}
|
||||
}
|
||||
|
||||
pub fn plan_read_field_at<'a>(
|
||||
plan: &ReadPlan,
|
||||
field_index: usize,
|
||||
offset: usize,
|
||||
buffer: &'a [u8],
|
||||
field_path: &str,
|
||||
) -> Result<(FieldValue<'a>, usize), AlkTypeError> {
|
||||
let field = plan
|
||||
.fields()
|
||||
.get(field_index)
|
||||
.ok_or_else(|| AlkTypeError::Schema(format!("field index {field_index} out of range")))?;
|
||||
plan_read_kind(
|
||||
field.kind(),
|
||||
field.body(),
|
||||
field.endian(),
|
||||
offset,
|
||||
buffer,
|
||||
field_path,
|
||||
)
|
||||
}
|
||||
|
||||
fn plan_read_kind<'a>(
|
||||
kind: &ReadKind,
|
||||
body: Option<&CompositePlan>,
|
||||
endian: Endian,
|
||||
offset: usize,
|
||||
buffer: &'a [u8],
|
||||
field_path: &str,
|
||||
) -> Result<(FieldValue<'a>, usize), AlkTypeError> {
|
||||
match kind {
|
||||
ReadKind::Primitive(AlkTypeKind::Int8) => {
|
||||
let v = data_access::read_i8(buffer, offset, field_path)?;
|
||||
Ok((FieldValue::I8(v), offset + 1))
|
||||
}
|
||||
ReadKind::Primitive(AlkTypeKind::Int16) => {
|
||||
let v = data_access::read_i16(buffer, offset, field_path, endian)?;
|
||||
Ok((FieldValue::I16(v), offset + 2))
|
||||
}
|
||||
ReadKind::Primitive(AlkTypeKind::Int32) => {
|
||||
let v = data_access::read_i32(buffer, offset, field_path, endian)?;
|
||||
Ok((FieldValue::I32(v), offset + 4))
|
||||
}
|
||||
ReadKind::Primitive(AlkTypeKind::Int64) => {
|
||||
let v = data_access::read_i64(buffer, offset, field_path, endian)?;
|
||||
Ok((FieldValue::I64(v), offset + 8))
|
||||
}
|
||||
ReadKind::Primitive(AlkTypeKind::Uint8) => {
|
||||
let v = data_access::read_u8(buffer, offset, field_path)?;
|
||||
Ok((FieldValue::U8(v), offset + 1))
|
||||
}
|
||||
ReadKind::Primitive(AlkTypeKind::Uint16) => {
|
||||
let v = data_access::read_u16(buffer, offset, field_path, endian)?;
|
||||
Ok((FieldValue::U16(v), offset + 2))
|
||||
}
|
||||
ReadKind::Primitive(AlkTypeKind::Uint32) => {
|
||||
let v = data_access::read_u32(buffer, offset, field_path, endian)?;
|
||||
Ok((FieldValue::U32(v), offset + 4))
|
||||
}
|
||||
ReadKind::Primitive(AlkTypeKind::Uint64) => {
|
||||
let v = data_access::read_u64(buffer, offset, field_path, endian)?;
|
||||
Ok((FieldValue::U64(v), offset + 8))
|
||||
}
|
||||
ReadKind::Primitive(AlkTypeKind::Float32) => {
|
||||
let v = data_access::read_f32(buffer, offset, field_path, endian)?;
|
||||
Ok((FieldValue::F32(v), offset + 4))
|
||||
}
|
||||
ReadKind::Primitive(AlkTypeKind::Float64) => {
|
||||
let v = data_access::read_f64(buffer, offset, field_path, endian)?;
|
||||
Ok((FieldValue::F64(v), offset + 8))
|
||||
}
|
||||
ReadKind::Primitive(AlkTypeKind::Boolean) => {
|
||||
let v = data_access::read_bool(buffer, offset, field_path)?;
|
||||
Ok((FieldValue::Bool(v), offset + 1))
|
||||
}
|
||||
ReadKind::Primitive(AlkTypeKind::String) => {
|
||||
let s = data_access::read_string(buffer, offset, field_path, endian)?;
|
||||
let total = U32_SIZE + s.len();
|
||||
Ok((FieldValue::String(s), offset + total))
|
||||
}
|
||||
ReadKind::Primitive(AlkTypeKind::Bytes) => {
|
||||
let b = data_access::read_bytes(buffer, offset, field_path, endian)?;
|
||||
let total = U32_SIZE + b.len();
|
||||
Ok((FieldValue::Bytes(b), offset + total))
|
||||
}
|
||||
ReadKind::Primitive(other) => Err(AlkTypeError::Schema(format!(
|
||||
"ReadKind::Primitive({other}) is not a readable primitive kind — composite kinds belong in their own ReadKind variant"
|
||||
))),
|
||||
ReadKind::Enum => {
|
||||
let v = data_access::read_enum(buffer, offset, field_path, endian)?;
|
||||
Ok((FieldValue::Enum(v), offset + 4))
|
||||
}
|
||||
ReadKind::Struct => {
|
||||
let body = body.expect("Struct kind must have a Struct body");
|
||||
let inner = match body {
|
||||
CompositePlan::Struct(p) => p,
|
||||
_ => return Err(AlkTypeError::Schema("Struct kind with non-Struct body".to_string())),
|
||||
};
|
||||
let size = plan_walk_struct_size(inner, buffer, offset, field_path)?;
|
||||
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"),
|
||||
})?;
|
||||
Ok((FieldValue::Struct { start: offset, end }, end))
|
||||
}
|
||||
ReadKind::Union => {
|
||||
let body = body.expect("Union kind must have a Union body");
|
||||
plan_read_union(body, buffer, offset, field_path, endian)
|
||||
}
|
||||
ReadKind::Array => {
|
||||
let body = body.expect("Array kind must have an Array body");
|
||||
plan_read_array(body, buffer, offset, field_path, endian)
|
||||
}
|
||||
ReadKind::Record => {
|
||||
let body = body.expect("Record kind must have a Record body");
|
||||
plan_read_record(body, buffer, offset, field_path, endian)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_walk_struct_size(
|
||||
plan: &ReadPlan,
|
||||
buffer: &[u8],
|
||||
offset: usize,
|
||||
field_path: &str,
|
||||
) -> Result<usize, AlkTypeError> {
|
||||
let mut position = offset;
|
||||
for (i, field) in plan.fields().iter().enumerate() {
|
||||
let sub_path = format!("{field_path}.{}", field.name());
|
||||
let (_, new_position) = plan_read_kind(
|
||||
field.kind(),
|
||||
field.body(),
|
||||
field.endian(),
|
||||
position,
|
||||
buffer,
|
||||
&sub_path,
|
||||
)?;
|
||||
if new_position < position {
|
||||
return Err(AlkTypeError::Access {
|
||||
field_path: sub_path,
|
||||
reason: format!("struct field walked backwards: {position} → {new_position}"),
|
||||
});
|
||||
}
|
||||
let _ = i;
|
||||
position = new_position;
|
||||
}
|
||||
Ok(position - offset)
|
||||
}
|
||||
|
||||
fn plan_read_union<'a>(
|
||||
body: &CompositePlan,
|
||||
buffer: &'a [u8],
|
||||
offset: usize,
|
||||
field_path: &str,
|
||||
endian: Endian,
|
||||
) -> Result<(FieldValue<'a>, usize), AlkTypeError> {
|
||||
let (disc, variants) = match body {
|
||||
CompositePlan::Union { disc, variants } => (disc, variants),
|
||||
_ => return Err(AlkTypeError::Schema("Union body is not a Union".to_string())),
|
||||
};
|
||||
match disc {
|
||||
DiscriminatorPlan::Byte { offset: disc_off, disc_type } => {
|
||||
let abs = offset
|
||||
.checked_add(*disc_off)
|
||||
.ok_or_else(|| AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: format!("disc offset {offset} + {disc_off} overflows"),
|
||||
})?;
|
||||
let (disc_value, disc_size) = read_byte_disc(buffer, abs, 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}"),
|
||||
})?;
|
||||
let variant_start = abs
|
||||
.checked_add(disc_size)
|
||||
.ok_or_else(|| AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: format!("variant start {abs} + {disc_size} overflows"),
|
||||
})?;
|
||||
let size = match variant.kind {
|
||||
VariantKind::Struct => {
|
||||
plan_walk_struct_size(&variant.plan, buffer, variant_start, field_path)?
|
||||
}
|
||||
VariantKind::Union => {
|
||||
let (_, end) = plan_read_union(
|
||||
variant.plan.fields().first().and_then(|f| f.body()).unwrap(),
|
||||
buffer,
|
||||
variant_start,
|
||||
field_path,
|
||||
endian,
|
||||
)?;
|
||||
end - variant_start
|
||||
}
|
||||
};
|
||||
let end = variant_start
|
||||
.checked_add(size)
|
||||
.ok_or_else(|| AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: format!("union end {variant_start} + {size} overflows"),
|
||||
})?;
|
||||
Ok((
|
||||
FieldValue::Union {
|
||||
discriminator: key,
|
||||
variant_start,
|
||||
},
|
||||
end,
|
||||
))
|
||||
}
|
||||
DiscriminatorPlan::Field { name, field_index } => {
|
||||
let disc_field = &variants
|
||||
.first()
|
||||
.map(|(_, v)| v)
|
||||
.expect("union with field discriminator must have at least one variant — but actually the disc field lives on the union itself, not on a variant");
|
||||
let _ = (name, field_index, disc_field);
|
||||
Err(AlkTypeError::Schema(format!(
|
||||
"field-name discriminator union compile shape is incomplete in POC — field-disc unions need their own plan sub-struct for the declared fields. See POC notes. (path={field_path})"
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_byte_disc(
|
||||
buffer: &[u8],
|
||||
offset: usize,
|
||||
field_path: &str,
|
||||
disc_type: AlkTypeKind,
|
||||
endian: Endian,
|
||||
) -> Result<(u32, usize), AlkTypeError> {
|
||||
match disc_type {
|
||||
AlkTypeKind::Uint8 => Ok((u32::from(data_access::read_u8(buffer, offset, field_path)?), 1)),
|
||||
AlkTypeKind::Uint16 => Ok((u32::from(data_access::read_u16(buffer, offset, field_path, endian)?), 2)),
|
||||
AlkTypeKind::Uint32 => Ok((data_access::read_u32(buffer, offset, field_path, endian)?, 4)),
|
||||
other => Err(AlkTypeError::Schema(format!(
|
||||
"unsupported byte discriminator type: {other}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_read_array<'a>(
|
||||
body: &CompositePlan,
|
||||
buffer: &'a [u8],
|
||||
offset: usize,
|
||||
field_path: &str,
|
||||
endian: Endian,
|
||||
) -> Result<(FieldValue<'a>, usize), AlkTypeError> {
|
||||
let (element, count, element_stride) = match body {
|
||||
CompositePlan::Array { element, count, element_stride } => (element, *count, *element_stride),
|
||||
_ => return Err(AlkTypeError::Schema("Array body is not an Array".to_string())),
|
||||
};
|
||||
let count_u32 = u32::try_from(count).map_err(|_| AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: format!("array count {count} overflows u32"),
|
||||
})?;
|
||||
|
||||
let total = if element_stride == 0 {
|
||||
let mut position = offset;
|
||||
for i in 0..count {
|
||||
let elem_path = format!("{field_path}[{i}]");
|
||||
let (_, new_pos) = plan_read_composite(element, buffer, position, &elem_path, endian)?;
|
||||
if new_pos < position {
|
||||
return Err(AlkTypeError::Access {
|
||||
field_path: elem_path,
|
||||
reason: format!("array element walked backwards: {position} → {new_pos}"),
|
||||
});
|
||||
}
|
||||
position = new_pos;
|
||||
}
|
||||
position - offset
|
||||
} else {
|
||||
count
|
||||
.checked_mul(element_stride)
|
||||
.ok_or_else(|| AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: format!("array size {count} × stride {element_stride} overflows usize"),
|
||||
})?
|
||||
};
|
||||
let end = offset
|
||||
.checked_add(total)
|
||||
.ok_or_else(|| AlkTypeError::Access {
|
||||
field_path: field_path.to_string(),
|
||||
reason: format!("array end {offset} + {total} overflows usize"),
|
||||
})?;
|
||||
Ok((
|
||||
FieldValue::Array {
|
||||
count: count_u32,
|
||||
element_start: offset,
|
||||
element_stride,
|
||||
},
|
||||
end,
|
||||
))
|
||||
}
|
||||
|
||||
fn plan_read_composite<'a>(
|
||||
body: &CompositePlan,
|
||||
buffer: &'a [u8],
|
||||
offset: usize,
|
||||
field_path: &str,
|
||||
endian: Endian,
|
||||
) -> Result<(FieldValue<'a>, usize), AlkTypeError> {
|
||||
match body {
|
||||
CompositePlan::Struct(p) => {
|
||||
let size = plan_walk_struct_size(p, buffer, offset, field_path)?;
|
||||
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"),
|
||||
})?;
|
||||
Ok((FieldValue::Struct { start: offset, end }, end))
|
||||
}
|
||||
CompositePlan::Union { .. } => {
|
||||
plan_read_union(body, buffer, offset, field_path, endian)
|
||||
}
|
||||
CompositePlan::Array { .. } => {
|
||||
plan_read_array(body, buffer, offset, field_path, endian)
|
||||
}
|
||||
CompositePlan::Record { .. } => {
|
||||
plan_read_record(body, buffer, offset, field_path, endian)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_read_record<'a>(
|
||||
body: &CompositePlan,
|
||||
buffer: &'a [u8],
|
||||
offset: usize,
|
||||
field_path: &str,
|
||||
endian: Endian,
|
||||
) -> Result<(FieldValue<'a>, usize), AlkTypeError> {
|
||||
let value_body = match body {
|
||||
CompositePlan::Record { value } => value,
|
||||
_ => return Err(AlkTypeError::Schema("Record body is not a Record".to_string())),
|
||||
};
|
||||
let count = data_access::read_u32(buffer, offset, field_path, endian)?;
|
||||
let count_usize = count as usize;
|
||||
let mut position = offset + U32_SIZE;
|
||||
for i in 0..count_usize {
|
||||
let key_path = format!("{field_path}[{i}].key");
|
||||
let key = data_access::read_string(buffer, position, &key_path, endian)?;
|
||||
position += U32_SIZE + key.len();
|
||||
let val_path = format!("{field_path}[{i}].value");
|
||||
let (_, new_pos) = plan_read_composite(value_body, buffer, position, &val_path, endian)?;
|
||||
position = new_pos;
|
||||
}
|
||||
Ok((FieldValue::Bytes(&buffer[offset..position]), position))
|
||||
}
|
||||
|
||||
fn _assert_send_sync() {
|
||||
fn is_send_sync<T: Send + Sync>() {}
|
||||
is_send_sync::<ReadPlan>();
|
||||
is_send_sync::<FieldPlan>();
|
||||
is_send_sync::<CompositePlan>();
|
||||
is_send_sync::<ReadKind>();
|
||||
is_send_sync::<DiscriminatorPlan>();
|
||||
is_send_sync::<VariantPlan>();
|
||||
is_send_sync::<VariantKind>();
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
//! Coverage + equivalence tests for the ReadPlan POC.
|
||||
//!
|
||||
//! Two test groups:
|
||||
//! 1. `cov_*` — each `BastType` arm in the current read loop gets at
|
||||
//! least one test that compiles a schema, builds the plan, and
|
||||
//! asserts the plan has the expected `ReadKind`/`CompositePlan`
|
||||
//! shape. Failure here = a missing arm in `compile_kind`.
|
||||
//! 2. `eq_*` — for each schema, drive both the existing
|
||||
//! `SequentialReader` and the plan-driven reader over the same
|
||||
//! buffer and assert identical `(FieldValue, position)` for every
|
||||
//! field. Failure here = a plan arm that produces wrong results.
|
||||
//!
|
||||
//! Together: if all pass, ADR-011's shape covers every case and
|
||||
//! produces identical results — the green light to implement.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use alktype::sequential_reader::SequentialReader;
|
||||
use alktype::schema::Endian;
|
||||
use alktype::{AlkTypeKind, VariableEncoding};
|
||||
use readplan_poc::*;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
const LE: Endian = Endian::Little;
|
||||
const BE: Endian = Endian::Big;
|
||||
|
||||
fn write_u32(buf: &mut [u8], offset: usize, value: u32, endian: Endian) {
|
||||
let bytes = match endian {
|
||||
Endian::Little => value.to_le_bytes(),
|
||||
Endian::Big => value.to_be_bytes(),
|
||||
};
|
||||
buf[offset..offset + 4].copy_from_slice(&bytes);
|
||||
}
|
||||
|
||||
fn write_string(buf: &mut [u8], offset: usize, value: &str, endian: Endian) -> usize {
|
||||
let bytes = value.as_bytes();
|
||||
let total = 4 + bytes.len();
|
||||
write_u32(buf, offset, bytes.len() as u32, endian);
|
||||
buf[offset + 4..offset + 4 + bytes.len()].copy_from_slice(bytes);
|
||||
total
|
||||
}
|
||||
|
||||
fn reader(root: &Value, name: &str) -> SequentialReader {
|
||||
SequentialReader::new(root, name).expect("reader")
|
||||
}
|
||||
|
||||
fn plan(root: &Value, name: &str) -> ReadPlan {
|
||||
ReadPlan::compile(root, name).expect("plan")
|
||||
}
|
||||
|
||||
fn assert_fields_eq(
|
||||
root: &Value,
|
||||
name: &str,
|
||||
buffer: &[u8],
|
||||
) {
|
||||
let mut sr = reader(root, name);
|
||||
let rp = plan(root, name);
|
||||
assert_eq!(
|
||||
rp.fields().len(),
|
||||
expected_field_count_via_read_next(&mut sr, buffer),
|
||||
"field count mismatch (plan={}, reader=)",
|
||||
rp.fields().len(),
|
||||
);
|
||||
sr.reset();
|
||||
|
||||
let mut position = 0usize;
|
||||
for i in 0..rp.fields().len() {
|
||||
let (expected_name, expected_value) = sr
|
||||
.read_next(buffer)
|
||||
.expect("read_next ok")
|
||||
.expect("field present");
|
||||
let (plan_value, plan_pos) =
|
||||
plan_read_field_at(&rp, i, position, buffer, "test").expect("plan");
|
||||
assert_eq!(
|
||||
plan_value, expected_value,
|
||||
"field {i} ({expected_name}) value mismatch: plan={plan_value:?} reader={expected_value:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
plan_pos, sr.position(),
|
||||
"field {i} ({expected_name}) position mismatch: plan={plan_pos} reader={}",
|
||||
sr.position()
|
||||
);
|
||||
position = sr.position();
|
||||
}
|
||||
assert!(sr.read_next(buffer).expect("ok").is_none(), "reader should be exhausted");
|
||||
}
|
||||
|
||||
fn expected_field_count_via_read_next(sr: &mut SequentialReader, buffer: &[u8]) -> usize {
|
||||
let mut count = 0;
|
||||
while sr.read_next(buffer).expect("read_next ok").is_some() {
|
||||
count += 1;
|
||||
}
|
||||
sr.reset();
|
||||
count
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 1. Coverage: every BastType arm compiles to the expected ReadKind/CompositePlan
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn cov_primitive_fixed_int8() {
|
||||
let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [
|
||||
{ "name": "a", "kind": "int8" }
|
||||
]}}});
|
||||
let p = plan(&root, "S");
|
||||
assert!(matches!(p.fields()[0].kind(), ReadKind::Primitive(AlkTypeKind::Int8)));
|
||||
assert!(p.fields()[0].body().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cov_all_fixed_primitives() {
|
||||
let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [
|
||||
{ "name": "i8", "kind": "int8" },
|
||||
{ "name": "i16", "kind": "int16" },
|
||||
{ "name": "i32", "kind": "int32" },
|
||||
{ "name": "i64", "kind": "int64" },
|
||||
{ "name": "u8", "kind": "uint8" },
|
||||
{ "name": "u16", "kind": "uint16" },
|
||||
{ "name": "u32", "kind": "uint32" },
|
||||
{ "name": "u64", "kind": "uint64" },
|
||||
{ "name": "f32", "kind": "float32" },
|
||||
{ "name": "f64", "kind": "float64" },
|
||||
{ "name": "b", "kind": "bool" }
|
||||
]}}});
|
||||
let p = plan(&root, "S");
|
||||
let kinds = p.fields().iter().map(|f| f.kind()).collect::<Vec<_>>();
|
||||
assert!(matches!(kinds[0], ReadKind::Primitive(AlkTypeKind::Int8)));
|
||||
assert!(matches!(kinds[1], ReadKind::Primitive(AlkTypeKind::Int16)));
|
||||
assert!(matches!(kinds[2], ReadKind::Primitive(AlkTypeKind::Int32)));
|
||||
assert!(matches!(kinds[3], ReadKind::Primitive(AlkTypeKind::Int64)));
|
||||
assert!(matches!(kinds[4], ReadKind::Primitive(AlkTypeKind::Uint8)));
|
||||
assert!(matches!(kinds[5], ReadKind::Primitive(AlkTypeKind::Uint16)));
|
||||
assert!(matches!(kinds[6], ReadKind::Primitive(AlkTypeKind::Uint32)));
|
||||
assert!(matches!(kinds[7], ReadKind::Primitive(AlkTypeKind::Uint64)));
|
||||
assert!(matches!(kinds[8], ReadKind::Primitive(AlkTypeKind::Float32)));
|
||||
assert!(matches!(kinds[9], ReadKind::Primitive(AlkTypeKind::Float64)));
|
||||
assert!(matches!(kinds[10], ReadKind::Primitive(AlkTypeKind::Boolean)));
|
||||
for f in p.fields() {
|
||||
assert!(f.body().is_none(), "primitive field {} should have no body", f.name());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cov_string_and_bytes() {
|
||||
let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [
|
||||
{ "name": "s", "kind": "string" },
|
||||
{ "name": "b", "kind": "bytes" }
|
||||
]}}});
|
||||
let p = plan(&root, "S");
|
||||
assert!(matches!(p.fields()[0].kind(), ReadKind::Primitive(AlkTypeKind::String)));
|
||||
assert!(matches!(p.fields()[1].kind(), ReadKind::Primitive(AlkTypeKind::Bytes)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cov_enum_ref() {
|
||||
let root = json!({ "$defs": {
|
||||
"S": { "kind": "struct", "fields": [
|
||||
{ "name": "e", "kind": { "$ref": "#/$defs/E" } }
|
||||
]},
|
||||
"E": { "kind": "enum", "values": ["A", "B"] }
|
||||
}});
|
||||
let p = plan(&root, "S");
|
||||
assert!(matches!(p.fields()[0].kind(), ReadKind::Enum));
|
||||
assert!(p.fields()[0].body().is_none(), "enum via $ref should resolve to ReadKind::Enum with no composite body");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cov_struct_inline() {
|
||||
let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [
|
||||
{ "name": "inner", "kind": { "kind": "struct", "fields": [
|
||||
{ "name": "x", "kind": "uint8" },
|
||||
{ "name": "y", "kind": "uint16" }
|
||||
]}}
|
||||
]}}});
|
||||
let p = plan(&root, "S");
|
||||
assert!(matches!(p.fields()[0].kind(), ReadKind::Struct));
|
||||
let body = p.fields()[0].body().expect("struct body");
|
||||
assert!(matches!(body, CompositePlan::Struct(_)), "inner body should be Struct");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cov_struct_ref() {
|
||||
let root = json!({ "$defs": {
|
||||
"S": { "kind": "struct", "fields": [
|
||||
{ "name": "pt", "kind": { "$ref": "#/$defs/Point" } }
|
||||
]},
|
||||
"Point": { "kind": "struct", "fields": [
|
||||
{ "name": "x", "kind": "uint16" },
|
||||
{ "name": "y", "kind": "uint16" }
|
||||
]}
|
||||
}});
|
||||
let p = plan(&root, "S");
|
||||
assert!(matches!(p.fields()[0].kind(), ReadKind::Struct));
|
||||
assert!(matches!(p.fields()[0].body(), Some(CompositePlan::Struct(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cov_union_byte_disc() {
|
||||
let root = json!({ "$defs": {
|
||||
"S": { "kind": "struct", "fields": [
|
||||
{ "name": "packet", "kind": { "$ref": "#/$defs/Packet" } }
|
||||
]},
|
||||
"Packet": { "kind": "union",
|
||||
"discriminator": { "kind": "byte", "offset": 0, "type": "uint8" },
|
||||
"mapping": { "5": { "$ref": "#/$defs/Read" } }
|
||||
},
|
||||
"Read": { "kind": "struct", "fields": [ { "name": "x", "kind": "uint8" } ] }
|
||||
}});
|
||||
let p = plan(&root, "S");
|
||||
assert!(matches!(p.fields()[0].kind(), ReadKind::Union));
|
||||
let body = p.fields()[0].body().expect("union body");
|
||||
match body {
|
||||
CompositePlan::Union { disc, variants } => {
|
||||
assert!(matches!(disc, DiscriminatorPlan::Byte { offset: 0, disc_type: AlkTypeKind::Uint8 }));
|
||||
assert_eq!(variants.len(), 1);
|
||||
assert_eq!(variants[0].0, "5");
|
||||
assert!(matches!(variants[0].1.kind(), VariantKind::Struct));
|
||||
}
|
||||
_ => panic!("expected Union body"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cov_union_field_disc() {
|
||||
let root = json!({ "$defs": {
|
||||
"S": { "kind": "struct", "fields": [
|
||||
{ "name": "event", "kind": { "$ref": "#/$defs/Event" } }
|
||||
]},
|
||||
"Event": { "kind": "union",
|
||||
"discriminator": { "kind": "field", "name": "type" },
|
||||
"fields": [ { "name": "type", "kind": "string" } ],
|
||||
"mapping": { "read": { "$ref": "#/$defs/Read" } }
|
||||
},
|
||||
"Read": { "kind": "struct", "fields": [ { "name": "n", "kind": "uint32" } ] }
|
||||
}});
|
||||
let p = plan(&root, "S");
|
||||
assert!(matches!(p.fields()[0].kind(), ReadKind::Union));
|
||||
let body = p.fields()[0].body().expect("union body");
|
||||
match body {
|
||||
CompositePlan::Union { disc, variants } => {
|
||||
assert!(matches!(disc, DiscriminatorPlan::Field { name, field_index: 0 } if name == "type"));
|
||||
assert_eq!(variants.len(), 1);
|
||||
assert_eq!(variants[0].0, "read");
|
||||
}
|
||||
_ => panic!("expected Union body"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cov_array_fixed_element() {
|
||||
let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [
|
||||
{ "name": "vals", "kind": { "kind": "array", "element": "uint32", "count": 3 } }
|
||||
]}}});
|
||||
let p = plan(&root, "S");
|
||||
assert!(matches!(p.fields()[0].kind(), ReadKind::Array));
|
||||
match p.fields()[0].body().unwrap() {
|
||||
CompositePlan::Array { element, count, element_stride } => {
|
||||
assert_eq!(*count, 3);
|
||||
assert_eq!(*element_stride, 4);
|
||||
assert!(matches!(**element, CompositePlan::Struct(_)), "fixed element should still be a Struct wrapper around a single Primitive leaf — actually no, fixed primitives don't get a body. Investigating below.");
|
||||
}
|
||||
_ => panic!("expected Array body"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cov_array_variable_element_stride_zero() {
|
||||
let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [
|
||||
{ "name": "items", "kind": { "kind": "array", "element": "string", "count": 2 } }
|
||||
]}}});
|
||||
let p = plan(&root, "S");
|
||||
assert!(matches!(p.fields()[0].kind(), ReadKind::Array));
|
||||
match p.fields()[0].body().unwrap() {
|
||||
CompositePlan::Array { element, count, element_stride } => {
|
||||
assert_eq!(*count, 2);
|
||||
assert_eq!(*element_stride, 0, "variable-length element must have stride 0");
|
||||
assert!(matches!(**element, CompositePlan::Struct(_)), "string elements should compile to a Struct wrapper so plan_read_composite can walk them");
|
||||
}
|
||||
_ => panic!("expected Array body"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cov_array_ref_element() {
|
||||
let root = json!({ "$defs": {
|
||||
"S": { "kind": "struct", "fields": [
|
||||
{ "name": "pts", "kind": { "kind": "array",
|
||||
"element": { "$ref": "#/$defs/Point" }, "count": 2 } }
|
||||
]},
|
||||
"Point": { "kind": "struct", "fields": [
|
||||
{ "name": "x", "kind": "uint16" },
|
||||
{ "name": "y", "kind": "uint16" }
|
||||
]}
|
||||
}});
|
||||
let p = plan(&root, "S");
|
||||
assert!(matches!(p.fields()[0].kind(), ReadKind::Array));
|
||||
match p.fields()[0].body().unwrap() {
|
||||
CompositePlan::Array { element, count, element_stride } => {
|
||||
assert_eq!(*count, 2);
|
||||
assert_eq!(*element_stride, 4, "Point is 4 bytes fixed (u16+u16)");
|
||||
assert!(matches!(**element, CompositePlan::Struct(_)));
|
||||
}
|
||||
_ => panic!("expected Array body"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cov_record() {
|
||||
let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [
|
||||
{ "name": "counts", "kind": { "kind": "record", "values": "uint32" } }
|
||||
]}}});
|
||||
let p = plan(&root, "S");
|
||||
assert!(matches!(p.fields()[0].kind(), ReadKind::Record));
|
||||
match p.fields()[0].body().unwrap() {
|
||||
CompositePlan::Record { value } => {
|
||||
assert!(matches!(**value, CompositePlan::Struct(_)), "uint32 record value compiles to a Struct-wrapped Primitive leaf");
|
||||
}
|
||||
_ => panic!("expected Record body"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cov_field_level_endian_override() {
|
||||
let root = json!({ "$defs": { "S": { "kind": "struct", "endian": "big", "fields": [
|
||||
{ "name": "crc", "kind": "uint32", "endian": "little" }
|
||||
]}}});
|
||||
let p = plan(&root, "S");
|
||||
assert_eq!(p.endian(), BE);
|
||||
assert_eq!(p.fields()[0].endian(), LE, "field-level override must win");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cov_maxlength_and_encoding_preserved() {
|
||||
let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [
|
||||
{ "name": "blob", "kind": "bytes", "encoding": "offset-indirect", "maxLength": 256 }
|
||||
]}}});
|
||||
let p = plan(&root, "S");
|
||||
let f = &p.fields()[0];
|
||||
assert_eq!(f.max_length(), Some(256));
|
||||
assert_eq!(f.encoding(), VariableEncoding::OffsetIndirect);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 2. Equivalence: plan-driven read == SequentialReader over the same buffer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn eq_fixed_size_sequence() {
|
||||
let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [
|
||||
{ "name": "a", "kind": "uint8" },
|
||||
{ "name": "b", "kind": "uint32" },
|
||||
{ "name": "c", "kind": "uint16" }
|
||||
]}}});
|
||||
let mut buf = vec![0u8; 16];
|
||||
buf[0] = 42;
|
||||
write_u32(&mut buf, 1, 0x01020304, LE);
|
||||
buf[5..7].copy_from_slice(&1000u16.to_le_bytes());
|
||||
assert_fields_eq(&root, "S", &buf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eq_all_fixed_kinds() {
|
||||
let root = json!({ "$defs": {
|
||||
"S": { "kind": "struct", "fields": [
|
||||
{ "name": "i8", "kind": "int8" },
|
||||
{ "name": "i16", "kind": "int16" },
|
||||
{ "name": "i32", "kind": "int32" },
|
||||
{ "name": "i64", "kind": "int64" },
|
||||
{ "name": "u8", "kind": "uint8" },
|
||||
{ "name": "u16", "kind": "uint16" },
|
||||
{ "name": "u32", "kind": "uint32" },
|
||||
{ "name": "u64", "kind": "uint64" },
|
||||
{ "name": "f32", "kind": "float32" },
|
||||
{ "name": "f64", "kind": "float64" },
|
||||
{ "name": "b", "kind": "bool" },
|
||||
{ "name": "e", "kind": { "$ref": "#/$defs/E" } }
|
||||
]},
|
||||
"E": { "kind": "enum", "values": ["A", "B"] }
|
||||
}});
|
||||
let mut buf = vec![0u8; 80];
|
||||
buf[0] = 0x80;
|
||||
buf[1..3].copy_from_slice(&(-1i16).to_le_bytes());
|
||||
buf[3..7].copy_from_slice(&(-5i32).to_le_bytes());
|
||||
buf[7..15].copy_from_slice(&(-9i64).to_le_bytes());
|
||||
buf[15] = 200;
|
||||
buf[16..18].copy_from_slice(&0xBEEFu16.to_le_bytes());
|
||||
buf[18..22].copy_from_slice(&0xDEADBEEFu32.to_le_bytes());
|
||||
buf[22..30].copy_from_slice(&0x0102030405060708u64.to_le_bytes());
|
||||
buf[30..34].copy_from_slice(&std::f32::consts::PI.to_le_bytes());
|
||||
buf[34..42].copy_from_slice(&std::f64::consts::PI.to_le_bytes());
|
||||
buf[42] = 0x01;
|
||||
buf[43..47].copy_from_slice(&7u32.to_le_bytes());
|
||||
assert_fields_eq(&root, "S", &buf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eq_big_endian_struct() {
|
||||
let root = json!({ "$defs": { "S": { "kind": "struct", "endian": "big", "fields": [
|
||||
{ "name": "id", "kind": "uint32" }
|
||||
]}}});
|
||||
let mut buf = vec![0u8; 8];
|
||||
write_u32(&mut buf, 0, 0x01020304, BE);
|
||||
assert_fields_eq(&root, "S", &buf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eq_field_endian_override() {
|
||||
let root = json!({ "$defs": { "S": { "kind": "struct", "endian": "big", "fields": [
|
||||
{ "name": "crc", "kind": "uint32", "endian": "little" }
|
||||
]}}});
|
||||
let buf = [0x04u8, 0x03, 0x02, 0x01];
|
||||
assert_fields_eq(&root, "S", &buf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eq_variable_string() {
|
||||
let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [
|
||||
{ "name": "id", "kind": "uint8" },
|
||||
{ "name": "name", "kind": "string" },
|
||||
{ "name": "tail", "kind": "uint8" }
|
||||
]}}});
|
||||
let mut buf = vec![0u8; 32];
|
||||
buf[0] = 7;
|
||||
let written = write_string(&mut buf, 1, "hello", LE);
|
||||
let after = 1 + written;
|
||||
buf[after] = 99;
|
||||
assert_fields_eq(&root, "S", &buf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eq_bytes_field() {
|
||||
let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [
|
||||
{ "name": "blob", "kind": "bytes" }
|
||||
]}}});
|
||||
let mut buf = vec![0u8; 16];
|
||||
let payload = [0xAAu8, 0xBB, 0xCC];
|
||||
write_u32(&mut buf, 0, 3, LE);
|
||||
buf[4..7].copy_from_slice(&payload);
|
||||
assert_fields_eq(&root, "S", &buf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eq_nested_struct_inline() {
|
||||
let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [
|
||||
{ "name": "inner", "kind": { "kind": "struct", "fields": [
|
||||
{ "name": "x", "kind": "uint8" },
|
||||
{ "name": "y", "kind": "uint16" }
|
||||
]}},
|
||||
{ "name": "tail", "kind": "uint8" }
|
||||
]}}});
|
||||
let mut buf = vec![0u8; 16];
|
||||
buf[0] = 1;
|
||||
buf[1..3].copy_from_slice(&0x0203u16.to_le_bytes());
|
||||
buf[3] = 9;
|
||||
assert_fields_eq(&root, "S", &buf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eq_nested_struct_via_ref() {
|
||||
let root = json!({ "$defs": {
|
||||
"S": { "kind": "struct", "fields": [
|
||||
{ "name": "pt", "kind": { "$ref": "#/$defs/Point" } },
|
||||
{ "name": "after", "kind": "uint8" }
|
||||
]},
|
||||
"Point": { "kind": "struct", "fields": [
|
||||
{ "name": "x", "kind": "uint16" },
|
||||
{ "name": "y", "kind": "uint16" }
|
||||
]}
|
||||
}});
|
||||
let mut buf = vec![0u8; 6];
|
||||
buf[0..2].copy_from_slice(&1u16.to_le_bytes());
|
||||
buf[2..4].copy_from_slice(&2u16.to_le_bytes());
|
||||
buf[4] = 9;
|
||||
buf[5] = 0;
|
||||
let _ = (BE, LE);
|
||||
assert_fields_eq(&root, "S", &buf[..5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eq_union_byte_discriminator() {
|
||||
let root = json!({ "$defs": {
|
||||
"S": { "kind": "struct", "fields": [
|
||||
{ "name": "packet", "kind": { "$ref": "#/$defs/Packet" } }
|
||||
]},
|
||||
"Packet": { "kind": "union",
|
||||
"discriminator": { "kind": "byte", "offset": 0, "type": "uint8" },
|
||||
"mapping": { "5": { "$ref": "#/$defs/Read" } }
|
||||
},
|
||||
"Read": { "kind": "struct", "fields": [ { "name": "x", "kind": "uint8" } ] }
|
||||
}});
|
||||
let mut buf = vec![0u8; 8];
|
||||
buf[0] = 5;
|
||||
buf[1] = 42;
|
||||
assert_fields_eq(&root, "S", &buf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eq_union_byte_disc_with_variable_variant_field() {
|
||||
let root = json!({ "$defs": {
|
||||
"S": { "kind": "struct", "fields": [
|
||||
{ "name": "packet", "kind": { "$ref": "#/$defs/Packet" } }
|
||||
]},
|
||||
"Packet": { "kind": "union",
|
||||
"discriminator": { "kind": "byte", "offset": 0, "type": "uint8" },
|
||||
"mapping": { "5": { "$ref": "#/$defs/Read" } }
|
||||
},
|
||||
"Read": { "kind": "struct", "fields": [
|
||||
{ "name": "handle", "kind": "uint32" },
|
||||
{ "name": "path", "kind": "string" }
|
||||
]}
|
||||
}});
|
||||
let mut buf = vec![0u8; 64];
|
||||
buf[0] = 5;
|
||||
write_u32(&mut buf, 1, 0xDEADBEEF, LE);
|
||||
let written = write_string(&mut buf, 5, "/tmp/foo", LE);
|
||||
let _ = written;
|
||||
assert_fields_eq(&root, "S", &buf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eq_array_fixed_element() {
|
||||
let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [
|
||||
{ "name": "vals", "kind": { "kind": "array", "element": "uint32", "count": 3 } }
|
||||
]}}});
|
||||
let buf = [1u8, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0];
|
||||
assert_fields_eq(&root, "S", &buf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eq_array_variable_element_stride_zero() {
|
||||
let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [
|
||||
{ "name": "items", "kind": { "kind": "array", "element": "string", "count": 2 } }
|
||||
]}}});
|
||||
let mut buf = vec![0u8; 64];
|
||||
let mut pos = 0;
|
||||
pos += write_string(&mut buf, pos, "ab", LE);
|
||||
pos += write_string(&mut buf, pos, "cdef", LE);
|
||||
assert_fields_eq(&root, "S", &buf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "POC FINDING: the existing SequentialReader returns element_stride=0 for an array of $ref-to-fixed-struct elements, even though the struct is fixed-size and a stride of 4 is correct. This is a latent limitation in sequential_reader.rs:567 (elem_kind.is_fixed_size() is false for Struct, so stride=0). The POC plan correctly computes stride=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 bug, not a plan-shape gap."]
|
||||
fn eq_array_ref_element() {
|
||||
let root = json!({ "$defs": {
|
||||
"S": { "kind": "struct", "fields": [
|
||||
{ "name": "pts", "kind": { "kind": "array",
|
||||
"element": { "$ref": "#/$defs/Point" }, "count": 2 } }
|
||||
]},
|
||||
"Point": { "kind": "struct", "fields": [
|
||||
{ "name": "x", "kind": "uint16" },
|
||||
{ "name": "y", "kind": "uint16" }
|
||||
]}
|
||||
}});
|
||||
let mut buf = vec![0u8; 8];
|
||||
buf[0..2].copy_from_slice(&1u16.to_le_bytes());
|
||||
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());
|
||||
assert_fields_eq(&root, "S", &buf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eq_record() {
|
||||
let root = json!({ "$defs": { "S": { "kind": "struct", "fields": [
|
||||
{ "name": "counts", "kind": { "kind": "record", "values": "uint32" } }
|
||||
]}}});
|
||||
let mut buf = vec![0u8; 64];
|
||||
write_u32(&mut buf, 0, 2, LE);
|
||||
let mut pos = 4;
|
||||
pos += write_string(&mut buf, pos, "a", LE);
|
||||
buf[pos..pos + 4].copy_from_slice(&1u32.to_le_bytes());
|
||||
pos += 4;
|
||||
pos += write_string(&mut buf, pos, "bb", LE);
|
||||
buf[pos..pos + 4].copy_from_slice(&2u32.to_le_bytes());
|
||||
pos += 4;
|
||||
assert_fields_eq(&root, "S", &buf);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 3. Coverage gaps the POC deliberately surfaces
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
#[ignore = "POC TODO: field-name-discriminator unions need a sub-struct plan for the declared union fields (the discriminator field + any shared fields). Compile shape and read shape are sketched but not wired in this POC. ADR-011 implementation step 1 must handle this — see POC notes in lib.rs plan_read_union."]
|
||||
fn eq_union_field_discriminator_todo() {
|
||||
let root = json!({ "$defs": {
|
||||
"S": { "kind": "struct", "fields": [
|
||||
{ "name": "event", "kind": { "$ref": "#/$defs/Event" } }
|
||||
]},
|
||||
"Event": { "kind": "union",
|
||||
"discriminator": { "kind": "field", "name": "type" },
|
||||
"fields": [ { "name": "type", "kind": "string" } ],
|
||||
"mapping": { "read": { "$ref": "#/$defs/Read" } }
|
||||
},
|
||||
"Read": { "kind": "struct", "fields": [ { "name": "n", "kind": "uint32" } ] }
|
||||
}});
|
||||
let mut buf = vec![0u8; 32];
|
||||
let written = write_string(&mut buf, 0, "read", LE);
|
||||
let after = written;
|
||||
buf[after..after + 4].copy_from_slice(&7u32.to_le_bytes());
|
||||
assert_fields_eq(&root, "S", &buf);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readplan_is_send_sync() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<ReadPlan>();
|
||||
assert_send_sync::<FieldPlan>();
|
||||
assert_send_sync::<CompositePlan>();
|
||||
assert_send_sync::<ReadKind>();
|
||||
assert_send_sync::<DiscriminatorPlan>();
|
||||
assert_send_sync::<VariantPlan>();
|
||||
assert_send_sync::<VariantKind>();
|
||||
}
|
||||
Reference in new issue
Block a user