sim-lib-pattern 0.2.0

Shape-based pattern matching and destructuring for SIM runtime values.
Documentation
// conformance: one bounded matcher safely serves every admitted pattern dialect.

use sim_lib_pattern::{
    ByteDomain, ByteOffset, CaptureId, CodeUnitDomain, CodeUnitOffset, DomainExecutionOutcome,
    EnginePolicy, ExecutionLimit, GlobPatternDialect, IrNode, LuaPatternDialect, PatternIr,
    RepeatBounds, ScalarDomain, ScalarOffset, TextLimits, compile, execute_bytes,
    execute_code_units, execute_scalars,
};
use sim_text::CodeUnitString;
use std::collections::BTreeMap;

fn limits(max_steps: usize) -> TextLimits {
    TextLimits {
        max_steps,
        ..TextLimits::default()
    }
}

#[test]
fn one_organ_executes_three_distinct_surface_lowerings() {
    let neutral = PatternIr::<ByteDomain, ()>::new(
        IrNode::Repeat {
            node: Box::new(IrNode::Symbol(b'a')),
            bounds: RepeatBounds::new(1, None).unwrap(),
            greedy: true,
        },
        BTreeMap::new(),
        &EnginePolicy::new([]),
    )
    .unwrap();
    let neutral = execute_bytes(&compile(&neutral), b"aaa", limits(128), |_, _| false);
    let DomainExecutionOutcome::Match { matched, .. } = neutral else {
        panic!("neutral IR must match through the shared organ");
    };
    assert_eq!((matched.start, matched.end), (ByteOffset(0), ByteOffset(3)));

    let lua = LuaPatternDialect.compile_ir("^a+$").unwrap();
    let lua = execute_scalars(&compile(&lua), &['a', 'a', 'a'], limits(128), |_, _| false);
    let DomainExecutionOutcome::Match { matched, .. } = lua else {
        panic!("Lua syntax must match through the shared organ");
    };
    assert_eq!(
        (matched.start, matched.end),
        (ScalarOffset::new(0), ScalarOffset::new(3))
    );

    let glob = GlobPatternDialect.compile_ir("a*").unwrap();
    let glob = execute_scalars(&compile(&glob), &['a', 'a', 'a'], limits(128), |_, _| false);
    let DomainExecutionOutcome::Match { matched, .. } = glob else {
        panic!("glob syntax must match through the shared organ");
    };
    assert_eq!(
        (matched.start, matched.end),
        (ScalarOffset::new(0), ScalarOffset::new(3))
    );
}

#[test]
fn adversarial_ambiguity_stops_with_an_exact_bounded_work_receipt() {
    let ambiguous = IrNode::Repeat {
        node: Box::new(IrNode::Alternation(vec![
            IrNode::Symbol(b'a'),
            IrNode::Concat(vec![IrNode::Symbol(b'a'), IrNode::Symbol(b'a')]),
        ])),
        bounds: RepeatBounds::new(0, None).unwrap(),
        greedy: true,
    };
    let ir = PatternIr::<ByteDomain, ()>::new(
        IrNode::Concat(vec![ambiguous, IrNode::Symbol(b'b')]),
        BTreeMap::new(),
        &EnginePolicy::new([]),
    )
    .unwrap();
    let outcome = execute_bytes(&compile(&ir), &vec![b'a'; 256], limits(40), |_, _| false);
    let DomainExecutionOutcome::Limit { limit, receipt } = outcome else {
        panic!("ambiguous rejection must stop at the caller's work boundary");
    };
    assert_eq!(limit, ExecutionLimit::Transitions);
    assert_eq!(receipt.transitions, 40);
    assert!(receipt.state_visits <= 40);
    assert_eq!(receipt.subject_symbols, 256);
}

#[test]
fn offsets_remain_exact_in_all_three_subject_domains() {
    let bytes = PatternIr::<ByteDomain, ()>::new(
        IrNode::Concat(
            "\u{1f600}x"
                .as_bytes()
                .iter()
                .copied()
                .map(IrNode::Symbol)
                .collect(),
        ),
        BTreeMap::new(),
        &EnginePolicy::new([]),
    )
    .unwrap();
    let DomainExecutionOutcome::Match { matched: bytes, .. } = execute_bytes(
        &compile(&bytes),
        "\u{1f600}x".as_bytes(),
        limits(128),
        |_, _| false,
    ) else {
        panic!("byte lowering must match");
    };

    let scalars = PatternIr::<ScalarDomain, ()>::new(
        IrNode::Concat(vec![IrNode::Symbol('\u{1f600}'), IrNode::Symbol('x')]),
        BTreeMap::new(),
        &EnginePolicy::new([]),
    )
    .unwrap();
    let DomainExecutionOutcome::Match {
        matched: scalars, ..
    } = execute_scalars(
        &compile(&scalars),
        &['\u{1f600}', 'x'],
        limits(128),
        |_, _| false,
    )
    else {
        panic!("scalar lowering must match");
    };

    let units = CodeUnitString::from_scalar("\u{1f600}x");
    let code_units = PatternIr::<CodeUnitDomain, ()>::new(
        IrNode::Concat(
            units
                .as_code_units()
                .iter()
                .copied()
                .map(IrNode::Symbol)
                .collect(),
        ),
        BTreeMap::new(),
        &EnginePolicy::new([]),
    )
    .unwrap();
    let DomainExecutionOutcome::Match {
        matched: code_units,
        ..
    } = execute_code_units(&compile(&code_units), &units, limits(128), |_, _| false)
    else {
        panic!("code-unit lowering must match");
    };

    assert_eq!(bytes.end, ByteOffset(5));
    assert_eq!(scalars.end, ScalarOffset::new(2));
    assert_eq!(code_units.end, CodeUnitOffset::new(3));
}

#[test]
fn ordered_capture_precedence_is_stable_and_unsupported_syntax_fails_closed() {
    let first = CaptureId(10);
    let second = CaptureId(20);
    let ir = PatternIr::<ByteDomain, ()>::new(
        IrNode::Alternation(vec![
            IrNode::Capture {
                id: first,
                node: Box::new(IrNode::Symbol(b'a')),
            },
            IrNode::Capture {
                id: second,
                node: Box::new(IrNode::Symbol(b'a')),
            },
        ]),
        BTreeMap::new(),
        &EnginePolicy::new([]),
    )
    .unwrap();
    let DomainExecutionOutcome::Match { matched, .. } =
        execute_bytes(&compile(&ir), b"a", limits(128), |_, _| false)
    else {
        panic!("ordered ambiguity must match");
    };
    assert_eq!(
        matched.captures.keys().copied().collect::<Vec<_>>(),
        [first]
    );

    assert!(LuaPatternDialect.compile_ir("[unterminated").is_err());
    assert!(GlobPatternDialect.compile_ir("[unterminated").is_err());
}