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
//! primitives.rs — codegen primitive surface (bc#36) のユニットテスト。
//!
//! 各式演算子 primitive が「正常系 + fail-closed エッジ」で `evaluate_expression`(SSoT)と
//! 完全一致することを差分検証する。さらに map_with_concurrency(順序保存・bounded)/
//! unproduced helper / hydrate_into を検証する。

use behavior_contracts::primitives as p;
use behavior_contracts::{
    deep_equals, evaluate_expression, hydrate_into, map_with_concurrency, skip_connection,
    skip_single, unproduced_value, Value,
};
use serde_json::json;

fn scope(pairs: &[(&str, Value)]) -> Vec<(String, Value)> {
    pairs
        .iter()
        .map(|(k, v)| (k.to_string(), v.clone()))
        .collect()
}

/// Value 等価アサート(Value は PartialEq 非実装のため deep_equals を使う)。
fn assert_value_eq(a: &Value, b: &Value) {
    assert!(deep_equals(a, b), "value mismatch");
}

/// primitive 結果が同一 IR を evaluate_expression に流した結果と一致することを主張する。
fn same(
    actual: Result<Value, behavior_contracts::ExprFailure>,
    node: serde_json::Value,
    sc: &[(String, Value)],
) {
    let expected = evaluate_expression(&node, sc);
    match (actual, expected) {
        (Ok(a), Ok(e)) => assert!(behavior_contracts::deep_equals(&a, &e), "value mismatch"),
        (Err(a), Err(e)) => assert_eq!(a.code.as_str(), e.code.as_str()),
        _ => panic!("ok/err disposition mismatch"),
    }
}

/// 両経路が同一 Failure code で fail-closed することを主張する。
fn both_fail(actual: Result<Value, behavior_contracts::ExprFailure>, node: serde_json::Value) {
    both_fail_sc(actual, node, &[]);
}

fn both_fail_sc(
    actual: Result<Value, behavior_contracts::ExprFailure>,
    node: serde_json::Value,
    sc: &[(String, Value)],
) {
    let a = actual.expect_err("primitive did not fail");
    let e = evaluate_expression(&node, sc).expect_err("evaluate did not fail");
    assert_eq!(a.code.as_str(), e.code.as_str());
}

// bc no-ir: native primitive は被演算子を**評価済み Value** で受ける(IR は渡さない)。各 native
// primitive の結果が「同じ意味の IR を evaluate_expression に流した結果」と値・Failure code とも
// 一致することを差分検証する(意味論 SSoT は evaluate — native core は同一実装)。

#[test]
fn literals_and_refs_match() {
    let sc = scope(&[
        ("a", Value::Obj(vec![("b".into(), Value::Int(7))])),
        ("n", Value::Null),
    ]);
    same(p::int_lit_native("42"), json!({"int": "42"}), &sc);
    same(p::float_lit_native(3.5), json!({"float": 3.5}), &sc);
    // bare number 分類(整数値→int / 小数→float)。
    same(p::number_lit_native(42.0), json!(42), &sc);
    same(p::number_lit_native(2.5), json!(2.5), &sc);
    same(
        p::r#ref_native(&["a", "b"], &sc),
        json!({"ref": ["a", "b"]}),
        &sc,
    );
    same(
        p::ref_opt_native(&["n", "x"], &sc),
        json!({"refOpt": ["n", "x"]}),
        &sc,
    );
    // obj/arr の被要素は評価済み Value。
    same(
        p::obj_native(vec![("x".into(), Value::Int(1))]),
        json!({"obj": {"x": {"int": "1"}}}),
        &sc,
    );
    same(
        p::arr_native(vec![Value::Int(1), Value::Int(2)]),
        json!({"arr": [{"int": "1"}, {"int": "2"}]}),
        &sc,
    );
    // __proto__ は fail-closed(FORBIDDEN_KEY)。
    both_fail(
        p::obj_native(vec![("__proto__".into(), Value::Int(1))]),
        json!({"obj": {"__proto__": {"int": "1"}}}),
    );
}

