perform
Since v2.87.0 · Used inside a declaration
Grammar
perform <Op>(<arg>, ...) # bare — resolves against the closed catalog
perform <Effect>.<Op>(<arg>, ...) # qualified — required when two effects share an Op
Raises one operation from a declared effect. Control yields to the
nearest enclosing handle scope, the matching clause runs, and — if
that clause calls resume(v) — control returns to this exact point with v as
the result.
flow study(topic: Text) -> Text {
step Analyze {
given: topic
ask: "Analyse the topic and produce one paragraph of findings."
perform Emit(Analyze.output)
perform Done()
output: Text
}
}
study names no transport, no handler, and no effect row of its own. It
performs Emit and Done, and something else decides what they mean.
Surface
Legal in two positions:
- at flow level, as a statement beside
stepandrun - inside a step body, as shown above
The bare form perform Emit(x) resolves against the closed catalog of declared
effects. When two effects declare the same operation the bare form is refused
naming both (axon-T964), and the qualified form perform Delivery.Emit(x)
says which one you meant.
Fields
The argument list must match the operation's declared parameters. The value the
perform evaluates to is the operation's declared return type — supplied by
the handler's resume(v), not by this site.
An operation declared -> Never never resumes: nothing after that perform
runs, so it is the last statement its path executes.
Runtime behaviour
Inside a step body, a perform runs AFTER the step generates. This is not
an implementation detail — it is the reason perform is not filed with the
step's other statements, which all run before generation. It is what makes
perform Emit(Analyze.output)
mean the paragraph the model actually produced, rather than an unresolved
identifier. A perform that ran first could only ever emit the name.
An undischarged perform is a compile error (axon-T966), not a runtime
surprise. If no handler for the effect is in scope at the entry point, the
program does not build. The check is interprocedural by necessity: the flow
that performs and the flow that handles are normally different flows, and
deleting a handle two call levels up is exactly the mistake this catches.
There is no runtime fallback. An effect with nowhere to yield to has no defined behaviour, so the language refuses to invent one.
What this primitive is NOT
Not a function call. The operation has no body here. What happens is chosen
by whichever handler is in scope, and the same perform can mean different
things under different handlers without the performing flow changing.
Not throw. An exception unwinds the stack and cannot come back. A
perform yields, and resume(v) returns to this exact site with the
computation intact.
Not output:. output: produces the step's value for its own flow;
perform hands a value out to a handler that may live in another flow
entirely.