# P4-10 逐 opcode 翻译器(per-op translator)——PJ10 工程方向

> 状态:**详细设计**(PJ10 启动文档,2026-06-30)。本文是 PJ10 工程的单一事实源——把 P4 从「按函数样子识别 + 写死字节级模板」彻底换成「逐 opcode 翻译,任意形式的 Proto 都能升 P4」。
>
> 上游契约:[00-overview](./00-overview.md)(P4 总览;本文是 §0 文档地图新增一行 10)、[01-launch-judgment](./01-launch-judgment.md)(P4 立项判定依然成立,本文是「方向修正」非「立项再议」)、[02-template-direction](./02-template-direction.md)(原方向裁决:JSC Baseline / V8 Sparkplug per-function 模板编译;本文承认其大方向,**对「形式识别 + 字节级模板」的具体完成方式做修正**——§1.3 详)。
>
> P1 依赖面:[../p1-interpreter/02-bytecode-isa](../p1-interpreter/02-bytecode-isa.md)(源 ISA 完整定义,本文输入)、[../p1-interpreter/05-interpreter-loop](../p1-interpreter/05-interpreter-loop.md)(解释器主循环,P4 翻译产物与之 byte-equal)。
>
> P3 对照面:[../p3-wasm-tier/02-translation](../p3-wasm-tier/02-translation.md)(P3 wasm 已经是「逐 opcode 翻译」的范式;本文继承其翻译框架——CFG + relooper + 逐 opcode emit + 渐进白名单——只把发射目标从 wasm 字节码换成 amd64/arm64 原生码)。
>
> P5 下游:[../p5-trace-jit/00-overview.md](../p5-trace-jit/00-overview.md)(trace JIT 的前置假设是「baseline 层能为任意 opcode 发射机器码」,本文交付的就是这层;PJ10 完成后 P5 立项的基础设施就齐了)。
>
> P4 内部:[03-speculation-ic](./03-speculation-ic.md)(投机 + IC;本文承担「逐 op emit」基线,投机模板套在 emit 顶上)、[04-osr-deopt](./04-osr-deopt.md)(OSR exit 协议,emit 函数遇到 guard 时生成 exit stub 经此回栈)、[05-system-pipeline](./05-system-pipeline.md)(四项税 / mmap+W^X / trampoline,本文复用)、[06-backends](./06-backends.md)(双后端寄存器约定;本文新增 per-op emit 函数也按 §3 的 ABI 写)。

对应 Go 包:`internal/gibbous/jit/{amd64,arm64}`(per-arch emit 函数 + 共享翻译器骨架,详 §3)。

---

## 0. 一句话定位

**把每条 Lua opcode 单独翻译成原生机器码,串成函数体——任意形状的 Proto 都能升 P4,不再受「字节级模板」白名单限制。** 这与 P3 wasm 翻译器([../p3-wasm-tier/02-translation](../p3-wasm-tier/02-translation.md))一样的思路,只是发射目标从 wasm 字节码换成 x86-64 / aarch64 原生码。

---

## 1. 为什么换方向(PJ0-PJ9 的字节级模板路径走到了天花板)

### 1.1 PJ0-PJ9 现状(2026-06-30)

PJ0-PJ9 的设计是 **「按函数样子识别 + 写死字节级模板」**:`analyzeShape(*Proto)` 返一个 `shapeInfo`,扫描函数字节码看「长得像不像」预定义的几种形式:

- 单 BB「值产生 + RETURN A 1/2」(LOADK/LOADBOOL/LOADNIL/MOVE/GETUPVAL 等十几种)
- 二段算术链 + RETURN(`return a + b * c`)
- FORLOOP 空 body / 单条 body / 二段 body 字节级 inline
- 表 IC ArrayHit / NodeHit(getter / setter / SELF method 六模板)
- CALL void / TAILCALL / SELF + CALL spec template(0-7 参 × N=2-15 返值 × 双 receiver)

每种形式对应一段手写汇编模板,长度 ~70-200 字节,Compile 时 mmap 一块可执行内存,把模板字节码拷进去,跳进去执行。

**这套设计在自己适配的形式上很厉害**——V14 luajc 档实测 12.6× / 25.1× / 13.5× over gopher-lua,远超 4.4× 基线([implementation-progress §8](./implementation-progress.md)),完整兑现了 P4 立项动机([01-launch-judgment §4](./01-launch-judgment.md))。

### 1.2 撞墙现象:覆盖率被「形式命中」卡死

但 V15b heavy bench(2026-06-30 加入,见 [09-acceptance-checklist §3.V15b](./09-acceptance-checklist.md))实测显示:

| 脚本 | kernel 形式 | P4 升层数 |
|---|---|---|
| heavy_arith | `for i=1,n do local x = i*1.5; s = s + x*x - x/2 + 0.25 end` | **0** |
| heavy_recursion | `if n == 1 then return s; if n%2==0 then return f(n/2,s+1); return f(3n+1,s+1)` | **0** |
| heavy_floatloop | `while i<256 do ... ; i = i+1 end` 嵌套 FORLOOP | **0** |

三个 kernel 全升 0,落回 P1 解释器。原因:

1. **`for i=1,n do` 内 body 多段算术 + 跨段累加** — 不在 PJ3 FORLOOP「空 body / 单 reg-K body / 二段 reg-K body」白名单内
2. **自递归 + 多 `if-then-end` 分支** — 不在 PJ5 CALL void / TAILCALL / SELF 形式白名单内
3. **`while` 单条件循环 + 嵌套 FORLOOP** — 没有对应字节级模板

字节级模板每加一种,工程量 1-3 天(写 amd64 模板 + arm64 模板 + 字节级单测 + 双轨 byte-equal + difftest 角落语料)。Lua 函数样子组合无穷,**这条路有天花板,而且天花板比想象中低**——heavy 三个脚本是写得很普通的浮点循环,就已经全跌穿。真实宿主代码里更复杂的循环嵌套、多分支、表/字符串混合操作,几乎全跌。

### 1.3 PJ0-PJ9 的物理基础保留,只换发射策略

注意:**PJ10 不是把 PJ0-PJ9 推倒重来**。PJ0-PJ9 交付的物理基础全部保留:

| PJ0-PJ9 交付物 | PJ10 复用方式 |
|---|---|
| `internal/gibbous/jit` 包骨架 + build tag 互斥 + bridge.P3Compiler 注入接口 | 完全保留(P4 还是「一个 P3Compiler 实现」) |
| amd64 mmap / W^X / icache flush / trampoline 四件套([05-system-pipeline](./05-system-pipeline.md))| 完全保留 — per-op 翻译产物还是一段可执行字节码,落进一样的 mmap 段 |
| arm64 codepage + trampoline + MAP_JIT([06-backends](./06-backends.md))| 完全保留 |
| `p4Code` / `Run` 接口([../p2-bridge/05](../p2-bridge/05-p3-p4-interface.md))| 完全保留 |
| 14 个 host helper([implementation-progress §7](./implementation-progress.md))| **大部分保留**,逐 op 翻译时直接 call 这些 helper 处理类型多态慢路径 |
| OSR exit 协议 + `p4SpecState[proto]` 子状态机([04-osr-deopt](./04-osr-deopt.md))| 完全保留 — guard 失败时仍走 OSR exit |
| 投机模板 + IC inline([03-speculation-ic](./03-speculation-ic.md))| **保留作 fast path** — 见 §6 |
| 字节级模板(PJ0-PJ9 的全部产物 ~25+ 形式)| **保留作 fast path** — 见 §6 |

**变的只有一件事**:`SupportsAllOpcodes` 不再是 `analyzeShape(proto).ok`,而是 P3 wasm 那种 `supported [numOps]bool` 白名单 — 一旦每个 op 都接,任何 Proto 都能升 P4。

### 1.4 [02-template-direction](./02-template-direction.md) 是否被推翻

不被推翻。02 文档的核心裁决是:

1. **JSC Baseline / V8 Sparkplug 风格 per-function 模板编译** — 这条仍然成立。「模板」在 02 的语境里指「按方法/函数为单位编译」,与「按 trace 编译」(JIT 设计的两大分类)对立。本文承担的「逐 op 翻译」也是按函数为单位的。
2. **不上 IR、不上跨指令 regalloc** — 这条也成立。逐 op 翻译每个 op 单独 emit,寄存器约定固定,不做 IR / 不做活跃性分析 / 不做跨 op 寄存器复用。
3. **F1-F7 检查继续生效** — 完全成立。本文实际上让 F7 真正发挥作用(原来 PJ0-PJ9 的 F7 是 `analyzeShape.ok`,本文改成「所有 op 都在 supported 集」)。

被修正的是 **02 §4「模板的具体物理形式」**:02 §4 倾向「整函数一段连续机器码,emit 时按形式查表填模板」,本文修正为「emit 时按 opcode 串行翻译,每个 op 独立 emit 一小段」。**这是 02 在 spike 前未亲手验证的细节**,02 §1.2 也明示「具体形式留 spike 后定」,本文是 spike 后的兑现。

---

## 2. 与 P3 wasm 翻译器的对照(直接借鉴)

P3 wasm `internal/gibbous/wasm/translate.go` 已经实现了「逐 op 翻译」的全套基础设施。PJ10 的核心捷径是 **借鉴 P3 的翻译框架,只换发射目标**。对照表:

| 翻译器组件 | P3 wasm 实现 | P4 PJ10 设计 |
|---|---|---|
| **CFG 构造** | `cfg.go::buildCFG` — 扫一遍 leader,切 BB,linkSuccs | **完全复用方法论**;P4 单独实现,但 BB 划分规则与 P3 一致(JMP/FORLOOP/FORPREP/RETURN/EQ/LT/LE/TEST/TESTSET/TFORLOOP/LOADBOOL 切 BB)|
| **可达性 + 可约简性** | `reachableBlocks` + `analyzeRelooper.isReducible` | **复用算法**;P4 同样要求 reducible CFG(不可约简则 SupportsAllOpcodes 返 false 拒升)|
| **结构化控制流** | `relooper.go` — 把 reducible CFG 还原成 if/loop/block 结构,wazero 接 wasm 结构化指令 | **不需要还原结构** — 原生码可以直接发 jmp/jcc(条件跳/无条件跳),无需 if/loop 结构化指令。直接按 BB 顺序 emit,BB 间用 label + jmp 链接,比 P3 简单 |
| **逐 op emit** | `translate.go::translateOp` switch on `bytecode.Op` | **完全复用 switch 结构**;每个 case 调对应 emit 函数 |
| **emit 函数** | `emit_*.go` — 一个 op 一个文件,产 wazero 字节码序列(api.ValueType 操作数 + opcode 字节)| **per-arch 各一份**:`amd64/emit_*.go` 产 x86-64 字节;`arm64/emit_*.go` 产 aarch64 字节 |
| **value stack 抽象** | P3 用 wasm 内嵌的 value stack(`local.get`/`local.set` 读写)| P4 直接用 **`R(A)` 槽内存地址寻址**(寄存器+偏移,不缓存值到 cpu reg),与 P1 解释器一样的槽语义 |
| **safepoint 布点** | 回边 + 调用前后([../p3-wasm-tier/05](../p3-wasm-tier/05-safepoint-gc.md))| 一样的规则,emit 时按 op 类型插入 safepoint check(已有 EmitSafepointCheck) |
| **跨层互调** | `h_call` / `h_tailcall` helper(已实现)| 仍走 host helper,emit `CALL` 时发一段「准备参数 + call 助手」的字节序列 |
| **IC 快照固化** | `emit_table.go` 经 P2 IC slot 直达槽 | 复用 PJ4 字节级 IC 模板(`emitter_pj4.go`)作 fast path,具体见 §6 |
| **`SupportsAllOpcodes`** | `compiler.go::SupportsAllOpcodes` — 渐进白名单 + 字符串常量拒 + 死代码不数 + reducible 校验 | **完全相同的算法**,只是 supported 数组随 PJ10a..d 扩张节奏不一样 |

**结论**:P3 wasm 已经把「逐 op 翻译器」整套的工程问题都解过了——CFG / relooper / 死代码 / 字符串常量 / safepoint / IC / 跨层互调,都有现成答案。**PJ10 不需要重新发明,只需要在 amd64 / arm64 端写每个 op 的 emit 函数,把对应的字节序列发出来**。

---

## 3. 架构总图