#[test]
fn arithmetic_match_and_fail_closed() {
    let e: &[(String, Value)] = &[];
    same(
        p::add_native(Value::Int(1), Value::Int(2)),
        json!({"add": [{"int": "1"}, {"int": "2"}]}),
        e,
    );
    same(
        p::sub_native(Value::Int(5), Value::Int(3)),
        json!({"sub": [{"int": "5"}, {"int": "3"}]}),
        e,
    );
    same(
        p::mul_native(Value::Int(4), Value::Int(6)),
        json!({"mul": [{"int": "4"}, {"int": "6"}]}),
        e,
    );
    same(
        p::neg_native(Value::Int(9)),
        json!({"neg": [{"int": "9"}]}),
        e,
    );
    same(
        p::div_native(Value::Int(7), Value::Int(2)),
        json!({"div": [{"int": "7"}, {"int": "2"}]}),
        e,
    );
    same(
        p::mod_native(Value::Int(7), Value::Int(3)),
        json!({"mod": [{"int": "7"}, {"int": "3"}]}),
        e,
    );

    both_fail(
        p::add_native(Value::Int(9223372036854775807), Value::Int(1)),
        json!({"add": [{"int": "9223372036854775807"}, {"int": "1"}]}),
    );
    both_fail(
        p::mod_native(Value::Int(1), Value::Int(0)),
        json!({"mod": [{"int": "1"}, {"int": "0"}]}),
    );
    both_fail(
        p::add_native(Value::Int(1), Value::Float(2.5)),
        json!({"add": [{"int": "1"}, {"float": 2.5}]}),
    );
    both_fail(
        p::div_native(Value::Int(9007199254740993), Value::Int(1)),
        json!({"div": [{"int": "9007199254740993"}, {"int": "1"}]}),
    );
}

#[test]
fn comparison_bool_len_match() {
    let e: &[(String, Value)] = &[];
    same(
        p::eq_native(Value::Int(1), Value::Int(1)),
        json!({"eq": [{"int": "1"}, {"int": "1"}]}),
        e,
    );
    same(
        p::ne_native(Value::Int(1), Value::Int(2)),
        json!({"ne": [{"int": "1"}, {"int": "2"}]}),
        e,
    );
    same(
        p::lt_native(Value::Str("a".into()), Value::Str("b".into())),
        json!({"lt": ["a", "b"]}),
        e,
    );
    same(
        p::le_native(Value::Str("a".into()), Value::Str("a".into())),
        json!({"le": ["a", "a"]}),
        e,
    );
    same(
        p::gt_native(Value::Int(3), Value::Int(2)),
        json!({"gt": [{"int": "3"}, {"int": "2"}]}),
        e,
    );
    same(
        p::ge_native(Value::Int(2), Value::Int(2)),
        json!({"ge": [{"int": "2"}, {"int": "2"}]}),
        e,
    );
    same(p::not_native(Value::Bool(true)), json!({"not": [true]}), e);
    same(
        p::len_native(Value::Arr(vec![Value::Bool(true), Value::Bool(false)])),
        json!({"len": [{"arr": [true, false]}]}),
        e,
    );

    both_fail(
        p::len_native(Value::Str("nope".into())),
        json!({"len": ["nope"]}),
    );
    both_fail(p::not_native(Value::Int(1)), json!({"not": [{"int": "1"}]}));
    let sc = scope(&[("n", Value::Null)]);
    both_fail_sc(
        p::r#ref_native(&["n", "x"], &sc),
        json!({"ref": ["n", "x"]}),
        &sc,
    );
}

#[test]
fn require_bool_native_fail_closed() {
    // 短絡(and/or/cond/coalesce)は primitive ではなく生成コードの native 制御フローで綴じられ、
    // その意味論(採用側のみ評価)は codegen conformance(generated ≡ run_behavior)で pin される。
    // ここでは制御フローが使う bool ゲートだけを検証する: bool は素通し、非 bool は TYPE_MISMATCH。
    assert!(p::require_bool_native(Value::Bool(true), "and").unwrap());
    assert!(!p::require_bool_native(Value::Bool(false), "or").unwrap());
    assert_eq!(
        p::require_bool_native(Value::Int(1), "cond")
            .expect_err("non-bool must fail")
            .code
            .as_str(),
        "TYPE_MISMATCH",
    );
}

#[test]
fn map_with_concurrency_order_and_bound() {
    use std::sync::atomic::{AtomicUsize, Ordering};
    let items: Vec<i64> = (0..8).collect();
    let in_flight = AtomicUsize::new(0);
    let max_in_flight = AtomicUsize::new(0);
    let out = map_with_concurrency(&items, 3, |x, _i| {
        let cur = in_flight.fetch_add(1, Ordering::SeqCst) + 1;
        max_in_flight.fetch_max(cur, Ordering::SeqCst);
        std::thread::sleep(std::time::Duration::from_millis(5));
        in_flight.fetch_sub(1, Ordering::SeqCst);
        x * 10
    });
    assert_eq!(out, items.iter().map(|x| x * 10).collect::<Vec<_>>()); // order preserved
    assert!(max_in_flight.load(Ordering::SeqCst) <= 3);
}

#[test]
fn map_with_concurrency_degenerate() {
    assert_eq!(
        map_with_concurrency(&[1, 2, 3], 1, |x, _i| x + 1),
        vec![2, 3, 4]
    );
    let empty: Vec<i64> = vec![];
    assert_eq!(
        map_with_concurrency(&empty, 4, |x, _i| *x),
        Vec::<i64>::new()
    );
}

