Skip to content

Function Block Call ​

AmritaSense provides FN / INTER_FN — helpers that look like function definitions in high-level languages. Before using them, one critical fact must be clear:

A function block is a control-flow transfer, not a real function call. It is a jump into a node region followed by a jump back. There is no function context: no stack frame, no local variables, no closure capture, no automatic argument passing, and no return-value convention. The word "function" is only a convenient metaphor.

What a Function Block Is (and Is Not) ​

AspectHigh-level language functionAmritaSense FN / INTER_FN
EntryCaller evaluates arguments, pushes a frameCALL / INT jump to an alias
Local variablesFresh frame with locals❌ none — the interpreter has a single shared state
ArgumentsPassed by value/reference❌ none — data flows via dependency injection from the interpreter's arg pool
Return valuereturn expr❌ none — RET / IRET only restore the pointer/context
StackDedicated call stack per functionShared _ret_addr_stack (a single jump target)
RecursionSupported❌ meaningless — there is no frame to re-enter
Closures / scopeLexical scoping, captures❌ none — the whole workflow shares one pointer space

Think of a function block as a named, archived jump target: entering it changes the pointer; ending it changes the pointer back. Everything else you might expect from a function simply does not exist.

FN(entrypoint, block) — Regular Function Block ​

python
from amrita_sense.instructions import FN

fn_block = FN(
    "fn_entry",  # required entrypoint alias
    fn_body,  # NodeCompose or SelfCompileInstruction
)

FN expands at compile time to:

text
[_fn_escape, ALIAS(NOP, "fn_entry"), <block>, RET()]
  • _fn_escape — a hidden node (rebase_ptr(pointer.copy().offset(3))). When the normal sequential flow reaches the block, it rebases the pointer past the block (offset(3) lands exactly on the trailing RET), so the whole block is skipped.
  • ALIAS(NOP, "fn_entry") — the named entry point. Jumps target this alias.
  • <block> — your body (a nested container; entered via the pointer).
  • RET() — appended automatically. Pops _ret_addr_stack, rebase_ptrs to the caller's saved address, and the interpreter advances onto the node after the call site.

Calling an FN Block ​

python
from amrita_sense.instructions import CALL

comp = (
    start
    >> CALL("fn_entry")  # from_adr=None -> return after this node
    >> after_fn  # resumed here after RET
    >> fn_block  # skipped by normal flow via _fn_escape
)

CALL(entrypoint) pushes its own address (from_adr defaults to None, which always resolves to the current pointer — the CALL node itself) and jumps to the entrypoint. RET restores the pointer there, and the interpreter advances onto the next node — after_fn.

INTER_FN(entrypoint, block) — Interrupt Service Routine ​

python
from amrita_sense.instructions import INTER_FN

isr = INTER_FN(
    "isr_entry",
    isr_body,
)

Identical shape, but the trailing instruction is IRET() instead of RET:

text
[_fn_escape, ALIAS(NOP, "isr_entry"), <block>, IRET()]

IRET pops a saved InterpreterContext and restores the whole interpreter state (pointer, exception-ignore list, dependency args, return-address stack, panic exception, status register) — not just the pointer.

Calling an INTER_FN Block ​

python
from amrita_sense.instructions import INT

comp = (
    main_start
    >> INT("isr_entry")  # dispatch; ret_to=None -> return after this node
    >> after_isr  # resumed here after IRET
    >> isr  # skipped by normal flow via _fn_escape
)

INT(entrypoint) snapshots the interpreter context (with the current pointer as the return address) and jumps to the handler. When the routine ends, IRET restores the snapshot; since the restore uses rebase_context (no jump flag), execution advances onto the next node — after_isr.

Relationship with ARCHIVED_SEGMENT ​

FN / INTER_FN already embed their own skip mechanism (_fn_escape), so the block can be placed directly in the >> chain without extra wrapping:

python
comp = (
    start
    >> CALL("fn_entry")
    >> after_fn
    >> FN("fn_entry", fn_body)  # fine — no ARCHIVED_SEGMENT needed
)

ARCHIVED_SEGMENT is the lower-level building block ([JMP 2, Payload, NOP]) used when you want to archive an arbitrary compose without the function-call semantics — e.g. a plain section reachable only via JMP that performs its own return (RET / IRET) manually.

Data Exchange without Function Context ​

Because there is no argument passing, share data through the workflow's dependency injection:

python
@Node()
async def fn_body(
    ctx: WorkflowContext,
) -> None: ...  # DI resolves `ctx` from the interpreter's arg pool at the call site

The interpreter resolves the body's parameters from the same _ava_args / _ava_kwargs pool — the block executes in the caller's context, not a fresh one.

Summary ​

  • FN / INTER_FN are named, archived jump targets with an auto-appended return instruction.
  • They change the control flow, not the execution context.
  • Enter via CALL(entrypoint) / INT(entrypoint); exit via the auto-appended RET() / IRET().
  • No locals, no arguments, no return values, no recursion — design accordingly (use DI for data, use the pointer/context restore for "return").

Apache 2.0 License