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/// Lower a dynamic `Value` into the equivalent native-backed [`RawValue`]. This is the
161/// ADAPTER bc's conformance uses to feed the RAW path from the SAME vectors the boxed path
162/// uses (so behavior-equality is checked against `run_behavior` on identical data). It is a
163/// TEST/BENCH-ADAPTER that lives OFF the hot path: a real consumer never builds a `Value`
164/// first — it produces [`RawValue`] from its wire payload directly. The de-box win is
165/// measured against v1-native precisely because production skips this step.
166pub fn raw_from_value(v: &Value) -> RawValue {
167    match v {
168        Value::Null => RawValue::Null,
169        Value::Bool(b) => RawValue::Bool(*b),
170        Value::Int(i) => RawValue::Int(*i),
171        Value::Float(f) => RawValue::Float(*f),
172        Value::Str(s) => RawValue::Str(s.clone()),
173        Value::Arr(xs) => RawValue::Arr(xs.iter().map(raw_from_value).collect()),
174        Value::Obj(pairs) => {
175            let mut row = RawRow::new();
176            for (k, val) in pairs {
177                row.set(k.clone(), raw_from_value(val));
178            }
179            RawValue::Row(row)
180        }
181    }
182}
183
184/// Build a MISSING_PROP `BehaviorError` (symmetric with the boxed marshaller's fail-closed
185/// path) for a raw marshaller. Kept here so the generated raw code raises the same coded
186/// failure as `run_behavior`.
187pub fn raw_missing_prop(struct_name: &str, key: &str) -> BehaviorError {
188    crate::expr::ExprFailure {
189        code: crate::expr::ExprFailureCode::MissingProp,
190        message: format!("typed raw marshal {struct_name}: missing property .{key}"),
191    }
192    .into()
193}
194
195/// Build a TYPE_MISMATCH `BehaviorError` for a raw marshaller.
196pub fn raw_type_mismatch(expected: &str, got: &str) -> BehaviorError {
197    crate::expr::ExprFailure {
198        code: crate::expr::ExprFailureCode::TypeMismatch,
199        message: format!("typed raw marshal: expected {expected}, got {got}"),
200    }
201    .into()
202}