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}