hara-native 0.1.8

HAL-free native host runtime and package launcher for Hara
Documentation
use super::*;

#[test]
fn protocol_resume_path_uses_the_coroutine_evaluator() {
    let fiber = EvalFiber::start(
        "(let [c (std.native.Coroutine/create (fn [] 42))] \
           (std.protocol.icoroutine.ICoroutine/resume c))",
        HashMap::new(),
    )
    .unwrap();
    assert_eq!(fiber.state(), EvalFiberState::Completed(Value::Number(42)));
}

fn keyword(name: &str) -> Value {
    Value::Keyword(crate::lang::data::Keyword::from(name))
}

fn status_of(env: &HashMap<String, Value>, name: &str) -> Value {
    match env.get(name) {
        Some(Value::Var(var)) => match var.deref_value() {
            Value::Coroutine(coroutine) => coroutine_status(&coroutine),
            other => panic!("expected coroutine var, got {other:?}"),
        },
        Some(Value::Coroutine(coroutine)) => coroutine_status(&coroutine),
        other => panic!("expected coroutine binding for {name}, got {other:?}"),
    }
}

#[test]
fn create_makes_suspended_coroutine() {
    let mut f = EvalFiber::start(
        "(std.native.Base/satisfies? std.protocol.icoroutine.ICoroutine (std.native.Coroutine/create (fn [x] x)))",
        HashMap::new(),
    )
    .unwrap();
    assert_eq!(f.state(), EvalFiberState::Completed(Value::Bool(true)));

    let mut f = EvalFiber::start(
        "(std.protocol.icoroutine.ICoroutine/status (std.native.Coroutine/create (fn [x] x)))",
        HashMap::new(),
    )
    .unwrap();
    assert_eq!(f.state(), EvalFiberState::Completed(keyword("suspended")));
}

#[test]
fn resume_runs_body_to_completion() {
    let mut f = EvalFiber::start(
        "(do (def c (std.native.Coroutine/create (fn [x] (* x 2)))) \
         (std.protocol.icoroutine.ICoroutine/resume c 21) \
         (std.protocol.icoroutine.ICoroutine/status c))",
        HashMap::new(),
    )
    .unwrap();
    assert_eq!(f.state(), EvalFiberState::Completed(keyword("dead")));
}

#[test]
fn resume_on_dead_throws() {
    let mut f = EvalFiber::start(
        "(do (def c (std.native.Coroutine/create (fn [] 1))) \
         (std.protocol.icoroutine.ICoroutine/resume c) \
         (std.protocol.icoroutine.ICoroutine/resume c))",
        HashMap::new(),
    )
    .unwrap();
    assert!(matches!(f.state(), EvalFiberState::Failed(e) if e.contains("dead")));
}

#[test]
fn body_error_rethrows_at_resume_and_kills_coroutine() {
    let mut f = EvalFiber::start(
        "(do (def c (std.native.Coroutine/create (fn [] (/ 1 0)))) \
         (std.protocol.icoroutine.ICoroutine/resume c))",
        HashMap::new(),
    )
    .unwrap();
    assert!(matches!(f.state(), EvalFiberState::Failed(_)));
    assert_eq!(status_of(&f.environment(), "c"), keyword("dead"));
}

#[test]
fn yield_exchanges_values_both_ways() {
    let mut f = EvalFiber::start(
        "(do (def c (std.native.Coroutine/create \
         (fn [start] \
           (let [a (std.native.Coroutine/yield (* start start))] \
             (let [b (std.native.Coroutine/yield :second)] \
               [a b]))))) \
         [(std.protocol.icoroutine.ICoroutine/resume c 10) \
          (std.protocol.icoroutine.ICoroutine/resume c :got-a) \
          (std.protocol.icoroutine.ICoroutine/resume c :got-b) \
          (std.protocol.icoroutine.ICoroutine/status c)])",
        HashMap::new(),
    )
    .unwrap();
    assert_eq!(
        f.state(),
        EvalFiberState::Completed(Value::Vector(
            vec![
                Value::Number(100),
                keyword("second"),
                Value::Vector(vec![keyword("got-a"), keyword("got-b")].into()),
                keyword("dead"),
            ]
            .into()
        ))
    );
}

