Skip to main content

behavior_contracts/
rawabi.rs

1//! The RAW handler ABI (bc#76): a de-boxed handler boundary so a generated typed module
2//! materializes a struct DIRECTLY from the wire payload, with no intermediate dynamic
3//! `Value`/`Value::Obj` tree on the row data plane.
4//!
5//! # Why this exists (the boxing #75 left)
6//!
7//! The typed codegen (#47/#48) materialized structs from an ALREADY-BOXED `Value`:
8//!
9//! ```ignore
10//! fn marshal_T0(raw: &Value) -> Result<T0, BehaviorError>   // raw is a Value::Obj tree
11//! ```
12//!
13//! The consumer's read handler first built the full dynamic `Value` tree and the generated
14//! module then WALKED it into a struct — ADDING a `Value`->struct conversion on top of the
15//! boxing instead of REPLACING it. The residual ~22% Rust gap #75 measured is exactly that
16//! `Value`/`Value::Obj` deep-clone at the handler ABI boundary (Rust felt it most: the
17//! `Value` enum boxes every `Obj` pair into a heap `Vec<(String, Value)>` that is cloned at
18//! the ABI, then re-walked).
19//!
20//! bc's conformance handlers return a `Value` mock, so the suite structurally could not see
21//! what the consumer's real handler did BEFORE the `Value` reached the module — the blind
22//! spot that let bc#47's B2 layering recur one layer out as bc#76.
23//!
24//! # The seam
25//!
26//! A RAW handler yields a [`RawValue`] — a native-backed accessor tree that is NOT a
27//! `Value`. The generated raw marshaller matches on it (scalar / [`RawRow`] / arr) straight
28//! into the concrete struct. The dynamic `Value` tree never exists on the row data plane;
29//! boxing is REPLACED, not layered. `RawRow::field` is the monomorphized wire read a
30//! hand-written v1-native struct-from-row would do.
31
32use crate::behavior::BehaviorError;
33use crate::value::Value;
34
35/// A handler result on the RAW data plane — deliberately NOT a [`Value`]: the whole point of
36/// #76 is that the dynamic `Value`/`Value::Obj` tree is never built here. A real consumer's
37/// handler returns its native wire shape as a [`RawValue`] with no `Value` allocation.
38#[derive(Debug, Clone)]
39pub enum RawValue {
40    Null,
41    Bool(bool),
42    Int(i64),
43    Float(f64),
44    Str(String),
45    Arr(Vec<RawValue>),
46    /// A native-backed object accessor (see [`RawRow`]) — NOT a `Value::Obj`.
47    Row(RawRow),
48}
49
50impl RawValue {
51    /// Normative type label (symmetric with [`Value::type_name`]) for TYPE_MISMATCH messages.
52    pub fn type_name(&self) -> &'static str {
53        match self {
54            RawValue::Null => "null",
55            RawValue::Bool(_) => "bool",
56            RawValue::Int(_) => "int",
57            RawValue::Float(_) => "float",
58            RawValue::Str(_) => "string",
59            RawValue::Arr(_) => "arr",
60            RawValue::Row(_) => "obj",
61        }
62    }
63}
64
65/// The object accessor on the raw data plane: a direct native field read that materializes
66/// NO `Value::Obj`. bc conformance uses this native `Vec`-backed row (no real wire format) so
67/// the generated raw marshaller exercises the de-boxed path; a real consumer implements the
68/// same shape over its wire payload (e.g. an AttributeValue map).
69#[derive(Debug, Clone, Default)]
70pub struct RawRow {
71    fields: Vec<(String, RawValue)>,
72}
73
74impl RawRow {
75    pub fn new() -> Self {
76        RawRow { fields: Vec::new() }
77    }
78
79    /// Insert or update a field, preserving first-insertion order (nested serialization for
80    /// the equivalence pin depends on it).
81    pub fn set(&mut self, key: impl Into<String>, val: RawValue) {
82        let key = key.into();
83        if let Some(slot) = self.fields.iter_mut().find(|(k, _)| *k == key) {
84            slot.1 = val;
85        } else {
86            self.fields.push((key, val));
87        }
88    }
89
90    /// Direct native field read (no `Value::Obj` materialized). Returns `None` when absent so
91    /// the raw marshaller can fail closed with MISSING_PROP.
92    pub fn field(&self, key: &str) -> Option<&RawValue> {
93        self.fields.iter().find(|(k, _)| k == key).map(|(_, v)| v)
94    }
95
96    /// Field pairs in insertion order (used by the equivalence-pin adapter only).
97    pub fn pairs(&self) -> &[(String, RawValue)] {
98        &self.fields
99    }
100}
101
102/// A RAW handler execution result. `Ok` carries a [`RawValue`] (native-backed), NOT a boxed
103/// `Value` — that is the whole de-box.
104#[derive(Debug, Clone)]
105pub enum RawOutcome {
106    Ok(RawValue),
107    Error(String),
108}
109
110/// The RAW handler registry seam: like `ComponentExec`, but the handler returns a
111/// [`RawValue`] (native-backed) instead of a boxed `Value`. A generated raw-ABI module
112/// dispatches through this and materializes structs directly from the [`RawValue`] — no
113/// dynamic `Value` tree on the row data plane.
114pub trait RawComponentExec {
115    fn exec_raw(
116        &mut self,
117        component: &str,
118        ports: &[(String, Value)],
119        bound: Option<&Value>,
120    ) -> Option<RawOutcome>;
121
122    /// ctx-carrying extension (mirrors `ComponentExec::exec_ctx`). Default forwards.
123    fn exec_raw_ctx(
124        &mut self,
125        node_id: &str,
126        component: &str,
127        ports: &[(String, Value)],
128        bound: Option<&Value>,
129    ) -> Option<RawOutcome> {
130        let _ = node_id;
131        self.exec_raw(component, ports, bound)
132    }
133}
134
135/// Blanket forwarding impl so a trait-object raw handler (`&mut dyn RawComponentExec`)
136/// satisfies [`RawComponentExec`] — mirrors the `&mut dyn ComponentExec` blanket (bc#68) so a
137/// consumer holding its raw registry behind a trait object drives the generated raw module
138/// through the unchanged generic entry.
139impl RawComponentExec for &mut dyn RawComponentExec {
140    fn exec_raw(
141        &mut self,
142        component: &str,
143        ports: &[(String, Value)],
144        bound: Option<&Value>,
145    ) -> Option<RawOutcome> {
146        (**self).exec_raw(component, ports, bound)
147    }
148
149    fn exec_raw_ctx(
150        &mut self,
151        node_id: &str,
152        component: &str,
153        ports: &[(String, Value)],
154        bound: Option<&Value>,
155    ) -> Option<RawOutcome> {
156        (**self).exec_raw_ctx(node_id, component, ports, bound)
157    }
158}
159
160// ── Native PORTS ABI (bc 0.5.0) ──────────────────────────────────────────────────────────
161//
162// The RESULT side above (`RawComponentExec`) de-boxed the handler RESULT. The PORTS side of
163// every generated componentRef still built a generic key-value container: `vec![("PK"
164// .to_string(), val), …]` — a `Vec` alloc + a heap `String` key per port + a boxed `Value`
165// per port — then a generic dispatch. That generic key-value plumbing is the residual cost.
166//
167// The native-ports emitter builds, per node, a NATIVE ports struct (typed fields, direct
168// construction, no `Vec`/`String`-key/`Value` boxing) and dispatches it through this seam. The
169// struct is passed behind a [`PortReader`] trait object so a consumer reads ports by name
170// (boxing one field on demand) or downcasts to its own type on the hot path. The generic
171// `ComponentExec(ports: &[(String, Value)])` path stays as an additive fallback.
172
173/// The optional uniform read accessor a generated native ports struct implements so a consumer
174/// that does not downcast can still read ports by name. Reading a statically-typed field this
175/// way clones/boxes ONLY that one field into a `Value` on demand (no `Vec`, no eager boxing of
176/// every port) — a hot-path consumer downcasts (`as_any`) and reads the typed fields directly.
177pub trait PortReader {
178    /// The `Value` for a port name, and whether it is a declared port of this node.
179    fn port(&self, name: &str) -> Option<Value>;
180    /// Downcast hook for a consumer that reads the concrete generated struct with zero boxing.
181    fn as_any(&self) -> &dyn std::any::Any;
182}
183
184/// The NATIVE-PORTS handler registry seam: the handler receives the generated native ports
185/// struct behind a [`PortReader`] and returns an `ExecOutcome` (a boxed `Value` RESULT — the
186/// UNTYPED straight-line path has no target struct to de-box the result into, and returns the
187/// boxed `Value` as the component output, exactly as the generic path does). NO generic
188/// `Vec<(String, Value)>` port CONTAINER is built on the port plane — that is the de-plumb.
189pub trait NativeComponentExec {
190    fn exec_native(
191        &mut self,
192        component: &str,
193        ports: &dyn PortReader,
194        bound: Option<&Value>,
195    ) -> Option<crate::plan::ExecOutcome>;
196
197    /// ctx-carrying extension (mirrors `ComponentExec::exec_ctx`). Default forwards.
198    fn exec_native_ctx(
199        &mut self,
200        node_id: &str,
201        component: &str,
202        ports: &dyn PortReader,
203        bound: Option<&Value>,
204    ) -> Option<crate::plan::ExecOutcome> {
205        let _ = node_id;
206        self.exec_native(component, ports, bound)
207    }
208}
209
210/// Blanket forwarding impl so a trait-object native handler (`&mut dyn NativeComponentExec`)
211/// satisfies [`NativeComponentExec`] (mirrors the `&mut dyn ComponentExec` blanket, bc#68).
212impl NativeComponentExec for &mut dyn NativeComponentExec {
213    fn exec_native(
214        &mut self,
215        component: &str,
216        ports: &dyn PortReader,
217        bound: Option<&Value>,
218    ) -> Option<crate::plan::ExecOutcome> {
219        (**self).exec_native(component, ports, bound)
220    }
221
222    fn exec_native_ctx(
223        &mut self,
224        node_id: &str,
225        component: &str,
226        ports: &dyn PortReader,
227        bound: Option<&Value>,
228    ) -> Option<crate::plan::ExecOutcome> {
229        (**self).exec_native_ctx(node_id, component, ports, bound)
230    }
231}
232
233// ── Combined NATIVE-ports + RAW-result ABI (bc#77, the 1.0 read de-box) ────────────────────
234//
235// [`NativeComponentExec`] de-plumbed the PORT container but still returns a boxed `Value`
236// RESULT. [`RawComponentExec`] de-boxes the RESULT but keeps the generic `Vec<(String, Value)>`
237// PORT container in AND is driven by `run_plan` — whose relation-child skip-gate reads the
238// PARENT's boxed result; a struct-native (nil) parent makes typed relationSingle / nestedBatchGet
239// diverge from `run_behavior` (graphddb#323 / bc#74).
240//
241// This seam COMBINES both halves: the handler receives the generated per-node NATIVE ports
242// struct (behind [`PortReader`] — no `Vec<(String, Value)>`) AND returns a native [`RawValue`]
243// ([`RawOutcome`] — no boxed `Value` result tree). The generated combined read runner drives the
244// nodes INLINE (sequential, no `run_plan`): each node's result is materialized DIRECTLY into its
245// outType struct via `marshal_raw_T*`, node results live as struct locals, and a relation child
246// reads the parent's REAL struct result inline — so relationSingle / connection reads converge
247// with `run_behavior`. Zero boxed `Value` end-to-end on both the port AND row plane.
248
249/// The COVERED-path port accessor (bc#94): the by-name read a combined handler uses, WITHOUT the
250/// dynamic [`PortReader::as_any`] downcast hook. The generated combined runner passes each node's
251/// CONCRETE ports struct to the handler by **direct generic instantiation** (`&P` where
252/// `P: PortReaderT`, monomorphized per node) — never behind `&dyn`, never via `as_any`/`downcast`.
253/// A `PortReaderT` handler still reads a port by name (boxing that one field on demand) if it does
254/// not want to name the concrete struct type, but the SEAM itself carries no `dyn`/`as_any`: the
255/// concrete type flows through monomorphization. (`PortReader` — with `as_any` — stays for the
256/// UNCOVERED [`NativeComponentExec`] path, retired later.)
257pub trait PortReaderT {
258    /// The `Value` for a port name, and whether it is a declared port of this node.
259    fn port(&self, name: &str) -> Option<Value>;
260}
261
262/// The combined handler registry seam (bc#94, nativized): native ports in (a CONCRETE `&P`, passed
263/// by monomorphized generic instantiation — no `&dyn`, no `as_any`/`downcast`), [`RawValue`] out.
264///
265/// `exec_native_raw` takes `&self` (not `&mut self`): the covered runner passes `&H`, so a `Sync`
266/// handler parallelizes across the bc#87 static-parallel stage with NO forced `Mutex` serialization
267/// (the generic seam is `&self`; a handler needing interior mutation uses its own `Sync` interior
268/// type — e.g. an atomic/`Mutex` field — rather than the seam forcing `&mut self` on every caller).
269pub trait NativeRawComponentExec {
270    fn exec_native_raw<P: PortReaderT + ?Sized>(
271        &self,
272        component: &str,
273        ports: &P,
274        bound: Option<&Value>,
275    ) -> Option<RawOutcome>;
276
277    /// ctx-carrying extension (mirrors `ComponentExec::exec_ctx`). Default forwards.
278    fn exec_native_raw_ctx<P: PortReaderT + ?Sized>(
279        &self,
280        node_id: &str,
281        component: &str,
282        ports: &P,
283        bound: Option<&Value>,
284    ) -> Option<RawOutcome> {
285        let _ = node_id;
286        self.exec_native_raw(component, ports, bound)
287    }
288}
289
290/// Lower a dynamic `Value` into the equivalent native-backed [`RawValue`]. This is the
291/// ADAPTER bc's conformance uses to feed the RAW path from the SAME vectors the boxed path
292/// uses (so behavior-equality is checked against `run_behavior` on identical data). It is a
293/// TEST/BENCH-ADAPTER that lives OFF the hot path: a real consumer never builds a `Value`
294/// first — it produces [`RawValue`] from its wire payload directly. The de-box win is
295/// measured against v1-native precisely because production skips this step.
296pub fn raw_from_value(v: &Value) -> RawValue {
297    match v {
298        Value::Null => RawValue::Null,
299        Value::Bool(b) => RawValue::Bool(*b),
300        Value::Int(i) => RawValue::Int(*i),
301        Value::Float(f) => RawValue::Float(*f),
302        Value::Str(s) => RawValue::Str(s.clone()),
303        Value::Arr(xs) => RawValue::Arr(xs.iter().map(raw_from_value).collect()),
304        Value::Obj(pairs) => {
305            let mut row = RawRow::new();
306            for (k, val) in pairs {
307                row.set(k.clone(), raw_from_value(val));
308            }
309            RawValue::Row(row)
310        }
311    }
312}
313
314/// Build a MISSING_PROP `BehaviorError` (symmetric with the boxed marshaller's fail-closed
315/// path) for a raw marshaller. Kept here so the generated raw code raises the same coded
316/// failure as `run_behavior`.
317pub fn raw_missing_prop(struct_name: &str, key: &str) -> BehaviorError {
318    crate::expr::ExprFailure {
319        code: crate::expr::ExprFailureCode::MissingProp,
320        message: format!("typed raw marshal {struct_name}: missing property .{key}"),
321    }
322    .into()
323}
324
325/// Build a TYPE_MISMATCH `BehaviorError` for a raw marshaller.
326pub fn raw_type_mismatch(expected: &str, got: &str) -> BehaviorError {
327    crate::expr::ExprFailure {
328        code: crate::expr::ExprFailureCode::TypeMismatch,
329        message: format!("typed raw marshal: expected {expected}, got {got}"),
330    }
331    .into()
332}