Advanced Topic: Manual Stack Space Management
The CALL instruction and call_sub method automatically manage the return address stack (_ret_addr_stack) for you: they push the current pointer before entering a subroutine, and the finally block pops it upon return. For most workflows, this is all you need.
However, AmritaSense also exposes the return address stack for manual control via PUSH_STACK and RET_FAR. The pattern is:
- PUSH_STACK — Push an alias or address onto
_ret_addr_stack - GOTO — Jump somewhere else in the workflow
- RET_FAR — Pop the saved address and jump back
This lets you implement custom call/return schemes that don't follow the rigid CALL/call_sub discipline.
The Return Address Stack
_ret_addr_stack is a Stack[PointerVector] on the WorkflowInterpreter. CALL pushes the current pointer onto it; the finally block of call_sub pops and restores it. With PUSH_STACK, you can push any alias target onto the stack directly from the composition chain without writing a custom node.
PUSH_STACK and RET_FAR
PUSH_STACK(alias_or_idata)— pushes the resolved address of a target alias (or a raw address list) onto_ret_addr_stack. The instruction returns aNodeType[None](an inline@Node-decorated callable), placed directly in the>>chain.RET_FAR()— pops the top entry from_ret_addr_stackand callsjump_far_ptrto jump to the saved address. Likewise an inline@Node-decorated callable placed in the composition chain.
Neither instruction should be return-ed from inside a @Node() function — place them directly in the >> chain.
Example: PUSH_STACK + GOTO + RET_FAR
from amrita_sense import ALIAS, NOP, Node, WorkflowInterpreter
from amrita_sense.instructions import GOTO, PUSH_STACK, RET_FAR
@Node()
async def start() -> None:
print("Start")
@Node()
async def doing_work() -> None:
"""The section we GOTO into."""
print(" Doing work")
@Node()
async def after_return() -> None:
"""RET_FAR pops _ret_addr_stack and jumps here."""
print("Back here (via RET_FAR)")
comp = (
start
>> PUSH_STACK("after")
>> GOTO("work")
>> ALIAS(after_return, "after")
>> GOTO("end")
>> ALIAS(doing_work, "work")
>> RET_FAR()
>> ALIAS(NOP, "end")
)
await WorkflowInterpreter(comp.render()).run()Flow:
PUSH_STACK("after")pushes the address ofafter_returnonto_ret_addr_stackGOTO("work")jumps to thedoing_worknode- After
doing_work,RET_FARpops the saved address and jumps back toafter_return
PUSH_AND_GOTO (v0.3.0+)
PUSH_AND_GOTO(from_adr, to_adr) is a convenience instruction that combines PUSH_STACK + GOTO into a single node. Internally it:
- Pushes
from_adronto_ret_addr_stack(just likePUSH_STACK) - Jumps to
to_adr(just likeGOTO)
Both arguments accept either an alias string or a raw address list.
from amrita_sense.instructions import PUSH_AND_GOTO, RET_FAR
# These two patterns are equivalent:
# Pattern A: explicit two-step
comp_a = (
start
>> PUSH_STACK("after")
>> GOTO("work")
>> ALIAS(after_return, "after")
>> ALIAS(doing_work, "work")
>> RET_FAR()
)
# Pattern B: PUSH_AND_GOTO convenience
comp_b = (
start
>> PUSH_AND_GOTO("after", "work")
>> ALIAS(after_return, "after")
>> ALIAS(doing_work, "work")
>> RET_FAR()
)PUSH_AND_GOTO is semantically identical to the two-step pattern — use whichever reads more naturally in your composition.
When to Use Manual Stack Management
| Scenario | Use |
|---|---|
| Simple subroutine call/return | CALL + natural call_sub return |
| Custom return destination | PUSH_STACK + GOTO + RET_FAR |
| Push-and-jump convenience | PUSH_AND_GOTO + RET_FAR |
| Multi-level stack unwinding | Push multiple addresses, RET_FAR once per level |
| Non-linear control flow | Combine with GOTO for arbitrary jump patterns |
Subroutine-like Pattern with ARCHIVED_NODES
PUSH_STACK + GOTO + RET_FAR can be combined with ARCHIVED_NODES to create self-contained "subroutines" that are skipped during normal execution but can be entered via GOTO:
from amrita_sense import ALIAS, ARCHIVED_NODES, NOP, Node, WorkflowInterpreter
from amrita_sense.instructions import GOTO, PUSH_STACK, RET_FAR
@Node()
async def start() -> None:
print("Start")
@Node()
async def step1() -> None:
print(" Step 1")
@Node()
async def step2() -> None:
print(" Step 2")
@Node()
async def after_return() -> None:
print("Back here (via RET_FAR)")
# Self-contained subroutine: normal flow skips it, GOTO enters it.
# Execution inside: step1 >> step2 >> RET_FAR() → pop stack → return.
subroutine = ARCHIVED_NODES(
ALIAS(NOP, "sub_entry"), # entry point marker
step1,
step2,
RET_FAR(),
)
comp = (
start
>> PUSH_STACK("after")
>> GOTO("sub_entry")
>> ALIAS(after_return, "after")
>> subroutine
)
await WorkflowInterpreter(comp.render()).run()Flow:
PUSH_STACK("after")saves the return destinationGOTO("sub_entry")enters the subroutine atNOP(the entry marker)step1 >> step2execute sequentiallyRET_FAR()pops the saved address and jumps back toafter_return
The NOP aliased as "sub_entry" acts as the named entry point — GOTO targets the alias, and the node itself is a no-op.
Caution
- Stack integrity:
RET_FARpops from_ret_addr_stackunconditionally. If the stack is empty, this raises anIndexError. Always push a corresponding address (viaCALLorPUSH_STACK) before reachingRET_FAR. - Jump flag:
RET_FARcallsjump_far_ptrwhich is decorated with@markup, setting_jump_marked = True. The interpreter will NOT advance the pointer afterRET_FAR— execution resumes at the jumped-to address. - Not a subprogram instruction:
PUSH_STACKandRET_FARare standalone nodes in the composition chain. Do NOT call them from inside a@Node()function — place them directly in the>>chain.