```
┌──────────────────────────────────────────────────────────────────┐
│                  P4 Compiler.Compile(*Proto)                     │
└──────────────────────────────────────────────────────────────────┘
       │
       │ ① analyzeShape(proto) 还在,但变成 fast path 判定:
       │    匹配 PJ0-PJ9 字节级模板 → 走 specialized 路径(性能极致)
       │    不匹配 → 走 ② 通用 per-op 翻译
       │
       ├──── fast path ────────────────────────────┐
       │     PJ0-PJ9 spec templates 保留           │
       │     - PJ2 投机算术 reg-reg/reg-K/chain-KK│
       │     - PJ3 FORLOOP 字节级 inline           │
       │     - PJ4 表 IC 六路径                    │
       │     - PJ5 CALL void / SELF spec templates │
       │                                           │
       └──── general path (PJ10 新增) ─────────────┤
                                                   │
       ② buildCFG(proto) — 切 BB,linkSuccs        │
       ③ reach + reducibility check                │
       ④ 按 BB rPostOrder 顺序 emit:               │
          for each BB:                             │
            emit_bb_label(bb.id)                   │
            for pc in [bb.startPC, bb.endPC):     │
              emit_<op>(proto, pc)                 │
            emit_terminator(bb)                    │
       ⑤ resolveLabels — 补 jmp 偏移               │
                                                   │
       └───────────────────────────────────────────┘
                                                   │
                                                   ▼
                              ┌──────────────────────────────────┐
                              │ wireToCodepage                   │
                              │ - mmap RW                        │
                              │ - 写字节                         │
                              │ - mprotect RX (linux) /          │
                              │   pthread_jit_write_protect_np   │
                              │   (darwin)                       │
                              │ - icache flush                   │
                              │ - 返 p4Code{entry, ...}          │
                              └──────────────────────────────────┘
```

**关键设计点**:

- **双轨**:`analyzeShape` 不删,作 **fast path 入口**。匹配字节级模板 → 走极致优化路径(PJ0-PJ9 全套);不匹配 → 走通用 per-op 翻译(PJ10 新增)。这样 V14 luajc 档(列内核形式命中 spec template)不丢精度,V15b heavy 形式走通用路径也能升 P4。
- **不引入 IR**:每个 op 直接发 amd64/arm64 字节,不经任何中间表示。寄存器约定固定(详 §4),不做活跃性分析、不做跨 op 寄存器复用。
- **per-arch 双份 emit**:amd64 一份、arm64 一份,跟现有 `internal/gibbous/jit/amd64/` 和 `internal/gibbous/jit/arm64/` 结构对齐。共享部分(CFG / label resolver / supported 表)在 `internal/gibbous/jit/` 主包。

---

## 4. 寄存器 ABI(amd64 端)

承 [05-system-pipeline §4](./05-system-pipeline.md) + [06-backends §3](./06-backends.md) 已有的寄存器约定,PJ10 通用 per-op 翻译沿用一样的 ABI,无新增:

| 寄存器 | 用途 | 跨 op 稳定性 |
|---|---|---|
| `R14` | `vsBase` — 值栈基地址(`R(0)` 在内存里就是 `[r14+0]`)| 永远稳定,emit 不动 |
| `R15` | `jitCtx` — host helper 跨层指针 | 永远稳定,emit 不动 |
| `RAX` | 临时寄存器 / RETURN 返值 | 单 op 内可任意用,跨 op 不保证 |
| `RCX` / `RDX` / `RSI` / `RDI` | 临时寄存器 / host helper SysV ABI 参数 | 同上 |
| `XMM0-3` | 浮点临时 | 同上 |
| `RBP` / `RSP` | 帧指针 / 栈指针 | trampoline 进入时建立帧,emit 不破坏 |

**重要约束**(承 PJ0-PJ9):

1. **不在寄存器里缓存 `R(A)` 值跨 op**:`R(A)` 永远存值栈槽 `[r14+A*8]`,每个 op 自己 load / store。即「**guard 即栈槽真相**」([04-osr-deopt §3](./04-osr-deopt.md))不变,OSR exit 时栈槽就是真相,不需要物化序列。
2. **safepoint check 不破坏 RAX/RCX**:safepoint check 用「`cmpq $0, gcPending(R15) + jne slow_path`」固定序列,RAX/RCX 在此前后必须一致(若 emit 当前 op 用了 RAX,emit 完该 op 再 emit safepoint check)。
3. **host helper 调用前后 RAX/RCX 视作 clobbered**:emit 一段 `call h_xxx` 后,所有 caller-saved 寄存器(RAX/RCX/RDX/RSI/RDI/R8-R11 + XMM0-15)视为不定,后续 op 重新 load。

arm64 端寄存器映射(承 PJ8):`X26 = vsBase`,`X27 = jitCtx`,`X0-X7` 调用约定参数,其余按 AAPCS64。具体在 [06-backends §5](./06-backends.md) 已字面化,本文不重抄。

---

## 5. opcode 翻译表(完整 0..37)

按 P3 PW2-PW7 渐进白名单结构,PJ10 拆成 4 个里程碑(PJ10a-d),累积扩 supported 集:

### 5.1 PJ10a 直线 opcode(MOVE / LOADK / LOADBOOL / LOADNIL / GETUPVAL / SETUPVAL / JMP / RETURN)

| op | A/B/C 语义 | amd64 emit 形式(伪汇编)|
|---|---|---|
| MOVE | R(A) := R(B) | `mov rax, [r14+B*8]; mov [r14+A*8], rax` |
| LOADK | R(A) := K(Bx) | `mov rax, imm64; mov [r14+A*8], rax`(K 编译期烧 imm64;字符串常量同 P3 拒 — F7 把关)|
| LOADBOOL | R(A) := bool(B);if C: pc++ | `mov rax, IMM_TRUE_OR_FALSE; mov [r14+A*8], rax;` C != 0 时多发一条 jmp 到 pc+2 的 BB 入口 |
| LOADNIL | R(A..B) := nil | 循环 emit `mov [r14+i*8], NIL_BITS`(B-A+1 次) |
| GETUPVAL | R(A) := U(B) | call host.GetUpval(B);RAX → `[r14+A*8]` |
| SETUPVAL | U(B) := R(A) | `mov rdi, [r14+A*8]; call host.SetUpval(B, rdi)` |
| JMP | pc += sBx | 由 BB terminator 处理 — emit `jmp <bb_label_of_target>` |
| RETURN | 出口 | call host.DoReturn(A, B);ret |

**关键**:这一档全是「值在内存里搬来搬去」+ 偶尔调 helper,不涉及类型判断、不涉及 guard。是 PJ10 最简单的一档,先打通 fully end-to-end pipeline。

### 5.2 PJ10b 算术 + 比较(ADD/SUB/MUL/DIV/MOD/POW/UNM/LEN/CONCAT/NOT/EQ/LT/LE/TEST/TESTSET)

每个算术 op 两条路:

