JMP 与 INVOKE 跳转指令
JMP 与 INVOKE 是 AmritaSense 中两种核心的控制流跳转指令。它们共享同一套基于 ALIAS 的地址解析基础设施,但服务于截然不同的场景:JMP 是无条件单向跳转,INVOKE 是带返回的子程序调用。
共同基础 两者都依赖
ALIAS系统注册的符号名来寻址,都在编译期的_post_compile阶段完成地址解析和拼写纠错,都使用@markup装饰器管理跳转标记。理解这些共同机制,有助于掌握两者的本质差异。
指令特性:非自编译的直接节点
JMP 和 INVOKE 不是 SelfCompileInstruction。它们不会在编译期展开为 NodeCompose 结构,而是直接作为单个跳转节点存在于工作流数组中。
这意味着:
- 它们在编译产物里就是一个
JumpNode或InvokeNode元素 - 执行时不会创建新的 Bubble 或嵌套作用域
- 所有行为完全体现在运行时的地址解析与指针改写上
JMP 指令
JMP 是对 JumpNode 的工厂封装,执行时调用解释器的 jump_to 方法,完成一次单向、不返回的指针改写。
执行流程
- 地址解析(
_post_compile阶段):在编译期将别名查表解析为list[int]绝对地址,或直接验证裸地址的有效性 - 跳转标记:调用
pc.jump_to(addr),该方法受@markup保护,会置起JMP位 - 指针替换:
_pointer被完整替换为目标地址向量 - 解释器响应:主循环检测到
JMP位,跳过常规的advance_pointer()步进,下一轮迭代从跳转目标开始执行
关键特性
- 不管理调用栈:JMP 不会向
_ret_addr_stack压入任何内容。跳了就跳了,没有“返回”的概念 - 可在任意层级跳转:因为
far_to(addr)直接替换整个指针向量,JMP 可以跨越 Bubble 边界 - 别名与裸地址双模:
JMP("target")使用符号名,JMP([1, 2, 3])使用绝对地址
典型使用场景
- 错误处理跳转:在检测到错误时,直接跳到统一的错误处理节点
- 分支合并:多个条件分支最终跳转到同一个汇合点
NOP - 状态机转换:在复杂状态机中,根据运行时状态跳转到不同的下一状态
INVOKE 指令
INVOKE 是对 InvokeNode 的工厂封装,执行时调用解释器的 call_sub 方法,完成一次压栈 -> 跳转 -> 执行 -> 弹栈返回的子程序调用。
执行流程
- 地址解析(
_post_compile阶段):在编译期将别名查表解析为list[int]绝对地址并缓存 - 压栈保存:
call_sub将当前指针向量压入_ret_addr_stack - 指针替换:执行指针被设置为目标子程序的入口地址
- 子程序执行:解释器从入口开始推进,执行子程序内所有节点
- 弹栈返回:子程序执行完毕后,
call_sub的finally块从_ret_addr_stack弹出原指针并恢复 - 继续推进:解释器从 INVOKE 指令的下一个节点继续执行
关键特性
- 管理调用栈:压栈保存返回地址,弹栈恢复,支持多层嵌套调用
- 子程序来源:目标子程序通常由
ARCHIVED_NODES定义,但 INVOKE 本身不限定目标必须来自存储区——任何有合法别名的节点序列都可以被 INVOKE 调用 - 参数传递:INVOKE 指令自身不传递参数。若子程序节点需要参数,应通过
Depends依赖注入声明,或在子程序节点内部自行调用call_sub传入操作数
典型使用场景
- 代码复用:将可复用的节点序列封装为子程序,在工作流中多次 INVOKE 调用
- 模块化分解:将复杂流程拆分为独立子程序,主流程只保留编排逻辑
- 中断处理:外部系统通过
call_sub(interrupt=True)调用预置的中断处理子程序
JMP vs INVOKE:对比总结
| JMP | INVOKE | |
|---|---|---|
| 是否保存返回地址 | 否 | 是(压入 _ret_addr_stack) |
| 执行完毕后行为 | 从目标节点继续向后推进 | 自动弹栈,回到调用点继续 |
| 调用栈影响 | 无 | 压栈一次,弹栈一次 |
| 适用场景 | 单向跳转、分支合并、错误跳转 | 子程序复用、模块化、中断响应 |
| 底层 API | pc.jump_to(addr) | pc.call_sub(addr) |
使用注意
- JMP 不能替代循环:JMP 没有“返回”语义,如果在循环内部使用 JMP 跳出,循环的调用栈不会被正确管理。循环内需要跳出应使用
BreakLoop,循环内需要子程序复用应使用 INVOKE - INVOKE 的目标必须是可寻址节点:被调用的节点(或子程序入口节点)必须具有
address_able=True,这是ALIAS的前提条件 - JMP 与 INVOKE 共享别名空间:两者都从
alias2vector_map查表,别名污染会影响两种指令。保持别名命名规范,避免冲突 - INVOKE 的返回依赖调用栈完整:在子程序内部使用 JMP 会置起
JMP位,导致call_sub跳过弹栈恢复。开发者需要理解:子程序内的 JMP 会“覆盖”正常的返回行为
示例
python
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:错误时直接跳转
workflow = (
start
>> do_something
>> JMP("error_cleanup")
>> ALIAS(error_handler, "error_cleanup")
)
# INVOKE:复用封装的子程序
subprogram = ARCHIVED_NODES(
ALIAS(reusable_step, "reusable"),
)
main = (
init
>> INVOKE("reusable") # 第一次调用
>> process
>> INVOKE("reusable") # 第二次复用
>> end
>> subprogram # 子程序放在末尾,正常流程跳过
)手动栈空间管理:
PUSH_RET+JMP+RET可显式控制返回地址栈,配合ARCHIVED_NODES还能实现子图式调用。完整说明请参见:高级主题:手动栈空间管理分配。
