Skip to content

Commit 490fb7d

Browse files
feat: add PUSH_STACK instruction for manual stack push in composition chains.
1 parent e4c5530 commit 490fb7d

13 files changed

Lines changed: 212 additions & 87 deletions

File tree

demos/13_ret_far.py

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,20 @@
1-
"""13_ret_far.py — Manual push + GOTO + RET_FAR pop-and-return
1+
"""13_ret_far.py — PUSH_STACK + GOTO + RET_FAR pop-and-return
22
33
Usage:
44
python demos/13_ret_far.py
55
"""
66

77
import asyncio
88

9-
from amrita_sense import ALIAS, NOP, Node, PointerVector, WorkflowInterpreter
10-
from amrita_sense.instructions import GOTO, RET_FAR
9+
from amrita_sense import ALIAS, NOP, Node, WorkflowInterpreter
10+
from amrita_sense.instructions import GOTO, PUSH_STACK, RET_FAR
1111

1212

1313
@Node()
1414
async def start() -> None:
1515
print("Start")
1616

1717

18-
@Node()
19-
async def save_ret_addr(pc: WorkflowInterpreter) -> None:
20-
"""Manually push a return destination onto _ret_addr_stack."""
21-
print(" Saving return address...")
22-
return_dest = PointerVector(pc.find_addr_alias("after"))
23-
pc._ret_addr_stack.push(return_dest)
24-
25-
2618
@Node()
2719
async def doing_work() -> None:
2820
"""The section we GOTO into."""
@@ -36,14 +28,14 @@ async def after_return() -> None:
3628

3729

3830
async def main() -> None:
39-
print("=== RET_FAR example ===")
40-
# Pattern: manual push → GOTO → RET_FAR pop-and-return
41-
# 1) save_ret_addr pushes "after" address onto _ret_addr_stack
31+
print("=== PUSH_STACK + GOTO + RET_FAR example ===")
32+
# Pattern: PUSH_STACK → GOTO → RET_FAR pop-and-return
33+
# 1) PUSH_STACK("after") pushes "after" address onto _ret_addr_stack
4234
# 2) GOTO("work") jumps to the work section
4335
# 3) RET_FAR() pops the saved address and jumps back
4436
comp = (
4537
start
46-
>> save_ret_addr
38+
>> PUSH_STACK("after")
4739
>> GOTO("work")
4840
>> ALIAS(after_return, "after")
4941
>> GOTO("end")

docs/guide/advanced/built-in_instruction_set/jump_clause.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,4 @@ main = (
117117
)
118118
```
119119

120-
> **Manual stack management**: For scenarios requiring explicit control over the return address stack — such as early exit from nested scopes or custom stack unwinding — see the `RET_FAR` instruction documented in [Advanced Topic: Manual Stack Space Management](/guide/practice/manual-stack-management).
120+
> **Manual stack management**: `PUSH_STACK` + `GOTO` + `RET_FAR` give you explicit control over the return address stack, and can be combined with `ARCHIVED_NODES` for subroutine-like patterns. See [Advanced Topic: Manual Stack Space Management](/guide/practice/manual-stack-management) for full details.

docs/guide/practice/manual-stack-management.md

Lines changed: 70 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,56 +2,48 @@
22

33
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.
44

5-
However, AmritaSense also exposes the return address stack for **manual control** via `RET_FAR`. The pattern is:
5+
However, AmritaSense also exposes the return address stack for **manual control** via `PUSH_STACK` and `RET_FAR`. The pattern is:
66

7-
1. **Manual push**A node explicitly pushes a `PointerVector` onto `_ret_addr_stack`
7+
1. **PUSH_STACK**Push an alias or address onto `_ret_addr_stack`
88
2. **GOTO** — Jump somewhere else in the workflow
99
3. **RET_FAR** — Pop the saved address and jump back
1010

1111
This lets you implement custom call/return schemes that don't follow the rigid `CALL`/`call_sub` discipline.
1212

1313
## The Return Address Stack
1414

15-
`_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. But you can also push onto it directly from any node that has access to the interpreter via `POINTER_DEPENDS`.
15+
`_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.
1616

1717
```mermaid
1818
sequenceDiagram
19-
participant N as Node (manual push)
19+
participant N as PUSH_STACK
2020
participant S as _ret_addr_stack
2121
participant W as Work Section
2222
23-
N->>S: push(PointerVector(return_addr))
23+
N->>S: push(target_addr)
2424
N->>W: GOTO("work")
2525
W-->>W: execute...
2626
W->>S: RET_FAR pops
2727
W->>N: jump_far_ptr(base_addr)
2828
```
2929

30-
## RET_FAR
30+
## PUSH_STACK and RET_FAR
3131

32-
`RET_FAR` is a factory function that creates a `RetFarNode`. At runtime, it does exactly one thing:
32+
- **`PUSH_STACK(alias_or_idata)`** — pushes the resolved address of a target alias (or a raw address list) onto `_ret_addr_stack`. It is a `PushStackNode`, placed directly in the `>>` chain.
33+
- **`RET_FAR()`** — pops the top entry from `_ret_addr_stack` and calls `jump_far_ptr` to jump to the saved address. Also a regular `BaseNode` placed in the composition chain.
3334

34-
1. Pops the top entry from `_ret_addr_stack`
35-
2. Calls `pc.jump_far_ptr(ptr.base_addr)` to jump to the saved address
35+
Neither instruction should be `return`-ed from inside a `@Node()` function — place them directly in the `>>` chain.
3636

37-
`RetFarNode` is a regular `BaseNode`, placed directly in the workflow composition — **not returned from inside another node**.
38-
39-
## Example: Manual Push + GOTO + RET_FAR
37+
## Example: PUSH_STACK + GOTO + RET_FAR
4038

4139
```python
42-
from amrita_sense import ALIAS, NOP, Node, PointerVector, WorkflowInterpreter
43-
from amrita_sense.instructions import RET_FAR, GOTO
40+
from amrita_sense import ALIAS, NOP, Node, WorkflowInterpreter
41+
from amrita_sense.instructions import GOTO, PUSH_STACK, RET_FAR
4442

4543
@Node()
4644
async def start() -> None:
4745
print("Start")
4846

49-
@Node()
50-
async def save_ret_addr(pc: WorkflowInterpreter) -> None:
51-
"""Manually push a return destination onto _ret_addr_stack."""
52-
return_dest = PointerVector(pc.find_addr_alias("after"))
53-
pc._ret_addr_stack.push(return_dest)
54-
5547
@Node()
5648
async def doing_work() -> None:
5749
"""The section we GOTO into."""
@@ -64,7 +56,7 @@ async def after_return() -> None:
6456

6557
comp = (
6658
start
67-
>> save_ret_addr
59+
>> PUSH_STACK("after")
6860
>> GOTO("work")
6961
>> ALIAS(after_return, "after")
7062
>> GOTO("end")
@@ -77,21 +69,73 @@ await WorkflowInterpreter(comp.render()).run()
7769

7870
**Flow**:
7971

80-
1. `save_ret_addr` pushes the address of `"after"` (i.e. `after_return`) onto `_ret_addr_stack`
72+
1. `PUSH_STACK("after")` pushes the address of `after_return` onto `_ret_addr_stack`
8173
2. `GOTO("work")` jumps to the `doing_work` node
82-
3. After `doing_work`, `RET_FAR` pops the saved `PointerVector` and jumps back to `after_return`
74+
3. After `doing_work`, `RET_FAR` pops the saved address and jumps back to `after_return`
8375

8476
## When to Use Manual Stack Management
8577

8678
| Scenario | Use |
8779
| ----------------------------- | ------------------------------------------------- |
8880
| Simple subroutine call/return | `CALL` + natural `call_sub` return |
89-
| Custom return destination | Manual push + `GOTO` + `RET_FAR` |
81+
| Custom return destination | `PUSH_STACK` + `GOTO` + `RET_FAR` |
9082
| Multi-level stack unwinding | Push multiple addresses, `RET_FAR` once per level |
9183
| Non-linear control flow | Combine with `GOTO` for arbitrary jump patterns |
9284

85+
## Subroutine-like Pattern with ARCHIVED_NODES
86+
87+
`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`:
88+
89+
```python
90+
from amrita_sense import ALIAS, ARCHIVED_NODES, NOP, Node, WorkflowInterpreter
91+
from amrita_sense.instructions import GOTO, PUSH_STACK, RET_FAR
92+
93+
@Node()
94+
async def start() -> None:
95+
print("Start")
96+
97+
@Node()
98+
async def step1() -> None:
99+
print(" Step 1")
100+
101+
@Node()
102+
async def step2() -> None:
103+
print(" Step 2")
104+
105+
@Node()
106+
async def after_return() -> None:
107+
print("Back here (via RET_FAR)")
108+
109+
# Self-contained subroutine: normal flow skips it, GOTO enters it.
110+
# Execution inside: step1 >> step2 >> RET_FAR() → pop stack → return.
111+
subroutine = ARCHIVED_NODES(
112+
ALIAS(NOP, "sub_entry"), # entry point marker
113+
step1,
114+
step2,
115+
RET_FAR(),
116+
)
117+
118+
comp = (
119+
start
120+
>> PUSH_STACK("after")
121+
>> GOTO("sub_entry")
122+
>> ALIAS(after_return, "after")
123+
>> subroutine
124+
)
125+
await WorkflowInterpreter(comp.render()).run()
126+
```
127+
128+
**Flow**:
129+
130+
1. `PUSH_STACK("after")` saves the return destination
131+
2. `GOTO("sub_entry")` enters the subroutine at `NOP` (the entry marker)
132+
3. `step1 >> step2` execute sequentially
133+
4. `RET_FAR()` pops the saved address and jumps back to `after_return`
134+
135+
The `NOP` aliased as `"sub_entry"` acts as the named entry point — `GOTO` targets the alias, and the node itself is a no-op.
136+
93137
## Caution
94138

95-
- **Stack integrity**: `RET_FAR` pops from `_ret_addr_stack` unconditionally. If the stack is empty, this raises an `IndexError`. Always push a corresponding address before reaching `RET_FAR`.
139+
- **Stack integrity**: `RET_FAR` pops from `_ret_addr_stack` unconditionally. If the stack is empty, this raises an `IndexError`. Always push a corresponding address (via `CALL` or `PUSH_STACK`) before reaching `RET_FAR`.
96140
- **Jump flag**: `RET_FAR` calls `jump_far_ptr` which is decorated with `@markup`, setting `_jump_marked = True`. The interpreter will NOT advance the pointer after `RET_FAR` — execution resumes at the jumped-to address.
97-
- **Not a subprogram instruction**: `RET_FAR` is a standalone node in the composition chain. Do NOT `return RET_FAR()` from inside a `@Node()` function — that return value is ignored. Place `RET_FAR()` directly in the `>>` chain.
141+
- **Not a subprogram instruction**: `PUSH_STACK` and `RET_FAR` are standalone nodes in the composition chain. Do NOT call them from inside a `@Node()` function — place them directly in the `>>` chain.

docs/zh/guide/advanced/built-in_instruction_set/jump_clause.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,4 @@ main = (
117117
)
118118
```
119119

120-
> **手动栈空间管理**对于需要显式控制返回地址栈的场景——如从嵌套作用域提前退出或自定义栈展开——请参见 `RET_FAR` 指令文档[高级主题:手动栈空间管理分配](/zh/guide/practice/manual-stack-management)
120+
> **手动栈空间管理**`PUSH_STACK` + `GOTO` + `RET_FAR` 可显式控制返回地址栈,配合 `ARCHIVED_NODES` 还能实现子图式调用。完整说明请参见[高级主题:手动栈空间管理分配](/zh/guide/practice/manual-stack-management)

docs/zh/guide/advanced/external_interrupt.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,14 @@ await interpreter.call_sub(
4545

4646
### `ARCHIVED_NODES` 的结构
4747

48-
`ARCHIVED_NODES` 是一个自编译指令,它接收一系列 `ALIAS` 节点,自动生成如下结构:
48+
`ARCHIVED_NODES` 是一个自编译指令,它接收一系列节点(通常通过 `ALIAS` 标记以支持 `CALL` 寻址),自动生成如下结构:
4949

5050
```text
5151
SubprogramJumpNode -> ALIAS(node1, "name1") -> ALIAS(node2, "name2") -> ... -> NOP
5252
```
5353

5454
- `SubprogramJumpNode` 无条件跳转到末尾的 `NOP`,因此正常执行时整个存储区被跳过。
55-
- 每个节点都有别名,外部可以通过别名寻址,按需调用其中任意一个节点
55+
- 每个节点可通过别名寻址,按需调用其中任意一个
5656

5757
### 示例
5858

docs/zh/guide/practice/manual-stack-management.md

Lines changed: 75 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,56 +2,48 @@
22

33
`CALL` 指令和 `call_sub` 方法会为你自动管理返回地址栈(`_ret_addr_stack`):进入子程序前压入当前指针,`finally` 块在返回时弹出。对于大多数工作流,这已经足够。
44

5-
然而,AmritaSense 也暴露了返回地址栈供**手动控制**,通过 `RET_FAR` 实现。其使用模式为:
5+
然而,AmritaSense 也暴露了返回地址栈供**手动控制**,通过 `PUSH_STACK``RET_FAR` 实现。其使用模式为:
66

7-
1. **手动压栈**——节点显式地将 `PointerVector` 压入 `_ret_addr_stack`
7+
1. **PUSH_STACK**——将别名或地址压入 `_ret_addr_stack`
88
2. **GOTO 跳转**——跳转到工作流中的其他位置
99
3. **RET_FAR 返回**——弹出保存的地址并跳回
1010

1111
这让你可以实现不遵循 `CALL`/`call_sub` 固定规则的自定义调用/返回方案。
1212

1313
## 返回地址栈
1414

15-
`_ret_addr_stack``WorkflowInterpreter` 上的一个 `Stack[PointerVector]``CALL` 将当前指针压入其中;`call_sub``finally` 块弹出并恢复。但你也可以通过 `POINTER_DEPENDS` 获取解释器引用,从任意节点直接压栈
15+
`_ret_addr_stack``WorkflowInterpreter` 上的一个 `Stack[PointerVector]``CALL` 将当前指针压入其中;`call_sub``finally` 块弹出并恢复。通过 `PUSH_STACK`,你可以在编排链中直接将任意别名目标压栈,无需编写自定义节点
1616

1717
```mermaid
1818
sequenceDiagram
19-
participant N as 节点(手动压栈)
19+
participant N as PUSH_STACK
2020
participant S as _ret_addr_stack
2121
participant W as 工作区
2222
23-
N->>S: push(PointerVector(返回地址))
23+
N->>S: push(target_addr)
2424
N->>W: GOTO("work")
2525
W-->>W: 执行...
2626
W->>S: RET_FAR 弹栈
2727
W->>N: jump_far_ptr(base_addr)
2828
```
2929

30-
## RET_FAR
30+
## PUSH_STACK 与 RET_FAR
3131

32-
`RET_FAR` 是一个工厂函数,用于创建 `RetFarNode`。运行时,它只做一件事:
32+
- **`PUSH_STACK(alias_or_idata)`**——将目标别名(或裸地址列表)解析后的地址压入 `_ret_addr_stack`。它是一个 `PushStackNode`,直接放在 `>>` 链中。
33+
- **`RET_FAR()`**——从 `_ret_addr_stack` 弹出栈顶条目,通过 `jump_far_ptr` 跳转到保存的地址。同样是一个放在编排链中的 `BaseNode`
3334

34-
1.`_ret_addr_stack` 弹出栈顶条目
35-
2. 调用 `pc.jump_far_ptr(ptr.base_addr)` 跳转到保存的地址
35+
两个指令都**不能**`@Node()` 函数内部 `return`——直接放在 `>>` 链中。
3636

37-
`RetFarNode` 是一个普通的 `BaseNode`,直接放在工作流编排链中——**不能从另一个节点内部 `return` 出来**
38-
39-
## 示例:手动压栈 + GOTO + RET_FAR
37+
## 示例:PUSH_STACK + GOTO + RET_FAR
4038

4139
```python
42-
from amrita_sense import ALIAS, NOP, Node, PointerVector, WorkflowInterpreter
43-
from amrita_sense.instructions import RET_FAR, GOTO
40+
from amrita_sense import ALIAS, NOP, Node, WorkflowInterpreter
41+
from amrita_sense.instructions import GOTO, PUSH_STACK, RET_FAR
4442

4543
@Node()
4644
async def start() -> None:
4745
print("开始")
4846

49-
@Node()
50-
async def save_ret_addr(pc: WorkflowInterpreter) -> None:
51-
"""手动将返回目标地址压入 _ret_addr_stack。"""
52-
return_dest = PointerVector(pc.find_addr_alias("after"))
53-
pc._ret_addr_stack.push(return_dest)
54-
5547
@Node()
5648
async def doing_work() -> None:
5749
"""GOTO 跳入的工作区。"""
@@ -64,7 +56,7 @@ async def after_return() -> None:
6456

6557
comp = (
6658
start
67-
>> save_ret_addr
59+
>> PUSH_STACK("after")
6860
>> GOTO("work")
6961
>> ALIAS(after_return, "after")
7062
>> GOTO("end")
@@ -77,21 +69,73 @@ await WorkflowInterpreter(comp.render()).run()
7769

7870
**执行流程**
7971

80-
1. `save_ret_addr``"after"`(即 `after_return`的地址压入 `_ret_addr_stack`
72+
1. `PUSH_STACK("after")``after_return` 的地址压入 `_ret_addr_stack`
8173
2. `GOTO("work")` 跳转到 `doing_work` 节点
82-
3. `doing_work` 执行完毕后,`RET_FAR` 弹出保存的 `PointerVector` 并跳回 `after_return`
74+
3. `doing_work` 执行完毕后,`RET_FAR` 弹出保存的地址并跳回 `after_return`
8375

8476
## 何时使用手动栈管理
8577

86-
| 场景 | 方案 |
87-
| ------------------- | -------------------------------- |
88-
| 简单子程序调用/返回 | `CALL` + 自然的 `call_sub` 返回 |
89-
| 自定义返回目标 | 手动压栈 + `GOTO` + `RET_FAR` |
90-
| 多级栈展开 | 压入多个地址,每级一个 `RET_FAR` |
91-
| 非线性控制流 | 结合 `GOTO` 实现任意跳转模式 |
78+
| 场景 | 方案 |
79+
| ------------------- | --------------------------------- |
80+
| 简单子程序调用/返回 | `CALL` + 自然的 `call_sub` 返回 |
81+
| 自定义返回目标 | `PUSH_STACK` + `GOTO` + `RET_FAR` |
82+
| 多级栈展开 | 压入多个地址,每级一个 `RET_FAR` |
83+
| 非线性控制流 | 结合 `GOTO` 实现任意跳转模式 |
84+
85+
## 子图式调用:配合 ARCHIVED_NODES 使用
86+
87+
`PUSH_STACK` + `GOTO` + `RET_FAR` 可以与 `ARCHIVED_NODES` 结合,创建自包含的"子程序"——正常流跳过,通过 `GOTO` 进入:
88+
89+
```python
90+
from amrita_sense import ALIAS, ARCHIVED_NODES, NOP, Node, WorkflowInterpreter
91+
from amrita_sense.instructions import GOTO, PUSH_STACK, RET_FAR
92+
93+
@Node()
94+
async def start() -> None:
95+
print("开始")
96+
97+
@Node()
98+
async def step1() -> None:
99+
print(" 步骤 1")
100+
101+
@Node()
102+
async def step2() -> None:
103+
print(" 步骤 2")
104+
105+
@Node()
106+
async def after_return() -> None:
107+
print("回到这里(通过 RET_FAR)")
108+
109+
# 自包含子程序:正常流跳过,GOTO 进入。
110+
# 内部执行: step1 >> step2 >> RET_FAR() → 弹栈 → 返回。
111+
subroutine = ARCHIVED_NODES(
112+
ALIAS(NOP, "sub_entry"), # 入口标记
113+
step1,
114+
step2,
115+
RET_FAR(),
116+
)
117+
118+
comp = (
119+
start
120+
>> PUSH_STACK("after")
121+
>> GOTO("sub_entry")
122+
>> ALIAS(after_return, "after")
123+
>> subroutine
124+
)
125+
await WorkflowInterpreter(comp.render()).run()
126+
```
127+
128+
**执行流程**
129+
130+
1. `PUSH_STACK("after")` 保存返回目标
131+
2. `GOTO("sub_entry")` 通过别名进入子程序入口(即 `NOP`
132+
3. `step1 >> step2` 顺序执行
133+
4. `RET_FAR()` 弹出保存的地址,跳回 `after_return`
134+
135+
别名标记为 `"sub_entry"``NOP` 作为命名入口——`GOTO` 以别名定位,节点本身不执行任何操作。
92136

93137
## 注意事项
94138

95-
- **栈完整性**`RET_FAR` 无条件从 `_ret_addr_stack` 弹出。如果栈为空,会引发 `IndexError`。始终确保在到达 `RET_FAR` 之前已压入对应地址。
139+
- **栈完整性**`RET_FAR` 无条件从 `_ret_addr_stack` 弹出。如果栈为空,会引发 `IndexError`。始终确保在到达 `RET_FAR` 之前已压入对应地址(通过 `CALL``PUSH_STACK`
96140
- **跳转标记**`RET_FAR` 调用 `jump_far_ptr`,该方法被 `@markup` 装饰,会设置 `_jump_marked = True`。解释器在 `RET_FAR` 之后不会推进指针——执行会从跳转目标地址继续。
97-
- **不是子程序指令**`RET_FAR` 是编排链中的独立节点。不要从 `@Node()` 函数内部 `return RET_FAR()`——该返回值会被忽略。将 `RET_FAR()` 直接放在 `>>` 链中。
141+
- **不是子程序指令**`PUSH_STACK``RET_FAR` 是编排链中的独立节点。不要从 `@Node()` 函数内部调用它们——直接放在 `>>` 链中。

0 commit comments

Comments
 (0)