- **fast path**:reg-reg / reg-K / chain-KK 形式 → 跑 PJ2 已有的字节级投机模板(IsNumber guard + SSE2 算术 + 失败 OSR exit)
- **slow path**(本档新增):任意形式 → emit 「load R(B), load R(C), call host.Arith(op, b, c), store R(A)」

emit 函数签名(amd64):

```go
func emitADD(buf *codeBuffer, A, B, C uint8)
func emitSUB(buf *codeBuffer, A, B, C uint8)
// ...
```

比较 op(EQ/LT/LE)既要 emit 算术比较又要 emit 条件跳:

```go
func emitLT(buf *codeBuffer, A uint8, B, C uint16, falseTarget BBLabel)
//    emit:
//      mov rdi, [r14+B*8]
//      mov rsi, [r14+C*8]
//      call host.LessThan
//      cmp eax, A     ; A 是 cmpA 字段
//      je <falseTarget>
//      ; fall through to next BB (= jumpTarget of JMP)
```

### 5.3 PJ10c 控制流 + 循环(FORPREP / FORLOOP / TFORLOOP)

- **FORPREP fast path**:全常量 init/limit/step → PJ3 字节级 inline(已有)
- **FORPREP slow path**(本档新增):任意形式 → emit 「load + call host.ForPrep + 条件跳」
- **FORLOOP fast path**:空 body / 单 reg-K body / 二段 reg-K body → PJ3 字节级 inline(已有)
- **FORLOOP slow path**(本档新增):任意 body → emit 「idx += step; cmp limit; jmp back-edge」(idx/limit/step 都是 SSE float;back-edge 前插 safepoint check)
- **TFORLOOP**:emit 「call host.TForLoop;条件跳」

### 5.4 PJ10d 表 + 函数调用(GETTABLE / SETTABLE / GETGLOBAL / SETGLOBAL / SELF / NEWTABLE / SETLIST / CALL / TAILCALL / CLOSURE / CLOSE)

每个表 op / 调用 op 两条路:

- **fast path**:命中 PJ4 字节级 IC 模板 / PJ5 CALL spec template → 走 PJ4/PJ5 spec(已有)
- **slow path**(本档新增):任意形式 → emit 「load + call host.GetTable/SetTable/CallBaseline/...」

CALL 形式特别注意:`B == 0`(参数到 top)/ `C == 0`(返回到 top)的多值窗口形式,emit 「准备 vararg 头 + call host.CallBaseline 处理 nargs/nresults 不定参形式」。

### 5.5 VARARG 永不接(同 P3)

VARARG 经 P2 F1 检查拒升([../p2-bridge/03 §3.1](../p2-bridge/03-compilability-analysis.md)),本档不实现。supported[VARARG] 永远 false。

---

## 6. fast path 与 slow path 的协作

P4 PJ10 最重要的工程纪律 — **不重写 PJ0-PJ9,只在外面套通用路径**:

```
SupportsAllOpcodes(proto):
    # PJ10 新逻辑(承 P3 wasm 一样的):
    cfg = buildCFG(proto)
    if not all_ops_in_supported(cfg, reachable=True): return False
    if not reducible(cfg): return False
    if any LOADK is string-const: return False  # 同 P3
    return True

Compile(proto):
    # ① 试 fast path(PJ0-PJ9 字节级模板,V14 luajc 档由此保住)
    if shape := analyzeShape(proto); shape.ok:
        return emitFastPathFromShape(shape)

    # ② 通用 per-op 翻译(PJ10 新增)
    cfg := buildCFG(proto)
    buf := newCodeBuffer()
    emitProlog(buf)
    for bb in rPostOrder(cfg):
        buf.bindLabel(bbLabel(bb.id))
        for pc := bb.startPC; pc < bb.endPC-1; pc++:
            emitOp(buf, proto.Code[pc])
        emitTerminator(buf, cfg, bb)
    emitEpilog(buf)
    buf.resolveLabels()
    return wireToCodepage(buf)
```

这样:

- **V14 luajc 档**:`for i=1,1000 do end` 形式命中 PJ3 FORLOOP spec template,走 fast path,保持 12-25× over gopher
- **V15b heavy**:`heavy_arith` 等形式 `analyzeShape` 不命中,走通用 per-op 翻译,虽然单 op 性能逊于字节级 spec,但**至少能升 P4 跑起来**,不像现在一样落回 P1

**预期 V15b heavy 上 P4 通用路径性能**:理论上 ≥ P3 wasm(因为 wasm 还要经 wazero 二次编译,P4 直接发原生码;且不经 wasm linear memory 跨层税),比 P1 解释器快 1.5-2.5× 是合理预期。具体数字 PJ10 完成后实测。

---

## 7. label / 跳转的处理(原生码 vs wasm 的关键差异)

P3 wasm 有 `block`/`loop`/`if-else`/`br`/`br_if` 五种结构化控制流指令,wasm 验证器要求嵌套合规 — 所以 P3 要 relooper 把 reducible CFG 还原成嵌套结构。

P4 原生码没有结构化指令,直接发条件跳(`jcc`)和无条件跳(`jmp`)。流程:

1. **第一遍 emit**:遇到跳目标 BB 不在当前位置时,emit `jmp 0x00000000`(占位 32-bit 偏移),记录这个 `jmp` 的 32-bit 偏移位置 + 目标 BB 标号到 `fixupList`
2. **绑定 BB label**:emit BB 的第一条 op 前,记录「BB X 的入口偏移 = 当前 buf 长度」到 `labelMap[X]`
3. **第二遍 resolve**:遍历 `fixupList`,把占位偏移填成 `labelMap[targetBB] - (jmpInstrEnd)`(32-bit 有符号偏移)

这是经典的 **two-pass assembler** 套路,工程量小,1-2 天就能写好框架(amd64 端,arm64 端一样的思路用 4-byte 立即数)。

P4 不需要 relooper,因为原生码不要求结构化 — **比 P3 简单**。但仍要求 CFG reducible:reducible 是「跳目标都是 leader」的等价条件,non-reducible 意味着「跳到 BB 中间」,这种情况只发生在 Lua 的 `goto`,而 luac 5.1.5 不生成 `goto`(5.2+ 才支持),所以 wangshu 的 luac 编出的所有 CFG 都 reducible,F7 把关即可。

---

## 8. supported 数组的渐进扩张

承 P3 PW2-PW7 节奏,PJ10 拆成 4 个 sub-PJ:

| Sub-PJ | 新增 supported(累积) | 工程量 | 验收 |
|---|---|---|---|
| **PJ10a** | 直线 opcode(8 个):MOVE / LOADK / LOADBOOL / LOADNIL / GETUPVAL / SETUPVAL / JMP / RETURN | 1-2 周 | end-to-end pipeline:从 `Compile` 到 `wireToCodepage` 到 `Run`,跑通最简 Proto(`return 42`)+ byte-equal P1 |
| **PJ10b** | 算术 + 比较 + UNM/NOT/LEN/CONCAT(15 个):ADD/SUB/MUL/DIV/MOD/POW/UNM/NOT/LEN/CONCAT/EQ/LT/LE/TEST/TESTSET | 3-4 周 | heavy_arith / heavy_floatloop 升 P4 + byte-equal P1 + 比 P1 快 |
| **PJ10c** | 控制流 + 循环(3 个):FORPREP / FORLOOP / TFORLOOP | 2-3 周 | 含 FORLOOP 的脚本全升 P4(realworld fib 等 + heavy 全套)+ byte-equal |
| **PJ10d** | 表 + 函数调用(10 个):GETTABLE/SETTABLE/GETGLOBAL/SETGLOBAL/SELF/NEWTABLE/SETLIST/CALL/TAILCALL/CLOSURE/CLOSE | 4-6 周 | realworld 五脚本全升 P4 + byte-equal,V15a P4 ≥ P3 真兑现 |

**总工程量估算:10-15 周**(约 2.5-4 人月)。比 [02-template-direction §1.4](./02-template-direction.md) 自估「P4 全栈 +1-2 人年」中的「单架构后端」部分 0.5-1 人年范围内,与 P3 PW0-PW10 总耗时(约 4 个月)相当。

PJ10b/c/d 同时启用 fast path(承 PJ0-PJ9 全套),即使本 sub-PJ 通用路径没接到所有 op,fast path 已接的形式仍保留性能。

---

## 9. 与 PJ11 的关系

PJ11 是原 PJ10 的「luajc 档验收 + 性能调优」,**判定门没变**:V14 列内核负载 ≥ 4.4× over gopher-lua。该门 PJ3 阶段早已突破(实测 12-25×),PJ11 只是把 V14 数字定稿、配套调优(locals 寄存器缓存可能展开 / guard 合并窥孔等,见 [00-overview §11](./00-overview.md))。

PJ10 完成后 PJ11 的工作量 **比原计划下降**——通用 per-op 翻译路径让 V15 / V15b 的数字稳健,不再需要为每种新形式写字节级模板。PJ11 的「性能调优」聚焦在两件事:

1. **fast path 命中率**:PJ4 IC inline / PJ5 CALL spec / PJ3 FORLOOP inline 在通用路径同时存在时的真实命中率(实测)
2. **通用路径细优化**:寄存器/栈槽访问的窥孔合并、host helper 调用前后的临时寄存器复用

---

## 10. 与 P5 trace JIT 的接口

[../p5-trace-jit/00-overview.md](../p5-trace-jit/00-overview.md) 是下一站,前置假设是 **「baseline 层能为任意 opcode 发射机器码」**。当前 PJ0-PJ9 的字节级模板路径**不满足这条**——baseline 只接已识别形式,trace 拿到一串 opcode 没法直接喂给 baseline emit。

PJ10 交付后,这条假设第一次成立:

```
trace recording → trace optimization → trace emit:
    for op in trace_ops:
        baseline.emitOp(buf, op)  # ← P4 PJ10 提供
    # 优化阶段:trace JIT 可以在 emitOp 上层做 guard hoisting / dead op 消除 / 跨 op 寄存器复用
```

PJ10 是 P5 立项的关键基础设施 — 没有 PJ10,P5 trace JIT 是空中楼阁。

---

## 11. 风险与缓解

| 风险 | 等级 | 缓解 |
|---|---|---|
| 通用 per-op 翻译性能不及预期(< P3 wasm)| 中 | fast path 兜底已有 V14 数字;通用路径只需 ≥ P1 解释器即可算「升对了」 |
| emit 函数 38 个,工程量超估 | 中 | 直接借鉴 P3 wasm 同名 emit 函数(P3 已实现 30 个,差别只在「发 wasm 字节 → 发原生字节」),逻辑结构可整段照搬 |
| double-emit(fast path 已生效,通用路径仍写一份)增加维护负担 | 低 | 通用路径作为 fallback,fast path 不命中时才用;两条路径单独 byte-equal 测;且通用路径的 emit 函数 PJ10d 完成后就稳定了 |
| arm64 端 per-op emit 与 amd64 完全对齐工作量翻倍 | 中 | 同 P3 PW0-PW10:amd64 先打通,arm64 跟上;两份 emit 函数签名一致,逻辑结构对应,只是字节序列不同 |
| 通用路径暴露新 byte-equal bug(P1 解释器某条角落语义 P4 通用路径没复制对)| 中 | 全套 difftest / conformance / luasuite 已就绪;每个 sub-PJ 完成后跑全套 |

---

## 12. 决议项

| # | 项 | 状态 | 备注 |
|---|---|---|---|
| D10-1 | PJ10 是否启动 | ✅ 已决议 | 2026-06-30 用户确认 |
| D10-2 | fast path 是否保留 | ✅ 保留 | 见 §6 — PJ0-PJ9 所有 spec templates 作 fast path |
| D10-3 | sub-PJ 拆分(PJ10a-d)是否按 §8 节奏 | ⬜ | PJ10a 启动前确认 |
| D10-4 | amd64 / arm64 并行还是串行 | ⬜ | 倾向串行(amd64 先打通 PJ10a-d,arm64 跟上一样的节奏),与 P3 PW0-PW10 节奏对位;并行风险是 arm64 物理 runner 还在 PJ8 工程中 |
| D10-5 | PJ11 V14 / V15 验收数字是否在 PJ10 完成时一并刷 | ⬜ | PJ10d 完成时实测 |

---

## 13. 不变式

PJ10 接续 PJ0-PJ9 的所有不变式,新增本档独有:

1. **fast path 不动**:PJ0-PJ9 字节级模板的 byte-equal P1 与 byte 序列**不能因 PJ10 通用路径接入而改变**(回归测试把关)
2. **per-op 通用路径与 P1 解释器 byte-equal**:每个 emit 函数的产物执行结果与 P1 解释器对应 op 完全一致(逐字节,含 NaN bit pattern / 错误消息 / traceback pc)
3. **OSR exit 仍生效**:通用路径里的 guard 失败(若有,主要在 fast path 内嵌投机时)走 OSR exit 协议,与 PJ0-PJ9 一样的
4. **safepoint 不漏**:回边、call 前后、长直线段后(每 N 条指令)都插 safepoint check,纪律承 [05-system-pipeline §6](./05-system-pipeline.md)
5. **不引入 IR**:emit 函数直接发字节,不经 SSA / 中间值 / 任何抽象层