#[test]
fn multi_arg_resume_delivers_vector_to_yield() {
    let mut f = EvalFiber::start(
        "(do (def c (std.native.Coroutine/create \
         (fn [] (let [v (std.native.Coroutine/yield :first)] v)))) \
         [(std.protocol.icoroutine.ICoroutine/resume c) \
          (std.protocol.icoroutine.ICoroutine/resume c 9 8) \
          (std.protocol.icoroutine.ICoroutine/status c)])",
        HashMap::new(),
    )
    .unwrap();
    assert_eq!(
        f.state(),
        EvalFiberState::Completed(Value::Vector(
            vec![
                keyword("first"),
                Value::Vector(vec![Value::Number(9), Value::Number(8)].into()),
                keyword("dead"),
            ]
            .into()
        ))
    );
}

#[test]
fn close_on_never_resumed_coroutine() {
    let mut f = EvalFiber::start(
        "(do (def c (std.native.Coroutine/create (fn [] :never-runs))) \
         (std.native.Base/satisfies? std.protocol.icoroutine.ICoroutine (std.protocol.iclose.IClose/close c)) \
         (std.protocol.icoroutine.ICoroutine/status c))",
        HashMap::new(),
    )
    .unwrap();
    assert_eq!(f.state(), EvalFiberState::Completed(keyword("dead")));
}

#[test]
fn yield_requires_one_argument() {
    let mut f = EvalFiber::start("(std.native.Coroutine/yield 1 2 3)", HashMap::new()).unwrap();
    assert_eq!(
        f.state(),
        EvalFiberState::Failed("function expects 1 arguments".into())
    );
}

#[test]
fn yield_works_from_nested_helper() {
    let mut f = EvalFiber::start(
        "(do (defn helper-n [x] (std.native.Coroutine/yield (* x 10))) \
         (def c (std.native.Coroutine/create (fn [] (helper-n 3) :end))) \
         [(std.protocol.icoroutine.ICoroutine/resume c) \
          (std.protocol.icoroutine.ICoroutine/resume c) \
          (std.protocol.icoroutine.ICoroutine/status c)])",
        HashMap::new(),
    )
    .unwrap();
    assert_eq!(
        f.state(),
        EvalFiberState::Completed(Value::Vector(
            vec![Value::Number(30), keyword("end"), keyword("dead"),].into()
        ))
    );
}

#[test]
fn yield_outside_coroutine_throws() {
    let mut f = EvalFiber::start("(std.native.Coroutine/yield 1)", HashMap::new()).unwrap();
    assert!(matches!(f.state(), EvalFiberState::Failed(e) if e.contains("outside")));
}

#[test]
fn reentrant_resume_throws() {
    let mut f = EvalFiber::start(
        "(do (def c nil) \
         (set! c (std.native.Coroutine/create (fn [] (std.protocol.icoroutine.ICoroutine/resume c)))) \
         (std.protocol.icoroutine.ICoroutine/resume c))",
        HashMap::new(),
    )
    .unwrap();
    assert!(matches!(f.state(), EvalFiberState::Failed(e) if e.contains("running")));
}

#[test]
fn nested_coroutines_resume_each_other() {
    let mut f = EvalFiber::start(
        "(do (def c-inner (std.native.Coroutine/create \
         (fn [] (std.native.Coroutine/yield :inner-yield) :inner-end))) \
         (def c-outer (std.native.Coroutine/create \
         (fn [] \
           (std.native.Coroutine/yield (std.protocol.icoroutine.ICoroutine/resume c-inner)) \
           (std.native.Coroutine/yield (std.protocol.icoroutine.ICoroutine/resume c-inner :x)) \
           :outer-end))) \
         [(std.protocol.icoroutine.ICoroutine/resume c-outer) \
          (std.protocol.icoroutine.ICoroutine/resume c-outer) \
          (std.protocol.icoroutine.ICoroutine/resume c-outer) \
          (std.protocol.icoroutine.ICoroutine/status c-outer)])",
        HashMap::new(),
    )
    .unwrap();
    assert_eq!(
        f.state(),
        EvalFiberState::Completed(Value::Vector(
            vec![
                keyword("inner-yield"),
                keyword("inner-end"),
                keyword("outer-end"),
                keyword("dead"),
            ]
            .into()
        ))
    );
}