#[test]
fn skip_unproduced_helpers() {
    let conn = Value::Obj(vec![
        ("items".into(), Value::Arr(vec![])),
        ("cursor".into(), Value::Null),
    ]);
    assert_value_eq(
        &unproduced_value(Some(behavior_contracts::RelationKind::Single)),
        &Value::Null,
    );
    assert_value_eq(
        &unproduced_value(Some(behavior_contracts::RelationKind::Connection)),
        &conn,
    );
    assert_value_eq(&unproduced_value(None), &Value::Null);
    assert_value_eq(&skip_single(), &Value::Null);
    assert_value_eq(&skip_connection(), &conn);
}

#[test]
fn hydrate_into_zip_attach() {
    let over = vec![
        Value::Obj(vec![("id".into(), Value::Int(1))]),
        Value::Obj(vec![("id".into(), Value::Int(2))]),
        Value::Obj(vec![("id".into(), Value::Int(3))]),
    ];
    let out = hydrate_into(
        &over,
        "tag",
        &[0, 2],
        &[Value::Str("A".into()), Value::Str("C".into())],
    )
    .unwrap();
    let expected = [
        Value::Obj(vec![
            ("id".into(), Value::Int(1)),
            ("tag".into(), Value::Str("A".into())),
        ]),
        Value::Obj(vec![("id".into(), Value::Int(2))]),
        Value::Obj(vec![
            ("id".into(), Value::Int(3)),
            ("tag".into(), Value::Str("C".into())),
        ]),
    ];
    assert_eq!(out.len(), expected.len());
    for (a, b) in out.iter().zip(expected.iter()) {
        assert_value_eq(a, b);
    }
    // fail-closed: augment target not an object
    let bad = hydrate_into(&[Value::Int(42)], "tag", &[0], &[Value::Str("x".into())]);
    assert!(bad.is_err());
}

// ── (f) 脱解釈ネイティブ面(A2 rust straight-line): evaluate と同一意味論を pin ──────
// ref_native / ref_opt_native / concat_native は `evaluate` を呼ばない実装だが、値・
// Failure code とも `ref`/`refOpt`/`concat` の evaluate アームと一致しなければならない。

#[test]
fn ref_native_matches_evaluate() {
    let sc = scope(&[(
        "obj",
        Value::Obj(vec![
            (
                "a".into(),
                Value::Obj(vec![("b".into(), Value::Str("deep".into()))]),
            ),
            ("nil".into(), Value::Null),
            ("scalar".into(), Value::Int(7)),
        ]),
    )]);
    // 正常系: scope→field を歩く。
    same(
        p::ref_native(&["obj", "a", "b"], &sc),
        json!({"ref":["obj","a","b"]}),
        &sc,
    );
    // UNKNOWN_BINDING
    same(
        p::ref_native(&["missing"], &sc),
        json!({"ref":["missing"]}),
        &sc,
    );
    // MISSING_PROP
    same(
        p::ref_native(&["obj", "a", "zzz"], &sc),
        json!({"ref":["obj","a","zzz"]}),
        &sc,
    );
    // NULL_REF(ref)
    same(
        p::ref_native(&["obj", "nil", "x"], &sc),
        json!({"ref":["obj","nil","x"]}),
        &sc,
    );
    // TYPE_MISMATCH(scalar への .field)
    same(
        p::ref_native(&["obj", "scalar", "x"], &sc),
        json!({"ref":["obj","scalar","x"]}),
        &sc,
    );
    // refOpt の null 伝播
    same(
        p::ref_opt_native(&["obj", "nil", "x"], &sc),
        json!({"refOpt":["obj","nil","x"]}),
        &sc,
    );
    same(
        p::ref_opt_native(&["obj", "a", "b"], &sc),
        json!({"refOpt":["obj","a","b"]}),
        &sc,
    );
}

#[test]
fn concat_native_matches_evaluate() {
    let sc = scope(&[]);
    // 正常系: code-point 連結。
    same(
        p::concat_native(&[
            Value::Str("a".into()),
            Value::Str("b".into()),
            Value::Str("c".into()),
        ]),
        json!({"concat":["a","b","c"]}),
        &sc,
    );
    // TYPE_MISMATCH(非 string 部分)。
    same(
        p::concat_native(&[Value::Str("a".into()), Value::Int(1)]),
        json!({"concat":["a",{"int":"1"}]}),
        &sc,
    );
    // arity<2 は InvalidNode。
    same(
        p::concat_native(&[Value::Str("a".into())]),
        json!({"concat":["a"]}),
        &sc,
    );
}