use std::sync::Arc;
use crate::flow_dispatcher::{DispatchCtx, DispatchError, NodeOutcome};
use crate::flow_execution_event::{now_ms, FlowExecutionEvent};
use crate::ir_nodes::{IREffectAbort, IREffectForward, IREffectHandle, IREffectPerform, IREffectResume};
pub type EffectFrame = Arc<IREffectHandle>;
pub async fn run_handle(
node: &IREffectHandle,
ctx: &mut DispatchCtx,
) -> Result<NodeOutcome, DispatchError> {
if ctx.cancel.is_cancelled() {
return Err(DispatchError::UpstreamCancelled);
}
let step_index = ctx.step_counter;
ctx.step_counter += 1;
let step_name = format!("handle:{}", node.effect_names.join(","));
emit_start(ctx, &step_name, step_index, "handle")?;
ctx.effect_frames.push(Arc::new(node.clone()));
let result = crate::flow_dispatcher::orchestration::dispatch_body_public(&node.body, ctx).await;
ctx.effect_frames.pop();
let outcome = match result? {
NodeOutcome::EffectAborted { frame_id, value } if frame_id == node.frame_id => {
NodeOutcome::Completed {
output: value,
tokens_emitted: 0,
step_index,
}
}
other => other,
};
if let NodeOutcome::Completed { output, .. } = &outcome {
emit_complete(ctx, &step_name, step_index, output, 0)?;
}
Ok(outcome)
}
pub async fn run_perform(
node: &IREffectPerform,
ctx: &mut DispatchCtx,
) -> Result<NodeOutcome, DispatchError> {
if ctx.cancel.is_cancelled() {
return Err(DispatchError::UpstreamCancelled);
}
let step_index = ctx.step_counter;
ctx.step_counter += 1;
if node.effect_name.is_empty() {
return Err(DispatchError::BackendError {
name: "algebraic_effects".to_string(),
message: format!(
"perform '{}' reached dispatch with no resolved effect. The compiler \
refuses this statically (axon-T964 / axon-T965); an IR that carries it \
anyway is not dispatched by operation name, because that would deliver \
the effect to a handler the author never named.",
node.operation_name
),
});
}
let step_name = format!("perform:{}.{}", node.effect_name, node.operation_name);
emit_start(ctx, &step_name, step_index, "perform")?;
let args: Vec<String> = node
.arguments
.iter()
.map(|a| resolve_argument(a, ctx))
.collect();
let outcome = dispatch_operation(
&node.effect_name,
&node.operation_name,
&args,
ctx.effect_frames.len(),
ctx,
)
.await?;
if let NodeOutcome::Completed { output, .. } = &outcome {
emit_complete(ctx, &step_name, step_index, output, 0)?;
}
Ok(outcome)
}
pub async fn run_forward(
node: &IREffectForward,
ctx: &mut DispatchCtx,
) -> Result<NodeOutcome, DispatchError> {
if ctx.cancel.is_cancelled() {
return Err(DispatchError::UpstreamCancelled);
}
if node.effect_name.is_empty() {
return Err(DispatchError::BackendError {
name: "algebraic_effects".to_string(),
message: format!(
"forward '{}' reached dispatch with no resolved effect (axon-T964 / \
axon-T965 refuse this statically).",
node.operation_name
),
});
}
Ok(NodeOutcome::EffectForwarded {
effect: node.effect_name.clone(),
operation: node.operation_name.clone(),
arguments: node
.arguments
.iter()
.map(|a| resolve_argument(a, ctx))
.collect(),
})
}
pub async fn run_resume(
node: &IREffectResume,
ctx: &mut DispatchCtx,
) -> Result<NodeOutcome, DispatchError> {
if ctx.cancel.is_cancelled() {
return Err(DispatchError::UpstreamCancelled);
}
Ok(NodeOutcome::EffectResumed {
value: resolve_argument(&node.value_expr, ctx),
})
}
pub async fn run_abort(
node: &IREffectAbort,
ctx: &mut DispatchCtx,
) -> Result<NodeOutcome, DispatchError> {
if ctx.cancel.is_cancelled() {
return Err(DispatchError::UpstreamCancelled);
}
Ok(NodeOutcome::EffectAborted {
frame_id: ctx.effect_clause_frame.unwrap_or(u32::MAX),
value: resolve_argument(&node.value_expr, ctx),
})
}
async fn dispatch_operation(
effect: &str,
operation: &str,
args: &[String],
search_from: usize,
ctx: &mut DispatchCtx,
) -> Result<NodeOutcome, DispatchError> {
let mut effect = effect.to_string();
let mut operation = operation.to_string();
let mut args: Vec<String> = args.to_vec();
let mut search_from = search_from;
loop {
let Some(frame_idx) = find_frame(ctx, &effect, search_from) else {
return Err(DispatchError::BackendError {
name: "algebraic_effects".to_string(),
message: format!(
"unhandled effect '{effect}.{operation}': no enclosing `handle {effect}` \
is in scope at this point. The compiler refuses this statically \
(axon-T966, the design plan D9 — there is no runtime fallback); reaching it \
here means the IR was not produced by `axon check`."
),
});
};
let frame = ctx.effect_frames[frame_idx].clone();
let Some(clause) = frame
.clauses
.iter()
.find(|c| c.operation_name == operation)
else {
return Err(DispatchError::BackendError {
name: "algebraic_effects".to_string(),
message: format!(
"handler for '{effect}' declares no clause for operation '{operation}' \
(it handles: {}). The compiler refuses this statically (axon-T962 / \
axon-T965).",
frame
.clauses
.iter()
.map(|c| c.operation_name.as_str())
.collect::<Vec<_>>()
.join(", ")
),
});
};
let saved: Vec<(String, Option<String>)> = clause
.parameter_names
.iter()
.map(|n| (n.clone(), ctx.let_bindings.get(n).cloned()))
.collect();
for (name, value) in clause.parameter_names.iter().zip(args.iter()) {
ctx.let_bindings.insert(name.clone(), value.clone());
}
let parked: Vec<EffectFrame> = ctx.effect_frames.split_off(frame_idx);
let prior_clause_frame = ctx.effect_clause_frame.replace(frame.frame_id);
let body_result =
crate::flow_dispatcher::orchestration::dispatch_body_public(&clause.body, ctx).await;
ctx.effect_clause_frame = prior_clause_frame;
ctx.effect_frames.extend(parked);
for (name, prior) in saved {
match prior {
Some(v) => {
ctx.let_bindings.insert(name, v);
}
None => {
ctx.let_bindings.remove(&name);
}
}
}
match body_result? {
NodeOutcome::EffectResumed { value } => {
return Ok(NodeOutcome::Completed {
output: value,
tokens_emitted: 0,
step_index: ctx.step_counter.saturating_sub(1),
});
}
NodeOutcome::EffectAborted { frame_id, value } => {
return Ok(NodeOutcome::EffectAborted { frame_id, value });
}
NodeOutcome::EffectForwarded {
effect: fwd_effect,
operation: fwd_operation,
arguments,
} => {
effect = fwd_effect;
operation = fwd_operation;
args = arguments;
search_from = frame_idx;
continue;
}
NodeOutcome::Completed { output, .. } => {
return Ok(NodeOutcome::EffectAborted {
frame_id: frame.frame_id,
value: output,
});
}
other => {
return Err(DispatchError::BackendError {
name: "algebraic_effects".to_string(),
message: format!(
"handler clause '{operation}' ended with {other:?}, which has no \
defined meaning inside a handler: a clause discharges via \
`resume`, `abort` or `forward`, or runs off its end (an implicit \
abort). Refused rather than reshaped into one of those."
),
});
}
}
}
}
fn find_frame(ctx: &DispatchCtx, effect: &str, start_exclusive: usize) -> Option<usize> {
let start = start_exclusive.min(ctx.effect_frames.len());
(0..start)
.rev()
.find(|&i| ctx.effect_frames[i].effect_names.iter().any(|e| e == effect))
}
fn resolve_argument(expr: &str, ctx: &DispatchCtx) -> String {
if expr.is_empty() {
return String::new();
}
match expr.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
Some(literal) => literal.to_string(),
None => crate::exec_context::resolve_value_reference(expr, &ctx.let_bindings),
}
}
fn emit_start(
ctx: &mut DispatchCtx,
step_name: &str,
step_index: usize,
step_type: &str,
) -> Result<(), DispatchError> {
ctx.tx
.send(FlowExecutionEvent::StepStart {
step_name: step_name.to_string(),
step_index,
step_type: step_type.to_string(),
branch_path: ctx.branch_path_string(),
timestamp_ms: now_ms(),
})
.map_err(|_| DispatchError::ChannelClosed)
}
fn emit_complete(
ctx: &mut DispatchCtx,
step_name: &str,
step_index: usize,
output: &str,
tokens_emitted: u64,
) -> Result<(), DispatchError> {
ctx.tx
.send(FlowExecutionEvent::StepComplete {
step_name: step_name.to_string(),
step_index,
success: true,
full_output: output.to_string(),
tokens_input: 0,
tokens_output: tokens_emitted,
branch_path: ctx.branch_path_string(),
timestamp_ms: now_ms(),
})
.map_err(|_| DispatchError::ChannelClosed)
}