#[test]
fn generator_pipeline_produces_lazily() {
    let mut f = EvalFiber::start(
        "(do (def c (std.native.Coroutine/create \
         (fn [n] (loop [i 0] \
           (if (< i n) \
             (do (std.native.Coroutine/yield (* i i)) (recur (+ i 1))) \
             :done))))) \
         [(std.protocol.icoroutine.ICoroutine/resume c 3) \
          (std.protocol.icoroutine.ICoroutine/resume c) \
          (std.protocol.icoroutine.ICoroutine/resume c) \
          (std.protocol.icoroutine.ICoroutine/resume c) \
          (std.protocol.icoroutine.ICoroutine/status c)])",
        HashMap::new(),
    )
    .unwrap();
    assert_eq!(
        f.state(),
        EvalFiberState::Completed(Value::Vector(
            vec![
                Value::Number(0),
                Value::Number(1),
                Value::Number(4),
                keyword("done"),
                keyword("dead"),
            ]
            .into()
        ))
    );
}

#[test]
fn await_returns_settled_promise_value() {
    let mut f = EvalFiber::start(
        "(do (def c (std.native.Coroutine/create \
         (fn [] (std.native.Coroutine/await (std.native.Promise/delay 50 (fn [] :delayed-value)))))) \
         (std.protocol.icoroutine.ICoroutine/resume c))",
        HashMap::new(),
    )
    .unwrap();
    assert_eq!(f.state(), EvalFiberState::Suspended);

    let promise = f.pending().unwrap();
    promise.resolve(keyword("delayed-value"));
    assert_eq!(
        f.resume(promise.state()),
        EvalFiberState::Completed(keyword("delayed-value"))
    );
    assert_eq!(status_of(&f.environment(), "c"), keyword("dead"));
}

#[test]
fn await_rethrows_promise_rejection() {
    let mut f = EvalFiber::start(
        "(do (def c (std.native.Coroutine/create \
         (fn [] (std.native.Coroutine/await (std.native.Promise/run (fn [] (/ 1 0))))))) \
         (std.protocol.icoroutine.ICoroutine/resume c))",
        HashMap::new(),
    )
    .unwrap();
    assert!(
        matches!(f.state(), EvalFiberState::Failed(e) if e.contains("Promise rejected") || e.contains("division") || e.contains("zero"))
    );
    assert_eq!(status_of(&f.environment(), "c"), keyword("dead"));
}

#[test]
fn yield_passes_promise_object_without_awaiting() {
    let mut f = EvalFiber::start(
        "(do (def p (std.native.Promise/new (fn [resolve reject] nil))) \
         (def c (std.native.Coroutine/create \
           (fn [] (std.native.Coroutine/yield p)))) \
         [(std.protocol.icoroutine.ICoroutine/resume c) \
          (std.protocol.icoroutine.ICoroutine/status c)])",
        HashMap::new(),
    )
    .unwrap();
    let state = f.state();
    assert!(
        matches!(&state, EvalFiberState::Completed(Value::Tuple(t)) if t.len() == 2),
        "expected completed 2-tuple, got {:?}",
        state
    );
    if let EvalFiberState::Completed(Value::Tuple(t)) = state {
        assert!(
            matches!(t.get(0), Some(Value::Promise(_))),
            "expected yielded value to be the promise object, got {:?}",
            t.get(0)
        );
        assert_eq!(t.get(1), Some(&keyword("suspended")));
    }
}

#[test]
fn yield_awaits_promise_when_composed_with_await() {
    let mut f = EvalFiber::start(
        "(do (def p (std.native.Promise/new (fn [resolve reject] nil))) \
         (def c (std.native.Coroutine/create \
           (fn [] (std.native.Coroutine/yield (std.native.Coroutine/await p))))) \
         (std.protocol.icoroutine.ICoroutine/resume c))",
        HashMap::new(),
    )
    .unwrap();
    assert_eq!(f.state(), EvalFiberState::Suspended);

    let promise = f.pending().unwrap();
    promise.resolve(keyword("resolved-value"));
    assert_eq!(
        f.resume(promise.state()),
        EvalFiberState::Completed(keyword("resolved-value"))
    );
    assert_eq!(status_of(&f.environment(), "c"), keyword("suspended"));
}