---

## 14. 实现进度

### 14.1 第一轮:Go 端回放骨架(2026-06-30,21 commits,承 [[2026-06-30-p4-pj10-perop-translator-round]])

PJ10 首版完成采取「正确性 floor 优先」路径:不真正发射多 BB amd64 原生码,而是把 Proto 的指令拆成 Go 端「head op + side effect」的回放清单,mmap 段只是占位 stub(`xor eax, eax; ret`),Run 在执行 stub 后用 Go 代码按清单调 host helper / 写寄存器。语义与 P1 解释器 byte-equal,80 e2e 全过。

「Go 端回放」路径覆盖到的 opcode:

| 子档 | 覆盖 opcode | 形式举例 |
|---|---|---|
| **PJ10a 直线** | MOVE / LOADK / LOADBOOL(C=0) / LOADNIL(含 multi-slot + scratch fill)/ GETUPVAL / SETUPVAL / RETURN(B=0 multret tail-call + B=1 setter + B>=2 多返回)| `return x, y, 1, 2` / `local outer; function set(v) outer = v end` |
| **PJ10b 算术 + 一元 + 拼接 + NOT** | ADD / SUB / MUL / DIV / MOD / POW / UNM / LEN / NOT / CONCAT | `return a + b, a - b` / `return -x, not y, #z` / `return a .. b` |
| **PJ10c 比较 diamond** | EQ / LT / LE(4-op 折叠形式) | `return a == b` / `return a < b` / `return a ~= b` / `return a > b` |
| **PJ10c 短路 diamond** | TESTSET + JMP + MOVE/LOADK(3-op 折叠) | `return x and y` / `return x or 0` |
| **PJ10c 数值循环** | FORPREP + FORLOOP(Go 端 host.ForPrep + 迭代 dispatch + bodyEffects 回放)| `for i=1,n do s = s + i end` |
| **PJ10c 泛型循环** | TFORLOOP(JMP-skip-body + body + TFORLOOP + JMP-back 形式,host.TForLoop 驱动)| `for k, v in customIter do ... end`(自写迭代器) |
| **PJ10d 表 + 全局** | GETTABLE / SETTABLE / GETGLOBAL / SETGLOBAL / NEWTABLE / SETLIST | `return t.x` / `return {1,2,3}` / `_G.foo = 1` |
| **PJ10d 函数调用** | CALL / TAILCALL / SELF | `local r = f(x)` / `return f(x)` / `obj:method()` |
| **PJ10d 闭包** | CLOSURE(读 `Proto.SubNUps[Bx]` 跳 pseudo 伪指令)/ CLOSE | `local f = function() return x end` |

**38 个核心 opcode 中回放骨架接住 35 个**(VARARG 设计上永不接;JMP / TEST / LOADBOOL C!=0 留待真 CFG)。回放阶段架构图对应 §3 的**正确性 floor**——语义前提,性能前提留下一轮。

---

### 14.2 第二轮:真 amd64 / arm64 原生码 emit(2026-07-01,ed2235b..0c9db3a ~17 commits,分支 `feat/pj10-native`,承 [[2026-07-01-p4-pj10-native-round]])

承 §14.1 回放骨架的 floor,本轮把 mmap 段的占位 stub 换成真原生 codegen——CFG builder + 两遍 label resolver + 35 opcode 每 arch 一份 emit(amd64 / arm64)。

**opcode emit 覆盖(每 arch 独立发射,2026-07-01 状态)**:

| 阶段 | opcode | amd64 | arm64 |
|---|---|---|---|
| PJ10a 直线 | MOVE / LOADK / LOADBOOL / LOADNIL / GETUPVAL / SETUPVAL / RETURN | ✅ | ✅ |
| PJ10b 算术 | ADD / SUB / MUL / DIV(reg-reg / reg-K / K-reg / K-K numeric) | ✅ inline SSE | ✅ shim |
| PJ10b 算术(慢) | MOD / POW / UNM / LEN / CONCAT | ✅ shim | ✅ shim |
| PJ10b 一元 | NOT | ✅ inline cmp | ✅ inline cmp |
| PJ10c 比较 | EQ / LT / LE(diamond) | ✅ inline UCOMISd(LT/LE)| ✅ shim |
| PJ10c 短路 | TEST / TESTSET / JMP | ✅ inline | ✅ shim |
| PJ10c 数值循环 | FORPREP / FORLOOP | ✅ inline | ✅ shim |
| PJ10c 泛型循环 | TFORLOOP | ✅ shim + saveGoG | ✅ shim |
| PJ10d 表 / 全局 | GETTABLE / SETTABLE / GETGLOBAL / SETGLOBAL / NEWTABLE / SETLIST / SELF | ✅ shim | ✅ shim |
| PJ10d 函数调用 | CALL / TAILCALL(gibbous dispatch tail-call)| ✅ shim + saveGoG | ✅ shim |
| PJ10d 闭包 | CLOSURE(读 SubNUps 跳 pseudo)/ CLOSE | ✅ shim | ✅ shim |

**35/38 opcode emit 函数双 arch 全覆盖**(shim 脚手架层面:VARARG 按 F1 设计永不接;JMP / TEST / LOADBOOL C!=0 已通过 CFG 多 BB + label resolver 接住)。

**注意 arch 分歧**(2026-07-02 勘误):上表统计的是「PJ10d shim 脚手架层的 emit 函数存在与否」,不是「native 接受门允许 inline 到 mmap 段的形式」。真正的 native 接受门(`AnalyzeNative` + `opSupported`)在两 arch 上不一样,详 §14.3:

- **amd64 native 接受门 = 26 op**(经 exit-reason 协议 + inline 混合覆盖,含 GETTABLE/SETTABLE/NEWTABLE/GETGLOBAL/SETGLOBAL/CALL/RETURN/GETUPVAL/SETUPVAL 等热 op);
- **arm64 native 接受门 = 18 op 线性子集**(算术 / 比较进一步在 `AnalyzeNative` 里被拒,不含 table / call / global);exit-reason 协议的 arm64 端口未做,详见 issues #37 和 #40(bench run 28575495631 中 arm64 P4 HeavyArith 慢约 20x 等回归)。

---

