Native Instructions (NATIVE_IF / NATIVE_WHILE / NATIVE_DO / BREAK_LOOP / CONTINUE)
The native control flow instruction set introduced in AmritaSense v0.5.1 is based on the PUSH / JMP / CONTINUE / BREAK_LOOP pointer operation pattern — an orthogonal extension to the traditional call_sub instructions. Since v0.6.0, loop bodies are always wrapped as NodeCompose(body, CONTINUE()) and RET_FAR is no longer involved in native loops.
Overview
Traditional instructions (IF / WHILE / DO) use call_sub to enter branch bodies — an extra nested call layer. Native instructions replace this with lightweight pointer jumps:
call_sub path: call_sub → execute → auto-return
native loop path: push → jump → execute → CONTINUE → pop → jump loop headLoop bodies always end with CONTINUE() (auto-appended by the compiler) which pops the stack and jumps back to the loop head for the next iteration.
DI & middleware are interpreter-level — both paths execute every node through
_call(), so dependency injection and the middleware hook apply identically. Native instructions only save thecall_subnesting layer; they do not bypass DI or middleware.
CONTINUE vs BREAK_LOOP
| Instruction | Purpose | Auto-inserted? |
|---|---|---|
CONTINUE() | End current iteration, jump to loop head | ✅ compiler fallback |
BREAK_LOOP() | Terminate the loop (pop stack + jump to sentinel) | ❌ manual only |
The key difference: CONTINUE() pops the stack and jumps back to the loop head (the loop continues with the next iteration), while BREAK_LOOP() pops the stack and jumps directly to the loop's sentinel NOP, cleanly ending the loop. Both target positions are configured at compile time by the enclosing loop's extract() via the DFS scanner _configure_loop_control_nodes() — never by hand.
RET_FARis not part of native loops anymore (since v0.6.0). It usesrebase_ptr+ naturaladvance_pointerand is meant for manual stack-return patterns (PUSH_AND_GOTO/PUSH_STACK).CONTINUE()/BREAK_LOOP()are directjump_far_ptroperations that set the jump flag.
Compile-Time Body Classification
_classify_body distinguishes payload types:
| payload type | NATIVE_IF path | NATIVE_WHILE / NATIVE_DO path |
|---|---|---|
BaseNode | call_offset (auto-return) | Auto-wrapped NodeCompose(body, CONTINUE()) |
NodeCompose / SelfCompileInstruction | Wrap in bubble (natural flow-back, no return instruction) | Wrap as NodeCompose(*body._graph, CONTINUE()) |
NATIVE_IF
Import
from amrita_sense.instructions.native import NATIVE_IFSignature
NATIVE_IF(
condition: Node[bool],
body: BaseNode | NodeCompose | SelfCompileInstruction,
) -> NativeIfClauseMethods
| Method | Signature | Description |
|---|---|---|
.ELIF(condition, body) | → Self | Append ELIF branch; chainable |
.ELSE(body) | → Self | Append ELSE branch; at most once |
Compiled Layout
Single IF (bubble body)
IF-ELIF-ELSE Chain
Every branch (IF, ELIF, ELSE) is a plain nested container with no return instruction — each flows naturally to the merge point via advance_pointer, matching Python's if/elif/else semantics (since v0.6.0).
Underlying Nodes
NativeIfJumpNode(_core.py): Condition jump node for IF/ELIF. The_is_singleflag determines the single-node (call_offset) vs bubble (jump_far_ptronly — no PUSH / no RET_FAR) path.
NATIVE_WHILE
Import
from amrita_sense.instructions.native import NATIVE_WHILESignature
NATIVE_WHILE(
condition: Node[bool],
) -> NativeWhileClauseMethods
| Method | Signature | Description |
|---|---|---|
.ACTION(body) | → Self | Set loop body; required, once only |
Compiled Layout
Underlying Nodes
NativeWhileNode(_core.py): Condition evaluation + dispatch node. Pure jump: condition true →PUSH [0](its own address) →jump_far_ptrinto body; condition false →jump_near(3)to exit. The body's trailingCONTINUE()pops and jumps back to[0].
NATIVE_DO
Import
from amrita_sense.instructions.native import NATIVE_DOSignature
NATIVE_DO(
body: BaseNode | NodeCompose | SelfCompileInstruction,
) -> NativeDoClauseMethods
| Method | Signature | Description |
|---|---|---|
.WHILE(condition) | → Self | Set loop condition; required, once only |
Compiled Layout
Single-node bodies are auto-wrapped the same way (NodeCompose(body, CONTINUE())).
Underlying Nodes
NativeDoWhileNode(_core.py): DO-WHILE back-edge node. When condition is true,jump_near(loop_pos)back to body entry (NativeBubbleEnterNodehandles re-entry); when false,jump_near(exit_pos)to exit.NativeBubbleEnterNode(_core.py): Bubble entry helper. AlwaysPUSHes a sentinel (its own address) thenjump_far_ptrs into the body, soCONTINUE()/BREAK_LOOP()can pop it. Constructor takes onlybody_pos(theret_posparameter was removed in v0.6.0).
BREAK_LOOP
Import
from amrita_sense.instructions.native import BREAK_LOOPSignature
BREAK_LOOP() -> _BreakLoopNode # factory function (v0.6.0+)Since v0.6.0, BREAK_LOOP is a factory function — call it: BREAK_LOOP(). The old module-level singleton was removed.
How It Works
pc._ret_addr_stack.pop()clean up the return address pushed on loop entry- Derive the enclosing bubble's parent address from the popped
PointerVector pc.jump_far_ptr([*parent, break_pos])jump to the sentinel NOP —break_poswas configured at compile time by the enclosing loop'sextract()(DFS scanner)
Constraints
- Must be inside a native loop body: a node not configured by any enclosing loop raises
RuntimeErrorat runtime - One level per call: pops one stack entry and exits the innermost enclosing native loop
- Nesting: each loop level configures only its own body's control nodes (the DFS scanner stops at inner native-loop boundaries)
- Normal execution continues after break: the next node after the loop executes as usual
Example
from amrita_sense.instructions.native import BREAK_LOOP, NATIVE_WHILE
NATIVE_WHILE(cond).ACTION(
process_item
>> BREAK_LOOP()
>> log_item
)CONTINUE
Import
from amrita_sense.instructions.native import CONTINUESignature
CONTINUE() -> _ContinueNode # factory functionHow It Works
Identical mechanics to BREAK_LOOP(), but the target is the loop head instead of the sentinel: for NATIVE_WHILE it jumps to [0] (re-evaluate condition); for NATIVE_DO it jumps to [2] (the do-while node, re-check condition).
Constraints
- Must be inside a native loop body: same runtime
RuntimeErrorif unconfigured - Auto-appended: every loop body already ends with
CONTINUE()— you only write it manually to skip the rest of the current iteration - Not an exception: it is a synchronous pointer operation (
wrap_to_async=False) and does not trigger exception handling
Example
from amrita_sense.instructions.native import CONTINUE, NATIVE_DO
NATIVE_DO(
step_a
>> CONTINUE() # skip step_b, re-check condition
>> step_b
).WHILE(cond)Comparison with Traditional Instructions
IF | NATIVE_IF | WHILE | NATIVE_WHILE | DO | NATIVE_DO | |
|---|---|---|---|---|---|---|
| Entry | call_sub | jump_far_ptr or call_offset | call_sub | PUSH+JMP | call_sub | PUSH+JMP |
| Return | auto (call_sub) | natural flow-back (bubble) / auto (single) | auto (call_sub) | CONTINUE() (auto) | auto (call_sub) | CONTINUE() (auto) |
| Break | raise BreakLoop | BREAK_LOOP() | raise BreakLoop | BREAK_LOOP() | raise BreakLoop | BREAK_LOOP() |
| Continue | — | — | — | CONTINUE() | — | CONTINUE() |
| Middleware | invoked | invoked (interpreter-level) | invoked | invoked (interpreter-level) | invoked | invoked (interpreter-level) |
| DI resolution | invoked | invoked (interpreter-level) | invoked | invoked (interpreter-level) | invoked | invoked (interpreter-level) |
Notes
- Loop bodies always end with CONTINUE(): the compiler auto-appends
CONTINUE(). If you manually construct aNodeComposeas a loop body, placeCONTINUE()at the end (or rely on auto-wrapping). - BREAK_LOOP / CONTINUE are not exceptions: they are synchronous pointer operations (
wrap_to_async=False) and do not trigger exception handling - Native instructions are composable: fully interoperable with
>>,NodeCompose, and traditional instructions - Branches flow back naturally: since v0.6.0,
NATIVE_IF/ELIF/ELSEbubbles carry no return instruction — they are plain nested containers that flow back to the merge point viaadvance_pointer, exactly like Pythonif/elif/elseblocks. There is no early-return mechanism (no manualRET_FARinside a branch body).
