sim-lib-control 0.1.0-rc.1

SIM workspace package for sim lib control.
Documentation
use std::sync::Arc;

use sim_kernel::{
    Cx, Datum, DatumStore, DefaultFactory, NoopEvalPolicy, Ref, Symbol,
    capability::{control_capture_capability, control_resume_capability},
    control::{
        control_aborted_status, control_captured_status, control_result_status,
        control_resumed_status,
    },
};

use super::*;

fn cx() -> Cx {
    let mut cx = Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
    install_control_policy(&mut cx);
    cx
}

fn symbol_ref(namespace: &str, name: &str) -> Ref {
    Ref::Symbol(Symbol::qualified(namespace, name))
}

#[test]
fn condition_signal_uses_nearest_dynamic_handler_and_control_capture() {
    let mut cx = cx();
    cx.grant(control_capture_capability());
    let condition_kind = Symbol::qualified("condition", "warning");
    let mut stack = ConditionStack::new();
    stack.push(ConditionHandler::new(
        condition_kind.clone(),
        symbol_ref("prompt", "outer"),
        symbol_ref("handler", "outer"),
    ));
    stack.push(ConditionHandler::new(
        Symbol::qualified("condition", "other"),
        symbol_ref("prompt", "other"),
        symbol_ref("handler", "other"),
    ));
    stack.push(ConditionHandler::new(
        condition_kind.clone(),
        symbol_ref("prompt", "inner"),
        symbol_ref("handler", "inner"),
    ));

    let continuation = signal_condition(
        &mut cx,
        &stack,
        Condition::new(condition_kind, symbol_ref("payload", "warning")),
    )
    .unwrap();

    assert_eq!(continuation.continuation(), &symbol_ref("handler", "inner"));
    assert_eq!(
        control_result_status(&cx, continuation.capture_result()).unwrap(),
        Some(control_captured_status())
    );
    assert_eq!(cx.effect_ledger().records().len(), 1);
}

#[test]
fn restart_resumes_at_signaling_site() {
    let mut cx = cx();
    cx.grant(control_capture_capability());
    cx.grant(control_resume_capability());
    let condition_kind = Symbol::qualified("condition", "missing-value");
    let restart_name = Symbol::qualified("restart", "use-value");
    let mut handlers = ConditionStack::new();
    handlers.push(ConditionHandler::new(
        condition_kind.clone(),
        symbol_ref("prompt", "condition"),
        symbol_ref("site", "signal"),
    ));
    let continuation = handlers
        .signal(
            &mut cx,
            Condition::new(condition_kind, symbol_ref("payload", "missing")),
        )
        .unwrap();
    let mut restarts = RestartStack::new();
    restarts.push(Restart::new(restart_name.clone(), continuation));

    let result = invoke_restart(
        &mut cx,
        &restarts,
        &restart_name,
        symbol_ref("value", "replacement"),
    )
    .unwrap();

    assert_eq!(
        control_result_status(&cx, result.reference()).unwrap(),
        Some(control_resumed_status())
    );
    assert_eq!(
        control_result_symbol_field(&cx, result.reference(), "continuation"),
        Some(Symbol::qualified("site", "signal"))
    );
}

#[test]
fn generator_yields_finite_sequence_and_reports_exhaustion() {
    let mut generator = Generator::new(vec![
        symbol_ref("item", "one"),
        symbol_ref("item", "two"),
        symbol_ref("item", "three"),
    ]);

    assert_eq!(
        generator.next_step(),
        GeneratorStep::Yielded(symbol_ref("item", "one"))
    );
    assert_eq!(
        generator.next_step(),
        GeneratorStep::Yielded(symbol_ref("item", "two"))
    );
    assert_eq!(
        generator.next_step(),
        GeneratorStep::Yielded(symbol_ref("item", "three"))
    );
    assert_eq!(generator.next_step(), GeneratorStep::Exhausted);
    assert!(generator.is_exhausted());
}