### 14.3 native 接受门:两级 gate + amd64 26 op / arm64 18 op 分岔(2026-07-02 重写)

Native path 的 `SupportsAllOpcodes` 走两层门:

**第一层:`opSupported`(编译期允许 emit)** — 承 arch 的物理约束(`peroptranslator/translator_native.go` amd64,`translator_native_arm64.go` arm64)。

**amd64 opSupported = 26 op**(2026-07-02 状态):

```
MOVE  LOADK  LOADBOOL  LOADNIL
ADD  SUB  MUL  DIV                 (inline SSE + shim slow path)
NOT  UNM
EQ  LT  LE
TEST  TESTSET
JMP  FORPREP  FORLOOP
GETTABLE  SETTABLE  NEWTABLE
GETUPVAL  SETUPVAL
GETGLOBAL  SETGLOBAL
CALL  RETURN
```

其中 MOVE/LOADK/LOADBOOL/LOADNIL/算术/NOT/UNM/EQ/LT/LE/TEST/TESTSET/JMP/FORPREP/FORLOOP/RETURN 走 **inline emit**(段内自跑,无跨界);GETTABLE/SETTABLE/NEWTABLE/GETUPVAL/SETUPVAL/GETGLOBAL/SETGLOBAL/CALL 及部分算术 slow path 走 **exit-reason 协议**(下文详)。

**被 opSupported 排除的 op**(仍走 head-op 回放 fallback,不进 native mmap 段):LEN / CONCAT / SELF / SETLIST / TAILCALL / CLOSURE / CLOSE / TFORLOOP / MOD / POW / VARARG。这些的 emit 函数在 PJ10d 脚手架里都存在(§14.2),只是不被 native path 接受。

