behavior-contracts 0.7.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)
    }
}

// ── Native PORTS ABI (bc 0.5.0) ──────────────────────────────────────────────────────────
//
// The RESULT side above (`RawComponentExec`) de-boxed the handler RESULT. The PORTS side of
// every generated componentRef still built a generic key-value container: `vec![("PK"
// .to_string(), val), …]` — a `Vec` alloc + a heap `String` key per port + a boxed `Value`
// per port — then a generic dispatch. That generic key-value plumbing is the residual cost.
//
// The native-ports emitter builds, per node, a NATIVE ports struct (typed fields, direct
// construction, no `Vec`/`String`-key/`Value` boxing) and dispatches it through this seam. The
// struct is passed behind a [`PortReader`] trait object so a consumer reads ports by name
// (boxing one field on demand) or downcasts to its own type on the hot path. The generic
// `ComponentExec(ports: &[(String, Value)])` path stays as an additive fallback.

/// The optional uniform read accessor a generated native ports struct implements so a consumer
/// that does not downcast can still read ports by name. Reading a statically-typed field this
/// way clones/boxes ONLY that one field into a `Value` on demand (no `Vec`, no eager boxing of
/// every port) — a hot-path consumer downcasts (`as_any`) and reads the typed fields directly.
pub trait PortReader {
    /// The `Value` for a port name, and whether it is a declared port of this node.
    fn port(&self, name: &str) -> Option<Value>;
    /// Downcast hook for a consumer that reads the concrete generated struct with zero boxing.
    fn as_any(&self) -> &dyn std::any::Any;
}

/// The NATIVE-PORTS handler registry seam: the handler receives the generated native ports
/// struct behind a [`PortReader`] and returns an `ExecOutcome` (a boxed `Value` RESULT — the
/// UNTYPED straight-line path has no target struct to de-box the result into, and returns the
/// boxed `Value` as the component output, exactly as the generic path does). NO generic
/// `Vec<(String, Value)>` port CONTAINER is built on the port plane — that is the de-plumb.
pub trait NativeComponentExec {
    fn exec_native(
        &mut self,
        component: &str,
        ports: &dyn PortReader,
        bound: Option<&Value>,
    ) -> Option<crate::plan::ExecOutcome>;

    /// ctx-carrying extension (mirrors `ComponentExec::exec_ctx`). Default forwards.
    fn exec_native_ctx(
        &mut self,
        node_id: &str,
        component: &str,
        ports: &dyn PortReader,
        bound: Option<&Value>,
    ) -> Option<crate::plan::ExecOutcome> {
        let _ = node_id;
        self.exec_native(component, ports, bound)
    }
}

/// Blanket forwarding impl so a trait-object native handler (`&mut dyn NativeComponentExec`)
/// satisfies [`NativeComponentExec`] (mirrors the `&mut dyn ComponentExec` blanket, bc#68).
impl NativeComponentExec for &mut dyn NativeComponentExec {
    fn exec_native(
        &mut self,
        component: &str,
        ports: &dyn PortReader,
        bound: Option<&Value>,
    ) -> Option<crate::plan::ExecOutcome> {
        (**self).exec_native(component, ports, bound)
    }

    fn exec_native_ctx(
        &mut self,
        node_id: &str,
        component: &str,
        ports: &dyn PortReader,
        bound: Option<&Value>,
    ) -> Option<crate::plan::ExecOutcome> {
        (**self).exec_native_ctx(node_id, component, ports, bound)
    }
}

// ── Combined NATIVE-ports + RAW-result ABI (bc#77, the 1.0 read de-box) ────────────────────
//
// [`NativeComponentExec`] de-plumbed the PORT container but still returns a boxed `Value`
// RESULT. [`RawComponentExec`] de-boxes the RESULT but keeps the generic `Vec<(String, Value)>`
// PORT container in AND is driven by `run_plan` — whose relation-child skip-gate reads the
// PARENT's boxed result; a struct-native (nil) parent makes typed relationSingle / nestedBatchGet
// diverge from `run_behavior` (graphddb#323 / bc#74).
//
// This seam COMBINES both halves: the handler receives the generated per-node NATIVE ports
// struct (behind [`PortReader`] — no `Vec<(String, Value)>`) AND returns a native [`RawValue`]
// ([`RawOutcome`] — no boxed `Value` result tree). The generated combined read runner drives the
// nodes INLINE (sequential, no `run_plan`): each node's result is materialized DIRECTLY into its
// outType struct via `marshal_raw_T*`, node results live as struct locals, and a relation child
// reads the parent's REAL struct result inline — so relationSingle / connection reads converge
// with `run_behavior`. Zero boxed `Value` end-to-end on both the port AND row plane.

/// The COVERED-path port accessor (bc#94): the by-name read a combined handler uses, WITHOUT the
/// dynamic [`PortReader::as_any`] downcast hook. The generated combined runner passes each node's
/// CONCRETE ports struct to the handler by **direct generic instantiation** (`&P` where
/// `P: PortReaderT`, monomorphized per node) — never behind `&dyn`, never via `as_any`/`downcast`.
/// A `PortReaderT` handler still reads a port by name (boxing that one field on demand) if it does
/// not want to name the concrete struct type, but the SEAM itself carries no `dyn`/`as_any`: the
/// concrete type flows through monomorphization. (`PortReader` — with `as_any` — stays for the
/// UNCOVERED [`NativeComponentExec`] path, retired later.)
pub trait PortReaderT {
    /// The `Value` for a port name, and whether it is a declared port of this node.
    fn port(&self, name: &str) -> Option<Value>;
}

/// The combined handler registry seam (bc#94, nativized): native ports in (a CONCRETE `&P`, passed
/// by monomorphized generic instantiation — no `&dyn`, no `as_any`/`downcast`), [`RawValue`] out.
///
/// `exec_native_raw` takes `&self` (not `&mut self`): the covered runner passes `&H`, so a `Sync`
/// handler parallelizes across the bc#87 static-parallel stage with NO forced `Mutex` serialization
/// (the generic seam is `&self`; a handler needing interior mutation uses its own `Sync` interior
/// type — e.g. an atomic/`Mutex` field — rather than the seam forcing `&mut self` on every caller).
pub trait NativeRawComponentExec {
    fn exec_native_raw<P: PortReaderT + ?Sized>(
        &self,
        component: &str,
        ports: &P,
        bound: Option<&Value>,
    ) -> Option<RawOutcome>;

    /// ctx-carrying extension (mirrors `ComponentExec::exec_ctx`). Default forwards.
    fn exec_native_raw_ctx<P: PortReaderT + ?Sized>(
        &self,
        node_id: &str,
        component: &str,
        ports: &P,
        bound: Option<&Value>,
    ) -> Option<RawOutcome> {
        let _ = node_id;
        self.exec_native_raw(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()
}