use axon::cancel_token::CancellationFlag;
use axon::flow_dispatcher::{dispatch_node, DispatchCtx, DispatchError, NodeOutcome};
use axon::flow_execution_event::FlowExecutionEvent;
use axon::ir_nodes::IRFlow;
use tokio::sync::mpsc;
fn flow_from_source(src: &str, name: &str) -> IRFlow {
let tokens = axon_frontend::lexer::Lexer::new(src, "gate.axon")
.tokenize()
.expect("lex");
let prog = axon_frontend::parser::Parser::new(tokens)
.parse()
.expect("the published surface must parse");
let ir = axon_frontend::ir_generator::IRGenerator::new().generate(&prog);
ir.flows
.into_iter()
.find(|f| f.name == name)
.unwrap_or_else(|| panic!("no flow named {name} in the generated IR"))
}
fn ctx() -> (DispatchCtx, mpsc::UnboundedReceiver<FlowExecutionEvent>) {
let (tx, rx) = mpsc::unbounded_channel();
(
DispatchCtx::new("F", "stub", "", CancellationFlag::new(), tx),
rx,
)
}
async fn run(src: &str, name: &str) -> (DispatchCtx, Vec<String>) {
let flow = flow_from_source(src, name);
let (mut c, mut rx) = ctx();
for node in &flow.steps {
dispatch_node(node, &mut c)
.await
.unwrap_or_else(|e| panic!("dispatch failed: {e:?}"));
}
let mut slugs = Vec::new();
while let Ok(ev) = rx.try_recv() {
if let FlowExecutionEvent::StepStart { step_type, .. } = ev {
slugs.push(step_type);
}
}
(c, slugs)
}
async fn run_expecting_error(src: &str, name: &str) -> DispatchError {
let flow = flow_from_source(src, name);
let (mut c, _rx) = ctx();
for node in &flow.steps {
match dispatch_node(node, &mut c).await {
Ok(_) => {}
Err(e) => return e,
}
}
panic!("expected the dispatch to FAIL CLOSED, but every node completed");
}
#[tokio::test]
async fn c1_the_handle_body_actually_runs() {
let (c, slugs) = run(
r#"
effect SSE { Emit(t: Text) -> Unit }
flow F() -> Text {
handle SSE { Emit(t) -> { resume() } } in {
let marker = "the body ran"
}
}
"#,
"F",
)
.await;
assert_eq!(
c.let_bindings.get("marker").map(String::as_str),
Some("the body ran"),
"the `in {{ … }}` block must EXECUTE. If this is unbound the handler \
parsed, lowered, dispatched — and ran nothing, which is the whole \
defect class v2.87.0 exists to not rebuild"
);
assert!(slugs.contains(&"handle".to_string()), "the wire must name the frame: {slugs:?}");
}
#[tokio::test]
async fn c2_perform_reaches_the_clause_and_the_clause_sees_the_value() {
let (c, slugs) = run(
r#"
effect SSE { Emit(t: Text) -> Unit }
flow F() -> Text {
let payload = "forty-two"
handle SSE {
Emit(token) -> {
let seen = token
resume()
}
} in {
perform Emit(payload)
}
}
"#,
"F",
)
.await;
assert_eq!(
c.let_bindings.get("seen").map(String::as_str),
Some("forty-two"),
"the clause body must RUN, and its parameter must be bound to the \
RESOLVED argument — not to the name `payload`"
);
assert!(slugs.contains(&"perform".to_string()), "the wire must name the perform: {slugs:?}");
}
#[tokio::test]
async fn c4_resume_returns_control_to_the_perform_site() {
let (c, _) = run(
r#"
effect SSE { Emit(t: Text) -> Unit }
flow F() -> Text {
handle SSE {
Emit(token) -> {
let during = "handler"
resume()
}
} in {
perform Emit("x")
let after = "continuation resumed"
}
}
"#,
"F",
)
.await;
assert_eq!(c.let_bindings.get("during").map(String::as_str), Some("handler"));
assert_eq!(
c.let_bindings.get("after").map(String::as_str),
Some("continuation resumed"),
"the statement AFTER the perform must run — that is what `resume` \
means. Its absence would be a dropped continuation with no diagnostic"
);
}
#[tokio::test]
async fn c4b_resume_carries_its_value_to_the_perform_site() {
let flow = flow_from_source(
r#"
effect Ask { Get() -> Text }
flow F() -> Text {
let answer = "from the handler"
handle Ask { Get() -> { resume(answer) } } in {
perform Get()
}
}
"#,
"F",
);
let (mut c, _rx) = ctx();
dispatch_node(&flow.steps[0], &mut c).await.expect("let");
let outcome = dispatch_node(&flow.steps[1], &mut c).await.expect("handle");
match outcome {
NodeOutcome::Completed { output, .. } => assert_eq!(
output, "from the handler",
"`resume(v)` must deliver `v` to the perform site"
),
other => panic!("expected a completion, got {other:?}"),
}
}
#[tokio::test]
async fn c5_a_clause_without_resume_aborts_the_handle() {
let flow = flow_from_source(
r#"
effect SSE { Done() -> Never }
flow F() -> Text {
handle SSE {
Done() -> { let closed = "handler ran" }
} in {
perform Done()
let unreachable = "must not run"
}
}
"#,
"F",
);
let (mut c, _rx) = ctx();
let outcome = dispatch_node(&flow.steps[0], &mut c).await.expect("handle");
assert_eq!(
c.let_bindings.get("closed").map(String::as_str),
Some("handler ran"),
"the clause itself must run"
);
assert!(
!c.let_bindings.contains_key("unreachable"),
"a clause that never resumes DROPS the continuation — the rest of the \
handle body must not run. Running it would resume a continuation the \
handler declined to invoke"
);
match outcome {
NodeOutcome::Completed { output, .. } => assert_eq!(
output, "handler ran",
"the handle yields the clause's last output as its own"
),
other => panic!(
"the implicit abort must be claimed BY THIS HANDLE and converted to \
a completion; it escaped as {other:?}"
),
}
}
#[tokio::test]
async fn c6_abort_yields_the_handle_with_its_value() {
let flow = flow_from_source(
r#"
effect SSE { Emit(t: Text) -> Unit }
flow F() -> Text {
handle SSE {
Emit(token) -> { abort("aborted value") }
} in {
perform Emit("x")
let unreachable = "must not run"
}
}
"#,
"F",
);
let (mut c, _rx) = ctx();
let outcome = dispatch_node(&flow.steps[0], &mut c).await.expect("handle");
match outcome {
NodeOutcome::Completed { output, .. } => {
assert_eq!(output, "aborted value", "the handle yields the aborted value")
}
other => panic!("the handle must COMPLETE with the abort's value, got {other:?}"),
}
assert!(!c.let_bindings.contains_key("unreachable"));
}
#[tokio::test]
async fn c6b_an_abort_terminates_the_frame_that_owns_it_not_the_innermost() {
let flow = flow_from_source(
r#"
effect Outer { Bail(v: Text) -> Unit }
effect Inner { Tick() -> Unit }
flow F() -> Text {
handle Outer {
Bail(v) -> { abort("outer aborted") }
} in {
handle Inner {
Tick() -> { resume() }
} in {
perform Bail("go")
let inner_tail = "must not run"
}
let outer_tail = "must not run either"
}
}
"#,
"F",
);
let (mut c, _rx) = ctx();
let outcome = dispatch_node(&flow.steps[0], &mut c).await.expect("handle");
match outcome {
NodeOutcome::Completed { output, .. } => assert_eq!(
output, "outer aborted",
"the OUTER handle owns the abort and yields its value"
),
other => panic!("expected the outer handle to complete, got {other:?}"),
}
assert!(
!c.let_bindings.contains_key("inner_tail"),
"the inner handle's remaining body must not run"
);
assert!(
!c.let_bindings.contains_key("outer_tail"),
"and neither must the OUTER handle's — the abort terminated it. If \
this binding exists, the inner frame swallowed the outer frame's exit"
);
}
#[tokio::test]
async fn c7_forward_delegates_to_the_outer_handler() {
let (c, _) = run(
r#"
effect SSE { Emit(t: Text) -> Unit }
flow F() -> Text {
handle SSE {
Emit(token) -> {
let outer_saw = token
resume()
}
} in {
handle SSE {
Emit(token) -> {
let inner_saw = token
forward Emit(token)
}
} in {
perform Emit("payload")
let after = "resumed through the outer handler"
}
}
}
"#,
"F",
)
.await;
assert_eq!(
c.let_bindings.get("inner_saw").map(String::as_str),
Some("payload"),
"the INNER clause intercepts first"
);
assert_eq!(
c.let_bindings.get("outer_saw").map(String::as_str),
Some("payload"),
"…and `forward` delegates to the OUTER one, with the arguments intact"
);
assert_eq!(
c.let_bindings.get("after").map(String::as_str),
Some("resumed through the outer handler"),
"the outer clause's `resume` must return control to the ORIGINAL \
perform site — a forward is a delegation, not a new perform"
);
}
#[tokio::test]
async fn c8_an_unhandled_perform_fails_closed() {
let err = run_expecting_error(
r#"
effect SSE { Emit(t: Text) -> Unit }
flow F() -> Text {
perform Emit("x")
}
"#,
"F",
)
.await;
match err {
DispatchError::BackendError { name, message } => {
assert_eq!(name, "algebraic_effects");
assert!(
message.contains("unhandled effect") && message.contains("SSE"),
"the diagnostic must name what had no handler: {message}"
);
}
other => panic!("expected a named refusal, got {other:?}"),
}
}
#[tokio::test]
async fn c8b_a_frame_for_another_effect_does_not_catch_it() {
let err = run_expecting_error(
r#"
effect A { Op(v: Text) -> Unit }
effect B { Other(v: Text) -> Unit }
flow F() -> Text {
handle A { Op(v) -> { resume() } } in {
perform B.Other("x")
}
}
"#,
"F",
)
.await;
match err {
DispatchError::BackendError { message, .. } => assert!(
message.contains("unhandled effect") && message.contains('B'),
"a frame over A must not intercept B: {message}"
),
other => panic!("expected a refusal, got {other:?}"),
}
}
#[tokio::test]
async fn c9_a_clause_parameter_does_not_leak_into_the_continuation() {
let (c, _) = run(
r#"
effect SSE { Emit(t: Text) -> Unit }
flow F() -> Text {
let token = "the outer binding"
handle SSE {
Emit(token) -> { resume() }
} in {
perform Emit("the performed value")
}
}
"#,
"F",
)
.await;
assert_eq!(
c.let_bindings.get("token").map(String::as_str),
Some("the outer binding"),
"the clause SHADOWS `token` for its own body and RESTORES it after. If \
this reads `the performed value`, the handler leaked its parameter \
into the surrounding scope"
);
}
#[tokio::test]
async fn c9b_an_unshadowed_clause_parameter_is_removed_after() {
let (c, _) = run(
r#"
effect SSE { Emit(t: Text) -> Unit }
flow F() -> Text {
handle SSE { Emit(token) -> { resume() } } in { perform Emit("v") }
}
"#,
"F",
)
.await;
assert!(
!c.let_bindings.contains_key("token"),
"a clause binder that shadowed nothing must be REMOVED on exit, not \
left behind for the next step to interpolate"
);
}
#[tokio::test]
async fn c10_a_step_body_perform_runs_after_generation_over_the_steps_output() {
let flow = flow_from_source(
r#"
effect SSE { Emit(t: Text) -> Unit }
flow F() -> Text {
handle SSE {
Emit(token) -> {
let handler_saw = token
resume()
}
} in {
step Gen {
given: seed
perform Emit(Gen.output)
output: Text
}
}
}
"#,
"F",
);
let (mut c, _rx) = ctx();
dispatch_node(&flow.steps[0], &mut c)
.await
.expect("the handle must dispatch");
let generated = c
.let_bindings
.get("Gen")
.cloned()
.expect("the step must bind its output under its own name");
let handler_saw = c
.let_bindings
.get("handler_saw")
.cloned()
.expect("the handler must have run");
assert_ne!(
handler_saw, "Gen.output",
"the handler received the NAME. That is what a `perform` filed under \
`pix_ops` would produce: the elevation runs BEFORE generation, \
`Gen.output` resolves to nothing, and an unresolved reference \
resolves to itself — so the wire carries an identifier where the \
adopter expected the step's output, with no error anywhere"
);
assert_eq!(
handler_saw, generated,
"the handler must see exactly what the step GENERATED — which is only \
possible if the perform ran after generation, against the live bindings"
);
}
#[tokio::test]
async fn c10b_a_step_body_perform_fires_exactly_once() {
let flow = flow_from_source(
r#"
effect SSE { Emit(t: Text) -> Unit }
flow F() -> Text {
handle SSE {
Emit(token) -> { resume() }
} in {
step Gen {
given: seed
perform Emit(Gen.output)
output: Text
}
}
}
"#,
"F",
);
let (mut c, mut rx) = ctx();
dispatch_node(&flow.steps[0], &mut c).await.expect("handle");
let mut performs = 0usize;
while let Ok(ev) = rx.try_recv() {
if let FlowExecutionEvent::StepStart { step_type, .. } = ev {
if step_type == "perform" {
performs += 1;
}
}
}
assert_eq!(
performs, 1,
"one `perform` in the source must produce exactly one dispatch. Two \
means the statement runs as an elevation AND as a postscript — the \
handler would receive the unresolved name first and the real value \
second, and the final binding would look correct"
);
}
#[tokio::test]
async fn c12_a_clause_performing_its_own_operation_does_not_self_dispatch() {
let err = run_expecting_error(
r#"
effect SSE { Emit(t: Text) -> Unit }
flow F() -> Text {
handle SSE {
Emit(token) -> { perform Emit(token) }
} in {
perform Emit("x")
}
}
"#,
"F",
)
.await;
match err {
DispatchError::BackendError { message, .. } => assert!(
message.contains("unhandled effect"),
"a clause must not see its own frame — it escapes OUTWARD, and with \
no outer handler it fails closed. Seeing itself would be an \
infinite self-dispatch: {message}"
),
other => panic!("expected a refusal, got {other:?}"),
}
}
#[tokio::test]
async fn c11_an_effect_free_flow_carries_no_frames() {
let (c, slugs) = run(
"flow F() -> Text { let x = \"plain\" }\n",
"F",
)
.await;
assert_eq!(c.let_bindings.get("x").map(String::as_str), Some("plain"));
assert!(
c.effect_frames.is_empty(),
"no effects declared ⇒ no frames ever pushed"
);
assert!(
!slugs.iter().any(|s| s == "handle" || s == "perform"),
"and no effect events on the wire: {slugs:?}"
);
}
#[tokio::test]
async fn c11b_the_frame_stack_is_balanced_so_a_closed_scope_catches_nothing() {
let err = run_expecting_error(
r#"
effect SSE { Emit(t: Text) -> Unit }
flow F() -> Text {
handle SSE { Emit(t) -> { resume() } } in {
perform Emit("inside")
}
perform Emit("outside")
}
"#,
"F",
)
.await;
match err {
DispatchError::BackendError { message, .. } => assert!(
message.contains("unhandled effect"),
"a `perform` AFTER the handle's scope closed must not find the \
frame — that is what delimited means: {message}"
),
other => panic!("expected a refusal, got {other:?}"),
}
}