**arm64 opSupported** 仍是原 18 op 线性子集,并且 `AnalyzeNative` 里对算术 / 比较 op 额外再拒(留待 issues #37 / #40);GETTABLE/SETTABLE/CALL/GETGLOBAL/SETGLOBAL/NEWTABLE 一律不接。

**第二层:`AnalyzeNative` 语义门**(amd64 + arm64 共享,arm64 再加 arith/compare 额外拒):

- 字符串常量 LOADK 拒(需要 arena 相对烘焙,留 followup);
- `IsVararg` 拒;
- 不可约简 CFG 拒;
- `GETTABLE` / `SETTABLE` 要求 IC slot Kind 为 `ArrayHit` 或 `NodeHit`(否则退 head-op);
- `GETGLOBAL` / `SETGLOBAL` 要求 IC slot Kind 为 `NodeHit`(注释里记录了从「无门直接接受」造成的 ~14% Transform CallInto 回归教训);
- `NEWTABLE` B/C ≥ 256 拒(超出编译期烧立即数范围);
- `CALL` B == 0 或 C == 0 拒(vararg 窗口 / to-top);
- **CALL 密度门**:`totalOps / callCount ≥ 16`,否则拒(注释里记录 fib 解释器 11 ms vs native 18 ms 的实测起因——CALL 占比过高时,exit-reason 往返成本压过 inline 收益);
- `returnCount == 0` 拒(全无 RETURN 出口的 Proto)。

**Multi-return 分流**:`codeBufProto.MultiReturn` 为 true 时,每条 RETURN 都被 lower 成 `HelperReturn` exit-reason(由 Go dispatcher 完整跑弹帧 + 多值回填);single-return 走 xor-eax 快出口 + Go 端 `DoReturn`。

**为什么两级门 = 硬约束**:mmap 段是 Go runtime 未登记的 code page,`morestack` prologue 走进这段无法从 `_func` 表回溯栈帧——并发 + 嵌套负载下 Go stack unwinder 撞死。这是 Go runtime 层面对 unregistered code page 的物理限制,不是 emit 顺序问题,只能在 emit 侧 fork 出「安全可 inline」子集,冷路径退 Go 端 dispatch。详见 [[2026-07-01-p4-pj10-native-round]] 教训 1。

**saveGoG shim tail 仍存在**:LEN/CONCAT/SELF/TAILCALL/CLOSURE/CLOSE/TFORLOOP/MOD/POW 以及 compare shim tails 走原有 `saveGoG` 协议把 G/vsBase 交还 Go 端调 helper——这条路径与 exit-reason 协议并存,分别覆盖「编译期就拒 inline」和「运行期从段内暂停回 Go」两种场景。

#### 14.3.1 exit-reason 协议(amd64,2026-07-02 新增)

针对 amd64 那 9 个「编译期允许接受,但 emit 里不能直接跑纯 inline」的 op(GETTABLE / SETTABLE / NEWTABLE / GETUPVAL / SETUPVAL / GETGLOBAL / SETGLOBAL / CALL / 部分算术 slow),native 段发出如下序列(`emit_ops_amd64.go::emitExitReason`):

1. 把 `(helperCode | a<<16 | b<<24 | c<<33 | pc<<42)` 打包一个 u64 写进 `jitCtx.exitArg0`;
2. 写入 `resumeOff` 占位(下一条 op 的 emit 起点会由 `emitResumePreludeIfPending` 回填;prelude 里同时重装 `rbx = vsBase`,因为 dispatcher 返回后 rbx 可能已经被 Go 端改动);
3. 把 `RAX = ExitInlineHelper` 后 `RET` 出段;
4. Go 端 `nativeCode.Run` dispatcher 读 `exitArg0` 分派到具体 helper(GetTable / SetTable / NewTable / GetUpval / SetUpval / CallBaseline / DoReturn 等),完成后经 `resumeOff` 二次 `callJITSpec` 重入段内继续。

**GETGLOBAL / SETGLOBAL 的 18-bit Bx 编码**:通过把 `Bx` 低 9 位放进 b 字段、高 9 位放进 c 字段拼回来,dispatcher 侧还原。

**Inline fast path 的 guard 简化**(2026-07-02 关键决策,详 [03-speculation-ic §2.3 addendum](./03-speculation-ic.md)):

- GETTABLE / SETTABLE ArrayHit inline 路径**完全去掉了 TableRef + gen identity guards**——ArrayHit inline 直接读当下的 asize / arrayRef,对任何 table 都正确(非 nil 的 array 槽读永不查 __index,非 nil-over-non-nil 的 store 永不 rehash;IC snapshot 只是 gate 「哪个 pc 位点允许 emit inline」,不参与运行期身份匹配);
- GETGLOBAL / SETGLOBAL NodeHit inline 用 gen-only guard + 编译期烧入的 node index(`GlobalsTaddr` 从 `host.GlobalsRaw` 取);「任何 key→slot 重定位都必须 BumpGen」是这条 inline 的承载不变量,已在 crescent `rawtable.go::insertNewKey` 上补齐(fuzz seed 4b3d10ff 回归)。

---

### 14.4 PreferNative 多 BB 收窄启发式

`PreferNative(*Proto) bool`(`translator_native.go`)是**入口判据**——决定 Compile 是走 shape-spec 快路径还是 native 路径。收窄条件:

1. `AnalyzeNative(proto)` 通过(opSupported gate + K operand 类型检查);
2. `buildCFG(proto).reachableBlocks()` **>= 2**(多 BB);
3. **至少一个 live BB 的 `endPC - startPC >= 4`**(有一段 ≥4 op 的直线块)。

**为什么不用 AnalyzeNative 直接进 native**:AnalyzeNative 能接住的形式是 shape-spec 也能接住的**超集**——其中「shape-spec 快路径能干」的部分 shape-spec 干得更快(spec 直接 head-op 回放不用 CFG / label resolve / codegen 开销)。Compile 入口把 shape-spec 的地盘抢过来是负收益。

`PreferNative` 精确匹配「shape-spec 天生打不着的形式」:shape-spec 的 FORLOOP-with-body 模板只 inline 1-2 op 的 reg-K body(见 `shapeInfo.hasBody` / `hasBody2`),3+ op body 的 FORLOOP 在 shape-spec 下回退 per-op 回放而 native 可以发全 inline SSE 主循环——**多 BB + big BB**  正是这条边界的精确指纹。

初版 `4b5abf8` 曾把入口判据放宽成「AnalyzeNative 就走 native」,立刻压掉 25 个 PJ3 / PJ5 / PJ7 已调优 shape-spec 测试;`0c9db3a` 收窄。详见 [[2026-07-01-p4-pj10-native-round]] 教训 3。

---

### 14.5 arch-aware label resolver(`fixupKind` enum)

多 arch label fixup 用 `fixupKind` enum(`codebuf.go`)分派——ISA 的分支编码是 platform physics,不能在 codegen 抽象层直接抹平:

| fixupKind | ISA | 编码 | 用途 |
|---|---|---|---|
| `fixupKindRel32Bytes` | amd64 | 签名 32-bit 字节位移 | 所有 amd64 前向 / 后向分支(`JMP rel32` / `Jcc rel32` 等) |
| `fixupKindArm64B26` | arm64 | rel26 word-scaled(<<2 后 ±128 MiB) | `B` 无条件分支 |
| `fixupKindArm64Cond` | arm64 | rel19 word-scaled(<<2 后 ±1 MiB) | `B.cond` / `CBNZ` 等条件分支 |

`codeBuf.addFixupKind(patchOff, srcInstrEnd, targetBB, kind)` 打补丁时按 kind 走对应位掩码 / scale。若从起点只支持 amd64 rel32,arm64 后补时要改 `codeBuf` 里每一个 label emit 点——反过来一次到位反而省事。详见 [[2026-07-01-p4-pj10-native-round]] 教训 5。

---

### 14.6 tail-call 走 gibbous dispatch(非 enterGibbous 重入)

TAILCALL 在 native 段完成 `SetTailcall` 复用父帧后,execute() 主循环的 TAILCALL case **在 `doTailCall` 返回正常 cci 之后**再检查 callee proto 是否有 GibbousCode:有则在 tail-call 帧上直接跑 `code.Run`,DoReturn 会按标准语义弹帧写返值。

**首版尝试**(commit `4b5abf8`)是把 dispatch 塞进 `doTailCall` 内部、返回 `(nil, nil)` 作 sentinel。这个方案两处出问题:(a) 破坏了 `doTailCall` 的返回契约(P3 wasm `TailCall` 直接把 `doTailCall` 的结果透传出去,不认 `(nil, nil)` sentinel);(b) 与 `SetTailcall` / `Fresh` flag / `funcIdx = parent.FuncIdx` 的 tail-call 帧 lifecycle 组合出双压帧。已 revert `a5bdb63`,并在 `0adaae6` 用「execute() 主循环内 dispatch」的对称写法真正修好——ci / proto / code 都是 switch 局部变量,gibbous 返回后自由重载即可。详见 [[2026-07-01-p4-pj10-native-round]] 教训 4(含证伪 → 二版正确修法闭环)。

---

### 14.7 V15b heavy 三本 P4 native > P3 wasm(2026-07-01 达标)

本轮性能验收:V15b heavy 三本 P4 native vs P3 wasm(10 iter × 3 samples,amd64):

| 脚本 | P4 native | P3 wasm | 加速比 |
|---|---|---|---|
| HeavyArith | 13.15 ms | ~86 ms | **6.5x** |
| HeavyRecursion | 5.37 ms | 12.66 ms | **2.4x** |
| HeavyFloatloop | 17.10 ms | 50.94 ms | **3.0x** |

**三本 P4 native 均 > P3 wasm 达标 ✓**(commit `0adaae6` proper tail-call gibbous dispatch 交付后)。

**V14 luajc 档无回归**(承 §14.1 回放骨架建立的 byte-equal),conformance / difftest / luasuite 逐字节一致。

---

### 14.8 arm64 端

`GOARCH=arm64 CGO_ENABLED=0 go build` 对 `wangshu_p4` / `wangshu_p4 wangshu_profile` 两 tag 组合都过。arm64 runtime e2e 留 CI followup——需 linux/arm64 host,QEMU 用户模式不真支持 mmap RWX→RX 翻面,darwin/arm64 通过 jitcgo forward 模式接。

---

### 14.9 剩余项(留 followup)

- arm64 runtime e2e 真机 CI(linux/arm64 self-hosted 或 darwin/arm64 M1 真机);
- 生产接线时 concurrent multi-goroutine force-all difftest 在 mmap trampoline RET 上仍有偶发崩(见 `peropcode.go::init` docstring),已把生产 wiring 保守回退到「回放骨架 + 显式调用 native 覆盖 e2e 测试」,native 完全消耗需解决 concurrent stack unwind race;
- 字符串常量 LOADK arena 相对烘焙(当前 AnalyzeNative 拒);
- V15b heavy 之外的真实宿主负载 profile;
- `gibbous_test_cfield_test.go` fixture 换非对称输入 + 显式路由断言,增强鉴别力(reviewer 指出对称树 `{{f,f},{f,f}}` 可能 A/C bug 都给一样的结果;实测 A-bug 会崩,仍有鉴别力,但值得显式化)。

