JMP & INVOKE Jump Instructions
JMP and INVOKE are two core control flow jump instructions in AmritaSense. They share the same alias-based addressing infrastructure, but serve very different scenarios: JMP is an unconditional one-way jump, while INVOKE is a subroutine invocation with a return.
Common foundation Both rely on the same alias registry (
ALIAS), perform address resolution during compile-time_post_compile, and use the@markupdecorator to manage jump markers. Understanding these common mechanisms helps clarify the difference between them.
Non-self-compiled direct nodes
JMP and INVOKE are not SelfCompileInstruction. They do not expand into NodeCompose at compile time; instead, they exist as single jump nodes in the workflow array.
That means:
- they are compiled as a single
JumpNodeorInvokeNodeelement - runtime behavior is expressed entirely through address resolution and pointer rewriting
- they do not create new Bubbles or nesting scopes
JMP
JMP is a factory wrapper for JumpNode. At runtime, it calls the interpreter’s jump_to method and performs a one-way, non-returning pointer rewrite.
Execution flow
- Address resolution (
_post_compile): resolve alias names through the alias table at compile time, or validate raw addresses. - Jump marker: call
pc.jump_to(addr), which is guarded by@markupand sets theJMPbit. - Pointer replacement: replace
_pointercompletely with the target address vector. - Interpreter response: the main loop sees the
JMPbit and skips normaladvance_pointer(), continuing from the jump target.
Key characteristics
- Does not manage the call stack:
JMPdoes not push anything onto_ret_addr_stack. Once jumped, there is no return. - Can jump across any nesting level: because
far_to(addr)replaces the entire pointer vector,JMPcan cross Bubble boundaries. - Supports both alias and raw address:
JMP("target")uses a symbol, whileJMP([1, 2, 3])uses an absolute address.
Typical use cases
- Error handling jumps: jump directly to a centralized error handler when a failure is detected.
- Branch merging: multiple conditional branches converge on the same
NOPpoint. - State machine transitions: jump to different next states based on runtime conditions.
INVOKE
INVOKE is a factory wrapper for InvokeNode. At runtime, it calls the interpreter’s call_sub method and performs a push -> jump -> execute -> pop subroutine call.
Execution flow
- Address resolution (
_post_compile): resolve alias names and cache the absolute address at compile time. - Push return address:
call_subsaves the current pointer vector on_ret_addr_stack. - Pointer replacement: set the execution pointer to the subroutine’s entry address.
- Execute subroutine: the interpreter advances through the subroutine nodes.
- Pop return address: when the subroutine completes, the
finallyblock pops the saved pointer and restores it. - Continue execution: the interpreter continues from the node after the
INVOKEinstruction.
Key characteristics
- Manages call stack: pushes the return address and pops it later, supporting nested calls.
- Subroutine source: the target can be any addressable node sequence, not necessarily stored in
ARCHIVED_NODES. - No direct parameter passing:
INVOKEitself does not pass arguments. If the subroutine needs parameters, useDependsinside the subroutine node or callcall_subdirectly within a node.
Typical use cases
- Code reuse: encapsulate reusable node sequences as subroutines and call them multiple times.
- Modular decomposition: split complex workflows into independent subprocedures and keep the main flow simple.
- Interrupt handling: external systems can invoke predefined interrupt handlers via
call_sub(interrupt=True).
JMP vs INVOKE: comparison
| Feature | JMP | INVOKE |
|---|---|---|
| Saves return addr | No | Yes (_ret_addr_stack) |
| After execution | continues from target onward | returns to caller afterward |
| Call stack effect | none | push once, pop once |
| Use cases | one-way jump, branch merge, error handling | subroutine reuse, modular flow, interrupts |
| Underlying API | pc.jump_to(addr) | pc.call_sub(addr) |
Usage notes
- JMP is not a substitute for loops:
JMPdoes not provide return semantics. Jumping out of a loop withJMPwill not correctly manage the loop state. UseBreakLoopfor loop exit andINVOKEfor reusable subroutines. - INVOKE targets must be addressable: the target node or entry node must have
address_able=True, which is required byALIAS. - JMP and INVOKE share alias space: both look up aliases in
alias2vector_map. Avoid alias name conflicts. - INVOKE return depends on stack integrity: a
JMPinside a subroutine sets theJMPbit, which can causecall_subto skip stack restoration. Understand thatJMPinside a subroutine can override normal return behavior.
Example
from amrita_sense.instructions import JMP, INVOKE, ALIAS, ARCHIVED_NODES
from amrita_sense.node import Node
@Node()
def error_handler():
print("Handling error")
@Node()
def reusable_step():
print("Executing reusable logic")
# JMP: jump to error cleanup
workflow = (
start
>> do_something
>> JMP("error_cleanup")
>> ALIAS(error_handler, "error_cleanup")
)
# INVOKE: reuse a subroutine
subprogram = ARCHIVED_NODES(
ALIAS(reusable_step, "reusable"),
)
main = init >> INVOKE("reusable") >> process >> INVOKE("reusable") >> end >> subprogramManual stack management:
PUSH_RET+JMP+RETgive you explicit control over the return address stack, and can be combined withARCHIVED_NODESfor subroutine-like patterns. See Advanced Topic: Manual Stack Space Management for full details.
