Interrupt Routine & Return
AmritaSense v0.4.x+ introduces a new capability for interrupt-style control transfer within a workflow: save the full interpreter state, jump to a handler routine, then restore and return. This is analogous to how a CPU saves context before vectoring to an interrupt service routine (ISR) and restores it on return.
Comparison with PUSH_STACK / RET_FAR
PUSH_STACK/RET_FARmanage only the return address stack — like a CPU saving just the program counter.PUSH_CONTEXT/POP_CONTEXTsave the complete interpreter state — like a full CPU context switch including all registers. See Manual Stack Management for the return-address-only approach.
Core Concepts
The Context Stack
Each WorkflowInterpreter now maintains a context stack (pc.context_stack), a LIFO stack of InterpreterContext snapshots. Each snapshot captures:
| Field | Description |
|---|---|
ptr | Current PointerVector (program counter) |
exception_ignored | Exception types that bypass TRY/CATCH |
s_args / s_kwargs | Dependency injection parameters (optional) |
stack | Return-address stack (optional) |
exception | Panic exception if any |
The if_flag
pc.if_flag is a boolean that marks whether the interpreter is in an interrupt context. INTERRUPT_INTO sets it to the if_state argument (default False), and INTERRUPT_RET resets it to False on return. When if_state=True is used, a subsequent INTERRUPT_INTO raises IllegalState — this guard prevents re-entry from inside an IF branch that was entered with the flag set. With the default if_state=False, nested interrupts are allowed (see Pattern 4).
Pattern 1: PUSH_CONTEXT + INTERRUPT_RET (Simplest Context Save)
The simplest pattern — save full state, jump to a sub-routine, restore and return. Since v0.6.0, PUSH_CONTEXT no longer jumps, so the jump into the sub-routine must be explicit (GOTO). No trailing GOTO("done") / ALIAS(NOP, "done") is needed — the archived block skips itself and the interpreter finishes at the end of the workflow.
from amrita_sense import ALIAS, NOP, Node, WorkflowInterpreter
from amrita_sense.instructions import GOTO, INTERRUPT_RET, PUSH_CONTEXT
@Node()
async def start() -> None: ...
@Node()
async def sub_routine() -> None: ...
@Node()
async def after_restore() -> None: ...
comp = (
start
>> PUSH_CONTEXT("resume") # save state; return address = resume NOP
>> GOTO("sub_entry") # explicit jump to sub (v0.6.0+)
>> ALIAS(NOP, "resume") # INTERRUPT_RET rebases here -> advance onto after_restore
>> after_restore # resumed here after INTERRUPT_RET
>> ALIAS(sub_routine, "sub_entry")
>> INTERRUPT_RET() # pop & restore
)
await WorkflowInterpreter(comp.render()).run()
PUSH_CONTEXTis the low-level primitive. For most use cases, preferINTER_FN+INTERRUPT_INTO(Pattern 2) — it handles the return address automatically.
Pattern 2: INTER_FN + INTERRUPT_INTO (Recommended)
The modern way: define the handler with INTER_FN(entrypoint, block) — it auto-appends INTERRUPT_RET() and embeds its own skip mechanism. Dispatch with INTERRUPT_INTO(entrypoint, None) — None means "return to the node right after the dispatch" (no manual ret_to / restore_here NOP needed).
from amrita_sense import Node, WorkflowInterpreter
from amrita_sense.instructions import INTER_FN, INTERRUPT_INTO
@Node()
async def main_logic() -> None: ...
@Node()
async def error_handler() -> None:
print("Handling error")
handler_block = INTER_FN("on_error", error_handler)
comp = (
main_logic
>> INTERRUPT_INTO("on_error", None) # jump to handler; return after this node
>> after_handler # resumed here after INTERRUPT_RET
>> handler_block # skipped by normal flow (_fn_escape)
)
await WorkflowInterpreter(comp.render()).run()What happens:
INTERRUPT_INTO("on_error", None)saves interpreter state (return address = the instruction itself), setsif_flag, jumps to the handler entry.error_handlerruns, then the auto-appendedINTERRUPT_RET()pops and restores the state —rebase_contextputs the pointer at the dispatch instruction, and the interpreter advances onto the next node (after_handler).after_handlerexecutes; thehandler_blockis skipped by_fn_escapeduring normal flow.
Pattern 3: Interrupt Handler Library with INTER_FN
Build a library of named interrupt handlers that normal execution skips — just concatenate INTER_FN blocks with >> (each has its own _fn_escape skip):
from amrita_sense import Node, WorkflowInterpreter
from amrita_sense.instructions import INTER_FN, INTERRUPT_INTO
@Node()
async def main_flow() -> None: ...
@Node()
async def handle_timeout() -> None:
print("[timeout] Cleaning up...")
@Node()
async def handle_auth_failure() -> None:
print("[auth] Refreshing credentials...")
handler_library = INTER_FN("timeout", handle_timeout) >> INTER_FN("auth", handle_auth_failure)
comp = (
main_flow
>> INTERRUPT_INTO("timeout", None)
>> INTERRUPT_INTO("auth", None)
>> handler_library
)
await WorkflowInterpreter(comp.render()).run()Pattern 4: Nested Interrupts
The context stack supports nested save/restore — like a CPU handling nested interrupts. With the default if_state=False, an INTERRUPT_INTO inside a handler is allowed; the inner INTER_FN restores back into the outer handler, which then completes and restores back to the main flow:
from amrita_sense import Node, WorkflowInterpreter
from amrita_sense.instructions import INTER_FN, INTERRUPT_INTO
@Node()
async def outer_func() -> None:
print(" [outer] Starting...")
@Node()
async def inner_func() -> None:
print(" [inner] Deep handler")
outer = INTER_FN(
"outer_handler",
outer_func >> INTERRUPT_INTO("inner_handler", None), # nested dispatch
)
inner = INTER_FN("inner_handler", inner_func)
comp = (
main_start
>> INTERRUPT_INTO("outer_handler", None)
>> after_all
>> outer
>> inner
)
await WorkflowInterpreter(comp.render()).run()Execution: main → outer handler → nested INTERRUPT_INTO → inner handler → inner INTERRUPT_RET (auto) → back into outer handler → outer INTERRUPT_RET (auto) → after_all.
Relationship with External Interrupts
| Mechanism | Source | How it works |
|---|---|---|
call_sub(interrupt=True) | External | Outside code injects a subroutine at node boundaries |
INTERRUPT_INTO / INTERRUPT_RET | Internal | Instructions in the >> chain perform context save/jump/restore |
See External Interrupt Calls for the external mechanism.
Caveats
- INTERRUPT_INTO with
if_state=Trueinside IF branches:pc.if_flag == TrueraisesIllegalState. With the defaultif_state=False, nesting is allowed (Pattern 4). - ret_to is optional (v0.6.0+):
INTERRUPT_INTO(jump_to)works withoutret_to—Noneresolves to the top of_ret_addr_stackinside acall_sub, otherwise to the current pointer (advancing onto the next node after restore). PreferNoneover a manualrestore_hereNOP. - if_flag cleared on return: After
INTERRUPT_RET,pc.if_flagis always reset toFalse. - INTERRUPT_RET does not set the jump flag: it restores via
rebase_context(i.e.rebase_ptr) — execution resumes at the node after the saved address. Save the predecessor of your real resume point when usingPUSH_CONTEXT/ explicitret_to. - Dependency injection preserved:
INTERRUPT_INTOalways includess_argsands_kwargs. - Context stack integrity: Ensure each
PUSH_CONTEXT/INTERRUPT_INTOhas a correspondingINTERRUPT_RET.
