behavior-contracts 0.3.0

Language-neutral IR runtime core (expression evaluation, template rendering, execution plan, canonical serialization) shared across DSL implementations. Passes the dsl-contracts conformance vectors byte-for-byte.
Documentation
//! The RAW handler ABI (bc#76): a de-boxed handler boundary so a generated typed module
//! materializes a struct DIRECTLY from the wire payload, with no intermediate dynamic
//! `Value`/`Value::Obj` tree on the row data plane.
//!
//! # Why this exists (the boxing #75 left)
//!
//! The typed codegen (#47/#48) materialized structs from an ALREADY-BOXED `Value`:
//!
//! ```ignore
//! fn marshal_T0(raw: &Value) -> Result<T0, BehaviorError>   // raw is a Value::Obj tree
//! ```
//!
//! The consumer's read handler first built the full dynamic `Value` tree and the generated
//! module then WALKED it into a struct — ADDING a `Value`->struct conversion on top of the
//! boxing instead of REPLACING it. The residual ~22% Rust gap #75 measured is exactly that
//! `Value`/`Value::Obj` deep-clone at the handler ABI boundary (Rust felt it most: the
//! `Value` enum boxes every `Obj` pair into a heap `Vec<(String, Value)>` that is cloned at
//! the ABI, then re-walked).
//!
//! bc's conformance handlers return a `Value` mock, so the suite structurally could not see
//! what the consumer's real handler did BEFORE the `Value` reached the module — the blind
//! spot that let bc#47's B2 layering recur one layer out as bc#76.
//!
//! # The seam
//!
//! A RAW handler yields a [`RawValue`] — a native-backed accessor tree that is NOT a
//! `Value`. The generated raw marshaller matches on it (scalar / [`RawRow`] / arr) straight
//! into the concrete struct. The dynamic `Value` tree never exists on the row data plane;
//! boxing is REPLACED, not layered. `RawRow::field` is the monomorphized wire read a
//! hand-written v1-native struct-from-row would do.

use crate::behavior::BehaviorError;
use crate::value::Value;

/// A handler result on the RAW data plane — deliberately NOT a [`Value`]: the whole point of
/// #76 is that the dynamic `Value`/`Value::Obj` tree is never built here. A real consumer's
/// handler returns its native wire shape as a [`RawValue`] with no `Value` allocation.
#[derive(Debug, Clone)]
pub enum RawValue {
    Null,
    Bool(bool),
    Int(i64),
    Float(f64),
    Str(String),
    Arr(Vec<RawValue>),
    /// A native-backed object accessor (see [`RawRow`]) — NOT a `Value::Obj`.
    Row(RawRow),
}

impl RawValue {
    /// Normative type label (symmetric with [`Value::type_name`]) for TYPE_MISMATCH messages.
    pub fn type_name(&self) -> &'static str {
        match self {
            RawValue::Null => "null",
            RawValue::Bool(_) => "bool",
            RawValue::Int(_) => "int",
            RawValue::Float(_) => "float",
            RawValue::Str(_) => "string",
            RawValue::Arr(_) => "arr",
            RawValue::Row(_) => "obj",
        }
    }
}

/// The object accessor on the raw data plane: a direct native field read that materializes
/// NO `Value::Obj`. bc conformance uses this native `Vec`-backed row (no real wire format) so
/// the generated raw marshaller exercises the de-boxed path; a real consumer implements the
/// same shape over its wire payload (e.g. an AttributeValue map).
#[derive(Debug, Clone, Default)]
pub struct RawRow {
    fields: Vec<(String, RawValue)>,
}

impl RawRow {
    pub fn new() -> Self {
        RawRow { fields: Vec::new() }
    }

    /// Insert or update a field, preserving first-insertion order (nested serialization for
    /// the equivalence pin depends on it).
    pub fn set(&mut self, key: impl Into<String>, val: RawValue) {
        let key = key.into();
        if let Some(slot) = self.fields.iter_mut().find(|(k, _)| *k == key) {
            slot.1 = val;
        } else {
            self.fields.push((key, val));
        }
    }

    /// Direct native field read (no `Value::Obj` materialized). Returns `None` when absent so
    /// the raw marshaller can fail closed with MISSING_PROP.
    pub fn field(&self, key: &str) -> Option<&RawValue> {
        self.fields.iter().find(|(k, _)| k == key).map(|(_, v)| v)
    }