#[test]
fn coroutine_alternates_deterministically() {
    let mut coroutine = Coroutine::alternating(
        vec![symbol_ref("first", "one"), symbol_ref("first", "two")],
        vec![symbol_ref("second", "one"), symbol_ref("second", "two")],
    );

    assert_eq!(
        coroutine.resume(),
        CoroutineStep::Yielded {
            lane: CoroutineLane::First,
            value: symbol_ref("first", "one")
        }
    );
    assert_eq!(
        coroutine.resume(),
        CoroutineStep::Yielded {
            lane: CoroutineLane::Second,
            value: symbol_ref("second", "one")
        }
    );
    assert_eq!(
        coroutine.resume(),
        CoroutineStep::Yielded {
            lane: CoroutineLane::First,
            value: symbol_ref("first", "two")
        }
    );
    assert_eq!(
        coroutine.resume(),
        CoroutineStep::Yielded {
            lane: CoroutineLane::Second,
            value: symbol_ref("second", "two")
        }
    );
    assert_eq!(coroutine.resume(), CoroutineStep::Exhausted);
}

#[test]
fn break_next_and_return_escape_to_matching_labeled_prompt() {
    let label = Symbol::qualified("label", "loop");
    let exits = [
        NonLocalExit::break_to(label.clone(), symbol_ref("value", "break")),
        NonLocalExit::next_to(label.clone(), symbol_ref("value", "next")),
        NonLocalExit::return_to(label.clone(), symbol_ref("value", "return")),
    ];

    for exit in exits {
        let mut cx = cx();
        cx.grant(control_capture_capability());
        let prompts = vec![
            LabeledPrompt::new(label.clone(), symbol_ref("prompt", "outer")),
            LabeledPrompt::new(
                Symbol::qualified("label", "other"),
                symbol_ref("prompt", "other"),
            ),
            LabeledPrompt::new(label.clone(), symbol_ref("prompt", "inner")),
        ];

        let result = escape_to_label(&mut cx, &prompts, exit).unwrap();

        assert_eq!(
            control_result_status(&cx, result.reference()).unwrap(),
            Some(control_aborted_status())
        );
        assert_eq!(
            control_result_symbol_field(&cx, result.reference(), "prompt"),
            Some(Symbol::qualified("prompt", "inner"))
        );
    }
}

#[test]
fn async_task_reports_pending_then_ready() {
    let mut task = AsyncTask::ready_after(2, symbol_ref("async", "ready"));

    assert_eq!(task.poll(), AsyncPoll::Pending);
    assert_eq!(task.poll(), AsyncPoll::Pending);
    assert_eq!(task.poll(), AsyncPoll::Ready(symbol_ref("async", "ready")));
}

#[test]
fn backtracker_walks_choices_and_then_fails_closed() {
    let mut backtracker = Backtracker::new(vec![
        symbol_ref("choice", "first"),
        symbol_ref("choice", "second"),
    ]);

    assert_eq!(
        backtracker.choose(),
        BacktrackStep::Choice(symbol_ref("choice", "first"))
    );
    assert_eq!(
        backtracker.fail(),
        BacktrackStep::Choice(symbol_ref("choice", "second"))
    );
    assert_eq!(backtracker.fail(), BacktrackStep::Failed);
}

fn control_result_symbol_field(cx: &Cx, result: &Ref, field_name: &str) -> Option<Symbol> {
    let Ref::Content(id) = result else {
        return None;
    };
    let Some(Datum::Node { fields, .. }) = cx.datum_store().get(id).unwrap() else {
        return None;
    };
    let field = fields
        .iter()
        .find_map(|(name, value)| (name == &Symbol::new(field_name)).then_some(value))?;
    let Datum::Node {
        fields: ref_fields, ..
    } = field
    else {
        return None;
    };
    ref_fields.iter().find_map(|(name, value)| {
        if name == &Symbol::new("symbol")
            && let Datum::Symbol(symbol) = value
        {
            return Some(symbol.clone());
        }
        None
    })
}