    /// Field pairs in insertion order (used by the equivalence-pin adapter only).
    pub fn pairs(&self) -> &[(String, RawValue)] {
        &self.fields
    }
}

/// A RAW handler execution result. `Ok` carries a [`RawValue`] (native-backed), NOT a boxed
/// `Value` — that is the whole de-box.
#[derive(Debug, Clone)]
pub enum RawOutcome {
    Ok(RawValue),
    Error(String),
}

/// The RAW handler registry seam: like `ComponentExec`, but the handler returns a
/// [`RawValue`] (native-backed) instead of a boxed `Value`. A generated raw-ABI module
/// dispatches through this and materializes structs directly from the [`RawValue`] — no
/// dynamic `Value` tree on the row data plane.
pub trait RawComponentExec {
    fn exec_raw(
        &mut self,
        component: &str,
        ports: &[(String, Value)],
        bound: Option<&Value>,
    ) -> Option<RawOutcome>;

    /// ctx-carrying extension (mirrors `ComponentExec::exec_ctx`). Default forwards.
    fn exec_raw_ctx(
        &mut self,
        node_id: &str,
        component: &str,
        ports: &[(String, Value)],
        bound: Option<&Value>,
    ) -> Option<RawOutcome> {
        let _ = node_id;
        self.exec_raw(component, ports, bound)
    }
}

/// Blanket forwarding impl so a trait-object raw handler (`&mut dyn RawComponentExec`)
/// satisfies [`RawComponentExec`] — mirrors the `&mut dyn ComponentExec` blanket (bc#68) so a
/// consumer holding its raw registry behind a trait object drives the generated raw module
/// through the unchanged generic entry.
impl RawComponentExec for &mut dyn RawComponentExec {
    fn exec_raw(
        &mut self,
        component: &str,
        ports: &[(String, Value)],
        bound: Option<&Value>,
    ) -> Option<RawOutcome> {
        (**self).exec_raw(component, ports, bound)
    }

    fn exec_raw_ctx(
        &mut self,
        node_id: &str,
        component: &str,
        ports: &[(String, Value)],
        bound: Option<&Value>,
    ) -> Option<RawOutcome> {
        (**self).exec_raw_ctx(node_id, component, ports, bound)
    }
}

/// Lower a dynamic `Value` into the equivalent native-backed [`RawValue`]. This is the
/// ADAPTER bc's conformance uses to feed the RAW path from the SAME vectors the boxed path
/// uses (so behavior-equality is checked against `run_behavior` on identical data). It is a
/// TEST/BENCH-ADAPTER that lives OFF the hot path: a real consumer never builds a `Value`
/// first — it produces [`RawValue`] from its wire payload directly. The de-box win is
/// measured against v1-native precisely because production skips this step.
pub fn raw_from_value(v: &Value) -> RawValue {
    match v {
        Value::Null => RawValue::Null,
        Value::Bool(b) => RawValue::Bool(*b),
        Value::Int(i) => RawValue::Int(*i),
        Value::Float(f) => RawValue::Float(*f),
        Value::Str(s) => RawValue::Str(s.clone()),
        Value::Arr(xs) => RawValue::Arr(xs.iter().map(raw_from_value).collect()),
        Value::Obj(pairs) => {
            let mut row = RawRow::new();
            for (k, val) in pairs {
                row.set(k.clone(), raw_from_value(val));
            }
            RawValue::Row(row)
        }
    }
}

/// Build a MISSING_PROP `BehaviorError` (symmetric with the boxed marshaller's fail-closed
/// path) for a raw marshaller. Kept here so the generated raw code raises the same coded
/// failure as `run_behavior`.
pub fn raw_missing_prop(struct_name: &str, key: &str) -> BehaviorError {
    crate::expr::ExprFailure {
        code: crate::expr::ExprFailureCode::MissingProp,
        message: format!("typed raw marshal {struct_name}: missing property .{key}"),
    }
    .into()
}

/// Build a TYPE_MISMATCH `BehaviorError` for a raw marshaller.
pub fn raw_type_mismatch(expected: &str, got: &str) -> BehaviorError {
    crate::expr::ExprFailure {
        code: crate::expr::ExprFailureCode::TypeMismatch,
        message: format!("typed raw marshal: expected {expected}, got {got}"),
    }
    .into()
}