From 4b0414c6226ece9b6ce0add4e8a72941f0fa27fa Mon Sep 17 00:00:00 2001 From: yciu Date: Sun, 1 Nov 2026 18:58:52 +0100 Subject: [PATCH] feat(vm): support 16-bit register operands for large function frames --- AGENTS.md | 2 +- docs/VM2.md | 80 +++--- docs/VmBudget.md | 3 +- .../Hosting/ScriptCompileProfiles.cs | 5 +- .../Bytecode/BytecodeEncoder.ArrayTemplate.cs | 4 +- .../Bytecode/BytecodeEncoder.cs | 12 +- src/EasyCon.Script/Bytecode/BytecodeModule.cs | 2 +- .../Bytecode/EcsContainerFormat.cs | 26 +- src/EasyCon.Script/Bytecode/EcsFormat.cs | 14 +- src/EasyCon.Script/Bytecode/EcsOpcode.cs | 22 +- src/EasyCon.Script/Bytecode/EcxInterpreter.cs | 2 +- src/EasyCon.Script/Bytecode/EcxLinker.cs | 2 +- .../Bytecode/InstructionCodec.cs | 114 ++++---- src/EasyCon.Vm/native/ecs_vm.c | 81 ++++-- src/EasyCon.Vm/native/ecs_vm.h | 19 +- .../Bytecode/ArgumentWindowTests.cs | 10 +- .../Bytecode/ArrayTemplateTests.cs | 2 +- .../Bytecode/BytecodeSizeAnalysisTests.cs | 17 +- test/EasyCon.Tests/Bytecode/CvmP0Tests.cs | 19 +- .../Bytecode/InstructionFormatTests.cs | 2 +- .../EasyCon.Tests/Bytecode/PcWideSlotTests.cs | 40 ++- .../Bytecode/StreamInvariantTests.cs | 13 +- .../Bytecode/WideOperandTests.cs | 265 ++++++++++++++++++ test/EasyCon.Tests/Support/CvmRunner.cs | 8 +- 24 files changed, 570 insertions(+), 194 deletions(-) create mode 100644 test/EasyCon.Tests/Bytecode/WideOperandTests.cs diff --git a/AGENTS.md b/AGENTS.md index 25e0f90f..3188deeb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -93,7 +93,7 @@ Rules to respect when changing code: - **Never hand-write `CompileOptions` literals.** Use `ScriptCompileProfiles.Interactive` (compile-on-the-fly, process cache only) or `ScriptCompileProfiles.Distributable` (obj/ content-addressed cache; the image is MCU-distributable). All products share one - format (ECX1 flat + v3 fixed-width encoding); the 255-slot frame budget is shared by + format (ECX1 flat + v4 optional 16-bit operands); the 65535-slot frame budget is shared by params, outbound windows and live values. Device fit is a burn-time capacity precheck concern, not a profile. - DI *container* is deliberately absent: composition is explicit constructor passing from diff --git a/docs/VM2.md b/docs/VM2.md index cbff215c..2553a49c 100644 --- a/docs/VM2.md +++ b/docs/VM2.md @@ -119,9 +119,9 @@ ToStr ToInt`。 ## 3. 指令集 -### 3.1 编码格式(小端;定长指令,ECX1 平铺容器) +### 3.1 编码格式(小端;按需扩展操作数,ECX1 平铺容器) -| 格式 | 布局(v3 定长,即线格式) | 用于 | +| 格式 | 基础布局(操作码 bit7=0) | 用于 | |------|------|------| | iABC | `op:8 \| A:8 \| B:8 \| C:8`(4B) | 三地址运算、域操作 | | AsBx | `op:8 \| A:8 \| sBx:16`(4B) | LoadI(s16 立即数;超界编译期归一 LoadK)、Jpt/Jpf(s16 **字节**偏移) | @@ -130,21 +130,23 @@ ToStr ToInt`。 | Ext | iABC + 后随 `data:32`(8B) | Call/CallN 目标、Slice end 槽、GetFI/PutFI 索引槽、StickP/StickPv、NewArrV 元素类型码、**WaitI/KeyI 时长**(v3 起) | | IabcJ | iABC + 后随 `jump:32`(8B) | **ForStep/CmpJ** 第 4 操作数(跳转**字节**偏移;v3 起) | -> **单流化 + v3 定长(已实施)**:内存唯一表示为解码形态 `EcsInstruction`(完整 int32 操作数, -> Jump = 相对指令下标);文件的 **v3 定长指令流**(ECX1 `.text`)与内存按上表一一对应, -> 投影/抬升由 `InstructionCodec` 单处换算(线格式跳转 = 相对**字节**偏移:目标 = 指令起始 + -> 本指令字节数 + delta)。操作数位宽由格式锁定:槽位 u8 ≤254(寄存器 ≤255 帧长上限,无哨兵值—— -> Call/CallN 的 C 恒为接收寄存器号);AsBx=s16;ABx=Bx16(≤65535,常量池 16 位索引是 pinned 句柄编码前提); -> IsJ=s24。越界一律**编译期响亮失败**。 -> 权威表:C# `EcsFormat.BuildTable`(漏登自检抛出)↔ C `ecs_op_words`(字数 1=4B/2=8B)。 +**v4 操作数扩展**:操作码限低 7 位,bit7 为扩展标记。任一寄存器操作数超过 255 时, +在基础头字后插入 `[A_hi:u8, B_hi:u8, C_hi:u8, reserved:u8=0]`;低字节仍在基础头字中。 +iABC/Ext/IabcJ 扩展 A/B/C;ABx/AsBx 仅扩展 A,B_hi/C_hi 必须为 0;IsJ 不允许扩展。 +EXT 或跳转数据字位于扩展字之后。指令尺寸为 4/8/12B;小寄存器指令保持原有 4/8B 编码。 +不需要的扩展字、非零保留位、截断和寄存器号 65535 均被拒绝,保持编解码往返唯一。 -**数据字**:12 条带字指令(Ext 8 条 + ForStep/CmpJ/WaitI/KeyI)的数据字是「数据」——数值可能 -恰好等于某个操作码,一切线性扫描必须按 `EcsFormat.WordCount` 步进跳过(历史 F4 缺陷铁律, -v3 随定长布局回归)。 +内存唯一表示仍是 `EcsInstruction`(int32 操作数,Jump 为相对指令下标),桌面与 MCU +使用同一条可序列化指令流。`InstructionCodec` 一遍布局并换算跳转:目标 = 指令起始 + +本指令实际字节数 + 字节偏移。宽度只依赖操作数,不依赖跳转距离,无需定点迭代。 +AsBx=s16、ABx=u16、IsJ=s24,立即数和跳转范围不变,越界编译期报错。 -容量上限(R-3):槽位上限 254 已前移到**编译期**(v3 槽位 u8,与宿主档案 `ECS_MAX_SLOTS=255` -对齐);加载期校验保留(nslots ≤ 宿主档案)。Jpt/Jpf 偏移越界由编码器跳转反转模板消化, -超 s16 由 `Project` 响亮失败。 +`EcsFormat.WordCount` ↔ C `ecs_op_words` 只表示基础字数;线格式步进与行号表必须使用 +`InstructionCodec.InstructionSize`,C 侧加上扩展字的 4B。扩展字和后随数据字都不能当作操作码扫描。 + +容器 ECX1/ECM1 的 format 与 ABI 修订号均升为 2;旧镜像需重新编译,旧缓存自动失效, +原生宿主或运行 ECS 字节码的固件必须同时更新 VM。函数表 `nslots:u16` 支持最多 65535 槽 +(R0–R65534),实际可运行帧大小由宿主帧段内存限制,容量不足返回 `ECS_ERR_POOL`。 ### 3.2 指令表 @@ -171,8 +173,8 @@ Not a, b;NegI NegD a, b;Conv a, b, kind(§2.4) Jmp s24;Jpt a, s16;Jpf a, s16 真值 = i32 != 0(S-06) # 调用(EXT) -Call a, n, c, fid:32 调用镜像函数 fid;实参 R[a..a+n) 深拷贝进新帧槽 0..n-1(S-17); - c=255 表无接收槽;返回值深拷贝到 R[c] +Call a, n, c, fid:32 调用镜像函数 fid;实参 R[a..a+n) 深拷贝进新帧槽 1..n(S-17); + c=0 为 RET 固定落点;返回值深拷贝到 R[c] CallN a, n, c, target:32 target bit31=1 → L2 syscall 编号;否则 L3 名表索引(§8) Ret a / Ret0 返回(深拷贝交接)/ 返回 VOID @@ -206,7 +208,7 @@ LCLICK=11 RCLICK=12 HOME=13 CAPTURE=14`;HAT `TOP=16..TOP_LEFT=23`;摇杆 `LS ### 3.3 寄存器与槽位模型(每函数栈窗口) **帧模型**(Dalvik/Lua 式):帧 = 函数自身的寄存器窗口,帧长 = 函数寄存器高水位(`EcsFunction.NSlots` -≤ 255,指令 A/B/C 域 u8 是唯一上限——项目代码量增长不牵动寄存器堆)。调用时在共享值栈上按被调方 +≤ 65535,寄存器操作数按需扩展到 u16)。调用时在共享值栈上按被调方 帧长开窗、返回时弹出。帧内布局: ```text @@ -214,7 +216,7 @@ R0 RET:返回值固定落点(Call 的 C=0,无显式 R1 .. NParams ARG 参数窗:仅占用函数声明的参数数(S-17 深拷贝) NParams+1 起 STAGING:按本函数最大出站调用/数组分块需求保留 STAGING 之后 分配区:独占槽(φ/ForStep/循环携带值/编码器临时)→ 池区 -frameLen ≤ 255 帧长 = NSlots = 发射结束后池区高水位 +frameLen ≤ 65535 帧长 = NSlots = 发射结束后池区高水位 ``` - **生命周期分配**(`AssignSlots`,参考线性扫描寄存器分配):非常量值统一池化——定义发射 @@ -225,8 +227,8 @@ frameLen ≤ 255 帧长 = NSlots = 发射结束后池区高水位 (`CollectLoopCarriedValues`:定义块不在其读取所在自然循环体内——线性扫描对回边活性的盲区, 素数筛语料实证)、编码器临时(Neq/scratch/模板守卫组)。φ 臂合并(下 P1b)后臂须退出池化 结算,否则会把 φ 独占槽错放回空闲池。 -- **参数数没有独立的 7 项上限**:声明上限为 `MaxRegisters - ArgBase = 254`,实际编译还须让 - 参数、出站窗口、存活值和临时值一起满足 255 槽帧预算;预算不足报寄存器溢出。无出站调用或 +- **参数数**:声明上限仍为 `ArgCount = 254`(模块签名中的参数计数为 u8,与帧槽数独立), + 参数、出站窗口、存活值和临时值共享 65535 槽帧预算;预算不足报寄存器溢出。无出站调用或 数组构建的函数不保留 STAGING。零参数调用的 A 取 R0,不读取实参。 - **STAGING 与参数/分配区物理隔离**:出站实参 Move 不会覆写存活值,嵌套调用的结果先放在 分配区再搬入出站窗口。链接期内联将壳体的整个窗口搬到调用者未使用的区间,先从调用点 A @@ -251,7 +253,7 @@ frameLen ≤ 255 帧长 = NSlots = 发射结束后池区高水位 实参连续暂存调用者出站窗口 `R[a..a+n)`(A 随调用点编码,位置见 §3.3),`Call` 深拷贝播种进 被调方参数窗 `R[1..1+n)`(S-17);接收槽 C 恒为寄存器号(无哨兵值),返回值深拷贝交接 -(C=0 即 RET 落点)。实参无独立固定上限:编码上限 254,实际与出站窗口、存活值共享 255 槽帧 +(C=0 即 RET 落点)。实参个数上限为 254,与出站窗口、存活值共享 65535 槽帧 预算(§3.3;绑定层声明校验 + 编码器预算校验)。递归天然支持;深度上限 `ECS_MAX_CALL_DEPTH`(64,双端对齐) → `ECS_ERR_DEPTH`;尾递归由 SSA 优化器(TRE)变循环回边,VM 无 tail-call 指令。 @@ -282,21 +284,29 @@ Link 在深拷贝副本上进行,不污染调用方产物(`.ecm` 可安全 r ```text 偏移 字段 -0 magic "ECX2"(4 字节) +0 magic "ECX1"(4 字节) 4 format_ver:u16 = 2 -6 flags:u16 bit0 D=调试名区存在;bit1 K=KeyAction;bit2 I=NeedIL;其余 0 -8 max_slots:u8 -9 max_depth:u8 -10 feats:u16 特征需求掩码(EcsImageFeatures:IL/CAPTURE/FFI/FILE/VISION) -12 const_count:u32 × 常量条目(tag:u8 + payload;STRING = units:u16 + UTF-16LE) +6 abi_rev:u16 = 2 +8 feats:u16 特征需求掩码 +10 entry:u16 +12 flags:u8 bit0 D=调试块存在;bit1 K=KeyAction;bit2 I=NeedIL +13 module_name_length:u8 +14 func_count:u16 +16 code_size:u32(字节) +20 debug_size:u32(字节) +24 max_literal_string_units:u16 +26 max_literal_array_elems:u16 +28 max_struct_slots:u16 +30 reserved:u16 = 0 +32 crc32:u32(清零本字段后计算全文件 CRC) +36 主模块名 UTF-8(长度由头部给出) +? const_count:u32 × 常量条目(tag:u8 + payload;STRING = units:u16 + UTF-16LE) ? struct_count:u32 × StructDef(name:utf8, nfields:u8, Field{name,kind,base,elem,count:u16}) ? global_count:u32 × Global(module:u8 + type:u8 定长;名字在调试区) ? native_count:u32 × Native(name:utf8) -? func_count:u32 × FuncDef(nparams:u8, nslots:u8, hasret:u8, code_off:u32, code_words:u32) -? 代码区(u32 指令字序列,4 字节对齐) -? 调试区(D=1 时:函数名表 + 全局名表,debug_count = func_count + global_count; - stripDebug 产 MCU 发布版时整区不存在,debug_count=0) -末 entry:u32 +? func_count:u32 × FuncDef(nslots:u16, code_off:u32;偏移相对代码区) +? 代码区(v4 指令流,每条指令占 1–3 个 4B 字;不要求文件绝对偏移对齐) +? 调试区(D=1 时:[lines_len:u32][lines][names_len:u32][names];stripDebug 时省略) ``` 原生名表只承载 L3(采集洞/FFI/std 加速洞 `__ENCODE__/__JQ__/__CTC_FOLD__`);L2 syscall 编号直传 EXT,不占名表 @@ -464,7 +474,7 @@ void ecs_vm_ret_str(ecs_vm *vm, ecs_value *ret, const uint16_t *units, int32_ | 项 | 预算 | |----|------| | ECX 镜像 | flash/XIP(光速过帧 540 B,含调试区;stripDebug 更小) | -| 指令流 | **零 RAM**:v3 定长 4B/8B,镜像 XIP 原地取指(无解码缓存、无解码段) | +| 指令流 | **零 RAM**:v4 按需扩展 4B/8B/12B,镜像 XIP 原地取指(无解码缓存、无解码段) | | 帧区 | `max_depth × max_slots × 16B`(光速过帧 608 B) | | 堆 | 句柄表 + 对象,池大小可配(4–64 KB) | | VM 本体 | ecs_vm.c ~2200 行 C99 单翻译单元,`cc -O2` 一条命令构建 | @@ -497,4 +507,4 @@ void ecs_vm_ret_str(ecs_vm *vm, ecs_value *ret, const uint16_t *units, int32_ → ECX
:Call → ... Call PRINT → PRINT 体内 CallN 0x80000001 执行 桌面:EcxHost.Syscall(1) → 行断协议 → Print 委托 → 控制台 MCU :h_syscall(1) → --print 通道开则输出;恒返回 len -``` \ No newline at end of file +``` diff --git a/docs/VmBudget.md b/docs/VmBudget.md index f681ef7e..85b6653a 100644 --- a/docs/VmBudget.md +++ b/docs/VmBudget.md @@ -38,7 +38,8 @@ P0 基于上游 dev `78d294b61fcc0d7e4059c2a6f76d20c6d4e62db5`。 预算档只有 3 个同时存活的对象块,不能承诺任意桌面脚本都能运行。 帧表存 arena 偏移;帧头尺寸仍与目标指针宽度有关。配置必须满足 8B 对齐和块容量约束,编译期检查。 -255 个槽是字节码格式上限。384B 帧段无法同时容纳 255 个 8B 值槽;设备适配仍需容量预检。 +当前 v4 编码支持 65535 个帧槽(ECX1 format=2、ABI=2,见 VM2.md §3.1);原有值模型迁移与 +该格式升级是两项独立变更。384B 帧段仍无法容纳大帧;设备适配必须按实际内存做容量预检。 `ECS_ERR_POOL` 是正常的资源性拒绝,可与无池的 C# 参考解释器发生资源性差异;错误现场必须可定位。 ## 4. WAIT 续跑 diff --git a/src/EasyCon.Core/Hosting/ScriptCompileProfiles.cs b/src/EasyCon.Core/Hosting/ScriptCompileProfiles.cs index 59469315..ceb886fb 100644 --- a/src/EasyCon.Core/Hosting/ScriptCompileProfiles.cs +++ b/src/EasyCon.Core/Hosting/ScriptCompileProfiles.cs @@ -9,9 +9,8 @@ namespace EasyCon.Core.Hosting; /// 避免 GUI / CLI 各自手写 字面量而悄悄漂移。 /// /// -/// 单流化后(docs/VM2.md §3.1):所有产物都是同一形态(ECX1 平铺 + v3 定长指令编码), -/// **没有宽窄之分**——原 Desktop/Portable 的格式维度随 EnablePcWideSlots -/// 一起删除。当前定长指令编码的函数帧预算为 255 槽;参数与出站窗口按需分配, +/// 所有产物都是同一形态(ECX1 平铺 + v4 按需扩展操作数编码), +/// 不按桌面/MCU 划分格式。函数帧最多 65535 槽;参数与出站窗口按需分配, /// 桌面与 MCU 共用该预算及同一条可序列化指令流。 /// 「能不能装进设备」是资源问题,由烧录前的容量预检判定(docs/McuBytecodeDelivery.md,计划中), /// 不再由编译档位表达。剩余的两档只是**缓存策略**(与产物内容无关,不进缓存键): diff --git a/src/EasyCon.Script/Bytecode/BytecodeEncoder.ArrayTemplate.cs b/src/EasyCon.Script/Bytecode/BytecodeEncoder.ArrayTemplate.cs index 808f636b..cca7d236 100644 --- a/src/EasyCon.Script/Bytecode/BytecodeEncoder.ArrayTemplate.cs +++ b/src/EasyCon.Script/Bytecode/BytecodeEncoder.ArrayTemplate.cs @@ -14,7 +14,7 @@ public static partial class BytecodeEncoder /// public const int TemplateArrayMinElements = 8; - /// 常量模板构建的分块元素数(NewArrV 的 C:8 ≤ 255、staging 槽预算内取值)。 + /// 常量模板构建的分块元素数(控制 staging 占用及 MCU 单对象容量)。 public const int TemplateChunkSize = 128; sealed partial class Encoder @@ -23,7 +23,7 @@ sealed partial class Encoder // // 数组字面量只能以立即数列表初始化,元素编译期完全可见——大数据表(如万级颜色/字库表) // 本质是常量数据。内联路径却把每个元素当独立 SSA 常量物化进帧槽(块首全量同时存活)+ - // staging 槽随元素数线性增长,单函数槽位随字面量爆炸(1 万元素 ≈ 2 万槽 > 255 的 ISA 上限)。 + // staging 槽随元素数线性增长;分块避免大字面量占用过多帧槽。 // // 降级方案(C 静态初始化模板 + 守卫的同型设计 / Go static composite literal init): // - 字面量内容以首个初始化点为准构建一次,存入模块级隐藏全局模板(内容键去重); diff --git a/src/EasyCon.Script/Bytecode/BytecodeEncoder.cs b/src/EasyCon.Script/Bytecode/BytecodeEncoder.cs index 28fdcd93..e0cee134 100644 --- a/src/EasyCon.Script/Bytecode/BytecodeEncoder.cs +++ b/src/EasyCon.Script/Bytecode/BytecodeEncoder.cs @@ -284,7 +284,7 @@ void EmitAsBx(EcsOpcode op, int a, int sbx) if (EcsFormat.Get(op) != EcsInsFormat.AsBx) throw Fail($"{op} 登记格式为 {EcsFormat.Get(op)},不能按 AsBx 发射(与 EcsFormat 表不一致)"); if (sbx < short.MinValue || sbx > short.MaxValue) - throw Fail($"{op} 的 sBx 越界: {sbx}(v3 定长 AsBx=s16)"); + throw Fail($"{op} 的 sBx 越界: {sbx}(AsBx=s16)"); Emit(new EcsInstruction(op, a, sbx)); } @@ -297,18 +297,18 @@ void EmitExt(EcsOpcode op, int a, int b, int c, uint ext) Emit(new EcsInstruction(op, a, b, c, ext)); } - /// Iabc 头部操作数字节域守卫(v3 定长):槽位 u8 ≤254(255 = 无接收哨兵); + /// Iabc 操作数守卫:寄存器可扩展到 u16; /// StickSet/StickP 的 B/C 是摇杆坐标(满 u8 0..255),不按槽位收紧。 static void CheckIabcOperands(EcsOpcode op, int a, int b, int c) { - if (a is < 0 or > 254) ThrowOp(op, $"A 槽位越界: {a}"); + if (a < 0 || a >= EcsRegisters.MaxRegisters) ThrowOp(op, $"A 槽位越界: {a}"); bool coord = op is EcsOpcode.StickSet or EcsOpcode.StickP; - if (b < 0 || b > (coord ? 255 : 255)) ThrowOp(op, $"B 越界: {b}"); - if (c < -1 || c > (coord ? 255 : 254)) ThrowOp(op, $"C 越界: {c}"); + if (b < 0 || b > (coord ? byte.MaxValue : EcsRegisters.MaxRegisters - 1)) ThrowOp(op, $"B 越界: {b}"); + if (c < 0 || c > (coord ? byte.MaxValue : EcsRegisters.MaxRegisters - 1)) ThrowOp(op, $"C 越界: {c}"); } static void ThrowOp(EcsOpcode op, string what) - => throw new BytecodeException(new[] { new BytecodeDiagnostic($"{op} {what}(v3 定长编码)", null, 0) }); + => throw new BytecodeException(new[] { new BytecodeDiagnostic($"{op} {what}(v4 操作数扩展编码)", null, 0) }); BytecodeException Fail(string message) => new(new[] { new BytecodeDiagnostic(message, _symbol.Name, _code.Count) }); diff --git a/src/EasyCon.Script/Bytecode/BytecodeModule.cs b/src/EasyCon.Script/Bytecode/BytecodeModule.cs index b903f371..40771bd0 100644 --- a/src/EasyCon.Script/Bytecode/BytecodeModule.cs +++ b/src/EasyCon.Script/Bytecode/BytecodeModule.cs @@ -62,7 +62,7 @@ public sealed class EcsGlobal /// /// 唯一指令表示(解码形态,docs/SingleStreamFormat.md §2):完整宽度操作数,形态与线格式无关。 -/// 线格式的 iABC/ABx/AsBx/IsJ 打包与 EXT 数据字只是 v3 定长序列化投影(), +/// 线格式的 iABC/ABx/AsBx/IsJ 打包、操作数扩展与 EXT 数据字只是 v4 序列化投影(), /// 内存中不存在第二套字节码。 /// Jump 仅对 Jmp/Jpt/Jpf 有效,语义 = 相对**指令下标**偏移(前插平移不变; /// 线格式投影时由 InstructionCodec 换算为相对字节偏移——内存解释器按下标取指,字节偏移无处落点)。 diff --git a/src/EasyCon.Script/Bytecode/EcsContainerFormat.cs b/src/EasyCon.Script/Bytecode/EcsContainerFormat.cs index 13d3dffa..eaa25e0b 100644 --- a/src/EasyCon.Script/Bytecode/EcsContainerFormat.cs +++ b/src/EasyCon.Script/Bytecode/EcsContainerFormat.cs @@ -7,16 +7,16 @@ namespace EasyCon.Script.Bytecode; /// 产物容器(v2.3 零期平铺化): /// .ecx 镜像 = ECX1 平铺(),.ecm 模块缓存 = ECM1 平铺 /// ()。同一纪律:定长 36B 头 + 自计数表依序排列 + 头内全量 CRC; -/// 无逐段框架——格式演进 = 版本断代 + 全量重编(旧产物经魔数不符自然失效)。 -/// 指令流经 投影为 v3 定长字节流。 +/// 无逐段框架——格式演进 = 版本断代 + 全量重编(旧产物经版本/ABI 校验失效)。 +/// 指令流经 投影为 v4 按需扩展字节流。 /// 容量上限不是格式字段;产物只携带关于自身静态极值的事实(ZeroAllocVm.md §4.1)。 /// public static class EcsContainer { public const uint FlatMagic = 0x31584345; // "ECX1"(平铺镜像容器) - public const ushort FlatFormat = 1; + public const ushort FlatFormat = 2; public const uint ModuleFlatMagic = 0x314D4345; // "ECM1"(平铺模块缓存容器) - public const ushort ModuleFlatFormat = 1; + public const ushort ModuleFlatFormat = 2; public const byte HeaderFlagDebug = 0x01; // ECX1 flags:bit0=调试块在(ECM1 flags 见 WriteModule) @@ -32,6 +32,7 @@ public static byte[] WriteImage(EcxImage image, bool stripDebug = false, bool wi var codeBuf = new List(); for (int i = 0; i < image.Functions.Count; i++) { + CheckFrameSize(image.Functions[i]); codeOffsets[i] = (uint)codeBuf.Count; codeBuf.AddRange(InstructionCodec.Project(image.Functions[i].Instructions)); } @@ -138,6 +139,7 @@ public static byte[] WriteModule(ModuleArtifact module) var codeBuf = new List(); for (int i = 0; i < module.Functions.Count; i++) { + CheckFrameSize(module.Functions[i]); codeOffsets[i] = (uint)codeBuf.Count; codeBuf.AddRange(InstructionCodec.Project(module.Functions[i].Instructions)); } @@ -264,7 +266,13 @@ public static byte[] WriteModule(ModuleArtifact module) return bytes; } - /// SEC_LINES 投影所需的指令字节布局(v3 定长:尺寸只依赖操作码,一遍即得)。 + static void CheckFrameSize(EcsFunction function) + { + if (function.NSlots < 1 || function.NSlots > EcsRegisters.MaxRegisters) + throw Err($"函数 {function.Name} 的帧槽数越界: {function.NSlots}", function.Name); + } + + /// SEC_LINES 投影所需的指令字节布局(包含按需扩展字,一遍即得)。 static int[] InstructionLayout(IReadOnlyList instructions) { int n = instructions.Count; @@ -273,7 +281,7 @@ static int[] InstructionLayout(IReadOnlyList instructions) for (int i = 0; i < n; i++) { offsets[i] = off; - off += InstructionCodec.SizeOf(instructions[i].Op); + off += InstructionCodec.InstructionSize(instructions[i]); } offsets[n] = off; return offsets; @@ -313,6 +321,8 @@ public static EcxImage ReadImage(byte[] bytes) if (format != FlatFormat) throw Err($"ECX1 format {format} 不受支持", null); int abi = BitConverter.ToUInt16(bytes, 6); + if (abi != EcsSyscall.AbiRevision) + throw Err($"ECX1 ABI {abi} 不受支持,请重新编译", null); ushort feats = BitConverter.ToUInt16(bytes, 8); int entry = BitConverter.ToUInt16(bytes, 10); byte flagsB = bytes[12]; @@ -422,7 +432,7 @@ public static EcxImage ReadImage(byte[] bytes) foreach (var ins in functions[fi].Instructions) { startsList.Add(acc); - acc += InstructionCodec.SizeOf(ins.Op); + acc += InstructionCodec.InstructionSize(ins); } var table = new List(pairs * 2); for (int k = 0; k < pairs; k++) @@ -473,6 +483,8 @@ public static ModuleArtifact ReadModule(byte[] bytes) int format = BitConverter.ToUInt16(bytes, 4); if (format != ModuleFlatFormat) throw Err($"ECM1 format {format} 不受支持", null); + if (BitConverter.ToUInt16(bytes, 6) != EcsSyscall.AbiRevision) + throw Err("ECM1 ABI 不受支持,请重新编译", null); // CRC 校验(字段清零后全量) var savedCrc = BitConverter.ToUInt32(bytes, 32); diff --git a/src/EasyCon.Script/Bytecode/EcsFormat.cs b/src/EasyCon.Script/Bytecode/EcsFormat.cs index c605e251..0a6ef5e8 100644 --- a/src/EasyCon.Script/Bytecode/EcsFormat.cs +++ b/src/EasyCon.Script/Bytecode/EcsFormat.cs @@ -1,6 +1,6 @@ namespace EasyCon.Script.Bytecode; -/// 指令编码格式(docs/VM2.md §4.1;v3 定长编码——线格式与内存布局一一对应)。 +/// 指令基础编码格式(docs/VM2.md §3.1;可另带 v4 操作数扩展字)。 public enum EcsInsFormat : byte { /// op:8 | A:8 | B:8 | C:8(4 字节) @@ -50,7 +50,7 @@ public enum EcsResultSlot : byte None = 0, /// 写 R[A](取值/运算/比较/构造类,含 SetVar 的深拷贝写)。 A, - /// 写 R[C](Call/CallN 的接收槽,C=255 表示无接收)。 + /// 写 R[C](Call/CallN 的接收槽,C=0 为 RET 固定落点)。 C, } @@ -60,8 +60,8 @@ public enum EcsResultSlot : byte /// 步进、InstructionScanner)、反汇编(EcxDisassembler)、体积分析(BytecodeSizeAnalysisTests) /// 与 C VM 的 ecs_op_words 表(ecs_vm.h,紧邻 OP_ 枚举)均以本表为对齐基准。 /// 关键不变量:EXT 后随字是「数据」,其数值可能恰好等于某个操作码(如元素类型码 3 == LoadK), -/// 一切线性扫描必须按 ExtWords 步进跳过,否则把数据误读为指令(轻则静默改坏 Slice 槽位/ -/// StickPv 时长,重则越界崩溃——历史 F4 缺陷即此类)。 +/// 线格式扫描必须按 InstructionCodec.InstructionSize 跳过操作数扩展字和数据字, +/// 防止将数据误读为指令(历史 F4 缺陷)。解码形态始终每个 EcsInstruction 一条指令。 /// 新增指令:EcsOpcode 枚举加值 + 本表加一行(漏登会在 BuildTable 完整性自检抛出)+ /// 双端执行/发射语义 + C 侧表同步;全部扫描/步进/反汇编消费点自动正确。 /// @@ -74,10 +74,10 @@ public static class EcsFormat /// 结果槽写集(执行语义的编码侧投影,见 EcsResultSlot)。 public static EcsResultSlot ResultSlot(EcsOpcode op) => Table[(int)op].Result; - /// 指令总字数(4 字节字为单位;Ext/IabcJ=2,其余=1)。 + /// 基础字数(Ext/IabcJ=2,其余=1);实际线格式尺寸见 InstructionCodec.InstructionSize。 public static int WordCount(EcsOpcode op) => Get(op) is EcsInsFormat.Ext or EcsInsFormat.IabcJ ? 2 : 1; - /// 后随 32 位数据字个数(4 字节指令为 0)。定长线性扫描的唯一步进依据; + /// 后随 32 位数据字个数(不计操作数扩展字); /// IabcJ 的数据字 = 跳转偏移(ForStep 出口 / CmpJ 跳转),同样是「数据不可误读为指令」。 public static int ExtWords(EcsOpcode op) => WordCount(op) - 1; @@ -96,6 +96,8 @@ static InsInfo[] BuildTable() int registered = 0; void F(EcsOpcode op, EcsInsFormat format, EcsResultSlot result = EcsResultSlot.A, EcsExtKind ext = EcsExtKind.None) { + if ((byte)op >= InstructionCodec.WideFlag) + throw new InvalidOperationException("操作码 bit7 保留给操作数扩展标记"); t[(int)op] = new InsInfo { Format = format, Ext = ext, Result = result }; registered++; } diff --git a/src/EasyCon.Script/Bytecode/EcsOpcode.cs b/src/EasyCon.Script/Bytecode/EcsOpcode.cs index 9679aedf..bdb3c5aa 100644 --- a/src/EasyCon.Script/Bytecode/EcsOpcode.cs +++ b/src/EasyCon.Script/Bytecode/EcsOpcode.cs @@ -5,11 +5,11 @@ namespace EasyCon.Script.Bytecode; /// /// VM2 指令集操作码。与 src/EasyCon.Vm/native/ecs_vm.h 中的枚举一一对应。 -/// 编码格式(见 docs/VM2.md §2.1): +/// 基础编码格式(见 docs/VM2.md §3.1;bit7 置位时头字后另有操作数高位字): /// iABC : op:8 | A:8 | B:8 | C:8 /// AsBx : op:8 | A:8 | sBx:16(有符号) /// ABx : op:8 | A:8 | Bx:16(无符号) -/// IsJ : op:8 | s24:24(JMP 专用,单位=指令字) +/// IsJ : op:8 | s24:24(JMP 专用,单位=字节) /// EXT : iABC + 后随 32 位字(CALL/CALLN 的目标 ID、SLICE 的 end 槽) /// public enum EcsOpcode : byte @@ -56,13 +56,13 @@ public enum EcsOpcode : byte Jpf, // 同 Jpt,假值跳转 // ---- 调用(EXT:后随 32 位目标 ID)---- - Call, // iABC + ext32:A=首参槽,B=参数个数,C=接收槽(255=无返回值);实参取 R[A..A+B),深拷贝进新帧槽 0..B-1;返回值按 RET 语义复制到 R[C] + Call, // iABC + ext32:A=首参槽,B=参数个数,C=接收槽(0=RET);实参取 R[A..A+B),深拷贝进新帧槽 1..B;返回值按 RET 语义复制到 R[C] CallN, // 同 Call,目标=原生函数 ID(镜像 native 名表) Ret, // iABC: 返回 R[A] Ret0, // 返回 VOID // ---- 数组 / 字符串 ---- - NewArrV, // iABC + ext32: R[A] = 数组字面量,元素取 R[B..B+C),ext=元素类型码 + NewArrV, // iABC + ext32: R[A] = 数组字面量,元素取 R[C..C+B),ext=元素类型码 NewArrE, // ABx : R[A] = 空数组,元素类型码 Bx(EcsTypeCode) GetI, // iABC: R[A] = R[B][R[C]](数组元素 / 字符串单字符) SetI, // iABC: R[B][R[C]] = R[A](原地修改) @@ -109,8 +109,8 @@ public static class EcsSyscall /// 镜像格式修订号(缓存键成分;格式语义变化时改值,无兼容梯—— /// 全新实现不做历史版本分级)。当前 = 每函数寄存器栈窗口 + 帧内固定 RET/ARG/STAGING 布局 - /// ()+ 定长指令编码 + 无接收哨兵。 - public const int AbiRevision = 1; + /// ()+ v4 按需扩展操作数 + 无接收哨兵。 + public const int AbiRevision = 2; // ---- 文件族(1..9 连续,IsFileFamily 依赖)---- public const int FWrite = 1; @@ -250,7 +250,7 @@ internal static class EcsJump /// /// 帧内寄存器布局约定(与 native/ecs_vm.h 的 ECS_REG_* 一一对应,双端手工镜像)。 /// 帧模型 = Dalvik/Lua 式每函数栈窗口:帧长 = 函数自身寄存器高水位(EcsFunction.NSlots, -/// ≤ ,指令域 u8 是唯一上限——项目增长不再牵动寄存器堆), +/// ≤ ,寄存器操作数按需扩展到 u16), /// 调用时在共享值栈上按被调方帧长开窗、返回时弹出。 /// 帧内布局:[R0 = RET][R1 起 = NParams 个参数][按需 STAGING][分配区 /// (独占:φ/ForStep/循环携带/编码器临时 → 池区)]。仅 RET/参数入口位置固定; @@ -259,8 +259,8 @@ internal static class EcsJump /// public static class EcsRegisters { - /// 寄存器号上限(指令 A/B/C 域为 u8;Lua 同款天花板)。帧长 ≤ 此值。 - public const int MaxRegisters = 255; + /// 帧槽数上限(函数表 NSlots 为 u16,寄存器范围 R0–R65534)。 + public const int MaxRegisters = ushort.MaxValue; /// R0 = RET:无显式接收寄存器调用的返回值固定落点(Call 的 C=0)。 public const int Ret = 0; @@ -268,8 +268,8 @@ public static class EcsRegisters /// R1 起 = 参数窗:Call 从 A 指定的出站窗口深拷贝 NParams 个实参(S-17)。 public const int ArgBase = 1; - /// 参数数的编码上限;参数、出站窗口与临时值还须共同满足函数帧预算。 - public const int ArgCount = MaxRegisters - ArgBase; + /// 参数计数保留原 ABI 上限(模块签名字段 u8),独立于寄存器宽度。 + public const int ArgCount = 254; /// 常量数组模板的出站分块预算;无数组构建时不预留这些槽位。 public const int ArrayStagingCount = 16; diff --git a/src/EasyCon.Script/Bytecode/EcxInterpreter.cs b/src/EasyCon.Script/Bytecode/EcxInterpreter.cs index 39df5713..67d1ec50 100644 --- a/src/EasyCon.Script/Bytecode/EcxInterpreter.cs +++ b/src/EasyCon.Script/Bytecode/EcxInterpreter.cs @@ -596,7 +596,7 @@ string ToStringNested(TaggedValue v) // ---- 主循环 ---- - /// 帧出租(每函数栈窗口):帧长 = 函数寄存器高水位(NSlots ≤255), + /// 帧出租(每函数栈窗口):帧长 = 函数寄存器高水位(NSlots ≤65535), /// 池空新建;复用时槽位 Array.Clear 复位为 Void(StoreFresh「无旧值」直写契约), /// 容量不足则扩容(容量只增,峰值深度即池上限——池随弹帧回收)。 Frame RentFrame(int nslots) diff --git a/src/EasyCon.Script/Bytecode/EcxLinker.cs b/src/EasyCon.Script/Bytecode/EcxLinker.cs index 0d00505d..22f5b24b 100644 --- a/src/EasyCon.Script/Bytecode/EcxLinker.cs +++ b/src/EasyCon.Script/Bytecode/EcxLinker.cs @@ -318,7 +318,7 @@ static uint ComputeFeatures(EcxImage image, bool reachesHole) // syscall/native 后返回。链接期在调用点展开,消除 CALL/RET 往返、壳函数体;被展开的壳 // 由随后的可达性清除删除。守卫(违一即放弃该调用点,保持调用形态): // 壳体:std/vision 模块、单块无跳转/无体内 Call/无中间 Ret、≤24 条、末条 Ret/Ret0; - // 调用点:NParams == 实参数、内联后调用者槽位 ≤254(v3 槽位 u8); + // 调用点:NParams == 实参数、内联后调用者帧长 ≤ EcsRegisters.MaxRegisters; // 壳体指令的操作数重映射按「槽位性」白名单执行(立即数/坐标/长度不映射); // 体内含函数调用的壳(vision OCR→OCR_INIT 链)不内联,保持包装形态。 // 不动点迭代 ≤4 轮。行号表(指令下标单位)与跳转(指令下标增量)随拼接平移重算。 diff --git a/src/EasyCon.Script/Bytecode/InstructionCodec.cs b/src/EasyCon.Script/Bytecode/InstructionCodec.cs index 2a4e2f4f..648b6390 100644 --- a/src/EasyCon.Script/Bytecode/InstructionCodec.cs +++ b/src/EasyCon.Script/Bytecode/InstructionCodec.cs @@ -1,25 +1,31 @@ namespace EasyCon.Script.Bytecode; /// -/// 解码形态 ↔ 定长字节流的单处换算(docs/SingleStreamFormat.md §2/§5,v3 定长编码)。 -/// -/// 线格式 = 定长指令:4B(Iabc / ABx / AsBx / IsJ)或 8B(Ext / IabcJ = iABC + u32 数据字), -/// 尺寸由 唯一决定;操作数位宽由格式锁定—— -/// 槽位/立即字节 = u8(Call/CallN 的接收槽 255 = 无)、ABx = u16、AsBx = s16、IsJ = s24、 -/// 数据字 = u32(AsBx/IsJ/IabcJ 的「数据字/尾字段」承载跳转的**字节**偏移)。 -/// Project(Lift(b)) == b、Lift(Project(x)) ≡ x 无条件下成立(布局不再依赖内容)。 +/// 解码形态 ↔ 字节流的单处换算(docs/VM2.md §3.1,v4 操作数扩展编码)。 +/// 基础指令仍为 4B/8B;需要大寄存器号时,操作码 bit7 置位,在头字后插入 +/// [A高8位, B高8位, C高8位, 0]。ABx/AsBx 只扩展 A,IsJ 不扩展。 +/// EXT/跳转数据字在扩展字之后。拒绝多余扩展和非零保留位,保证编码往返唯一。 /// /// 跳转偏移的双表示:**内存** = 相对指令下标(解释器按下标取指;前插平移不变); /// **线格式** = 相对**字节**偏移(目标 = 指令起始 + 本指令字节数 + delta)。 -/// 换算只发生在本类: 一遍计算布局(定长 ⇒ 无定点迭代), +/// 换算只发生在本类: 一遍计算布局(宽度只依赖操作数,无定点迭代), /// 借指令起始字节表反查目标下标。 /// public static class InstructionCodec { - /// 每条指令的字节数(4B 或 8B,由格式表唯一决定)。 + public const byte WideFlag = 0x80; + + /// 不含操作数扩展字的基础字节数(4B 或 8B)。 public static int SizeOf(EcsOpcode op) => EcsFormat.WordCount(op) * 4; - public static int InstructionSize(in EcsInstruction ins) => SizeOf(ins.Op); + static bool HasAbc(EcsOpcode op) + => EcsFormat.Get(op) is EcsInsFormat.Iabc or EcsInsFormat.Ext or EcsInsFormat.IabcJ; + + static bool IsWide(in EcsInstruction ins) + => EcsFormat.Get(ins.Op) != EcsInsFormat.IsJ + && (ins.A > byte.MaxValue || (HasAbc(ins.Op) && (ins.B > byte.MaxValue || ins.C > byte.MaxValue))); + + public static int InstructionSize(in EcsInstruction ins) => SizeOf(ins.Op) + (IsWide(ins) ? 4 : 0); // ---- 布局依赖的跳转(内存 Jump(指令下标)→ 线上字节偏移) ---- @@ -43,25 +49,17 @@ static void CheckJumpRange(EcsOpcode op, int jumpBytes, int at) $"{op} 跳转字节偏移越界: {jumpBytes}(格式容许 ±{limit})", null, at) }); } - static byte SlotByte(int v, EcsOpcode op, int at) - { - if (v < 0 || v > 254) - throw new BytecodeException(new[] { new BytecodeDiagnostic( - $"{op} 槽位越界: {v}(v3 定长编码槽位为 u8;寄存器范围 R0–R254)", null, at) }); - return (byte)v; - } - - static byte ByteOperand(int v, EcsOpcode op, string name, int at) + static byte OperandLow(int v, EcsOpcode op, string name, int at) { - if (v < 0 || v > 255) + if (v < 0 || v >= EcsRegisters.MaxRegisters) throw new BytecodeException(new[] { new BytecodeDiagnostic( - $"{op} 的 {name} 越界: {v}(v3 定长编码为 u8)", null, at) }); + $"{op} 的 {name} 越界: {v}(寄存器范围 R0–R{EcsRegisters.MaxRegisters - 1})", null, at) }); return (byte)v; } // ---- Project:解码形态 → 字节流 ---- - /// 编码一个函数的指令序列为定长字节流(一遍布局;跳转范围越界响亮失败)。 + /// 编码一个函数的指令序列(一遍布局,按需插入操作数扩展字;跳转越界报错)。 public static byte[] Project(IReadOnlyList instructions) { int n = instructions.Count; @@ -90,24 +88,27 @@ public static byte[] Project(IReadOnlyList instructions) CheckJumpRange(ins.Op, jumpBytes, i); } - buf.Add((byte)ins.Op); + bool wide = IsWide(ins); + buf.Add((byte)((byte)ins.Op | (wide ? WideFlag : 0))); switch (EcsFormat.Get(ins.Op)) { case EcsInsFormat.Iabc: - buf.Add(SlotByte(ins.A, ins.Op, i)); - buf.Add(ByteOperand(ins.B, ins.Op, "B", i)); - buf.Add(ByteOperand(ins.C, ins.Op, "C", i)); + case EcsInsFormat.Ext: + case EcsInsFormat.IabcJ: + buf.Add(OperandLow(ins.A, ins.Op, "A", i)); + buf.Add(OperandLow(ins.B, ins.Op, "B", i)); + buf.Add(OperandLow(ins.C, ins.Op, "C", i)); break; case EcsInsFormat.ABx: - buf.Add(SlotByte(ins.A, ins.Op, i)); + buf.Add(OperandLow(ins.A, ins.Op, "A", i)); W16(buf, ins.B >= 0 && ins.B <= 0xFFFF ? ins.B : throw new BytecodeException(new[] { new BytecodeDiagnostic( $"{ins.Op} 的 Bx 越界: {ins.B}(u16)", null, i) })); break; case EcsInsFormat.AsBx: - buf.Add(SlotByte(ins.A, ins.Op, i)); + buf.Add(OperandLow(ins.A, ins.Op, "A", i)); if (ins.IsJump) { CheckJumpRange(ins.Op, jumpBytes, i); S16(buf, jumpBytes); } else S16(buf, ins.B >= short.MinValue && ins.B <= short.MaxValue ? ins.B : throw new BytecodeException(new[] { new BytecodeDiagnostic( @@ -119,24 +120,21 @@ public static byte[] Project(IReadOnlyList instructions) S24(buf, jumpBytes); break; - case EcsInsFormat.Ext: - buf.Add(SlotByte(ins.A, ins.Op, i)); - buf.Add(ByteOperand(ins.B, ins.Op, "B", i)); - buf.Add(ByteOperand(ins.C, ins.Op, "C", i)); - W32(buf, unchecked((int)ins.Ext)); - break; - - case EcsInsFormat.IabcJ: - buf.Add(SlotByte(ins.A, ins.Op, i)); - buf.Add(ByteOperand(ins.B, ins.Op, "B", i)); - buf.Add(ByteOperand(ins.C, ins.Op, "C", i)); - W32(buf, jumpBytes); - break; - default: throw new BytecodeException(new[] { new BytecodeDiagnostic( $"{ins.Op} 未登记编码格式", null, i) }); } + if (wide) + { + buf.Add((byte)(ins.A >> 8)); + buf.Add(HasAbc(ins.Op) ? (byte)(ins.B >> 8) : (byte)0); + buf.Add(HasAbc(ins.Op) ? (byte)(ins.C >> 8) : (byte)0); + buf.Add(0); + } + if (EcsFormat.Get(ins.Op) == EcsInsFormat.Ext) + W32(buf, unchecked((int)ins.Ext)); + else if (EcsFormat.Get(ins.Op) == EcsInsFormat.IabcJ) + W32(buf, jumpBytes); } return buf.ToArray(); } @@ -152,6 +150,8 @@ public static List Lift(ReadOnlySpan stream, int start, in public static List LiftWithStarts(ReadOnlySpan stream, int start, int end, out List instructionStarts, string? function = null) { + if (start < 0 || end < start || end > stream.Length) + throw Fail("指令流区间越界", function, start); var result = new List(); var startBytes = new List(); var rawJumpBytes = new List(); // 与 result 平行:跳转指令的线格式字节偏移(非跳转 = 0) @@ -159,18 +159,38 @@ public static List LiftWithStarts(ReadOnlySpan stream, int while (pos < end) { int insStart = pos; - var op = (EcsOpcode)stream[pos]; + bool wide = (stream[pos] & WideFlag) != 0; + var op = (EcsOpcode)(stream[pos] & ~WideFlag); if ((int)op >= System.Enum.GetValues().Length) throw Fail($"未登记操作码 {(int)op}", function, insStart); - int size = SizeOf(op); + int size = SizeOf(op) + (wide ? 4 : 0); if (pos + size > end) throw Fail("指令流截断(定长指令不完整)", function, insStart); int a = stream[pos + 1]; int b = stream[pos + 2]; int c = stream[pos + 3]; + if (wide) + { + if (EcsFormat.Get(op) == EcsInsFormat.IsJ || stream[pos + 7] != 0 + || (!HasAbc(op) && (stream[pos + 5] != 0 || stream[pos + 6] != 0))) + throw Fail("无效的操作数扩展字", function, insStart); + a |= stream[pos + 4] << 8; + if (HasAbc(op)) + { + b |= stream[pos + 5] << 8; + c |= stream[pos + 6] << 8; + } + if (a == ushort.MaxValue || (HasAbc(op) && (b == ushort.MaxValue || c == ushort.MaxValue))) + throw Fail("扩展操作数超出寄存器范围", function, insStart); + if (a <= byte.MaxValue && (!HasAbc(op) || (b <= byte.MaxValue && c <= byte.MaxValue))) + throw Fail("不需要操作数扩展字", function, insStart); + } uint word = 0; - if (size == 8) - word = (uint)(stream[pos + 4] | stream[pos + 5] << 8 | stream[pos + 6] << 16 | stream[pos + 7] << 24); + if (EcsFormat.ExtWords(op) != 0) + { + int wordStart = pos + size - 4; + word = (uint)(stream[wordStart] | stream[wordStart + 1] << 8 | stream[wordStart + 2] << 16 | stream[wordStart + 3] << 24); + } pos += size; int rawJump = 0; @@ -227,7 +247,7 @@ public static List LiftWithStarts(ReadOnlySpan stream, int if (!layoutJump) continue; int raw = rawJumpBytes[i]; - int targetByte = startBytes[i] + SizeOf(result[i].Op) + raw; + int targetByte = startBytes[i] + InstructionSize(result[i]) + raw; int targetIdx = startBytes.BinarySearch(targetByte); if (targetIdx < 0) throw Fail($"跳转落点 {targetByte} 不是指令起始字节", function, i); diff --git a/src/EasyCon.Vm/native/ecs_vm.c b/src/EasyCon.Vm/native/ecs_vm.c index f8335282..1e969088 100644 --- a/src/EasyCon.Vm/native/ecs_vm.c +++ b/src/EasyCon.Vm/native/ecs_vm.c @@ -43,10 +43,6 @@ #ifndef ECS_MAX_FRAMES #define ECS_MAX_FRAMES (ECS_FRAME_SEG_BYTES / 64) #endif -/* 宿主容量档案(R-3:容量属宿主不属产物;加载期按档案校验每函数槽位) */ -#ifndef ECS_MAX_SLOTS -#define ECS_MAX_SLOTS 255 -#endif #define ECS_DEFAULT_BUDGET 1000000 /* 接收槽恒为寄存器号(无哨兵值) */ @@ -153,8 +149,7 @@ typedef struct { uint32_t ret_pc; /* 续跑点 = 函数内字节偏移(定长取指;仅调用边界与让出点写回) */ uint32_t ret_idx; /* 续跑点 = 指令下标(error_pc 诊断单位,与 ret_pc 同点写回) */ uint16_t func_index; /* 诊断 / 函数表下标(镜像 func_count 为 u16,< 64K 函数) */ - uint8_t ret_slot; /* 接收寄存器(恒 ≤ 255 = 指令域上限;0 = RET 固定落点) */ - uint8_t _pad; + uint16_t ret_slot; /* 接收寄存器(0 = RET 固定落点) */ ecs_value slots[]; } frame; /* 帧字节数由 fn->nslots 派生(frame_size),不入帧头 */ @@ -1053,7 +1048,7 @@ static int32_t load_image(ecs_vm *vm, const uint8_t *image, size_t len) /* ---- ECX1 平铺头(36B)---- */ if (rd_u32(image) != 0x31584345u) return ECS_ERR_IMAGE; /* "ECX1" */ - if (rd_u16(image + 4) != 1) return ECS_ERR_IMAGE; /* format = 1 */ + if (rd_u16(image + 4) != 2) return ECS_ERR_IMAGE; /* format = 2: wide operands */ if (rd_u16(image + 6) != (uint16_t)ECS_ABI_REV) return ECS_ERR_UNSUPPORTED; uint16_t feats = rd_u16(image + 8); vm->entry = (int16_t)rd_u16(image + 10); @@ -1214,7 +1209,7 @@ static int32_t load_image(ecs_vm *vm, const uint8_t *image, size_t len) fd->nslots = (uint16_t)r_bytes(&r, 2); fd->code_off = (uint32_t)r_bytes(&r, 4); if (r.overflow) return ECS_ERR_IMAGE; - if (fd->nslots > ECS_REG_MAX) return ECS_ERR_IMAGE; /* 帧长上限 = 指令域 u8 */ + if (fd->nslots == 0) return ECS_ERR_IMAGE; } if (code_size > len - r.pos) return ECS_ERR_IMAGE; const uint8_t *stream = image + r.pos; @@ -1616,6 +1611,27 @@ static void pop_frame(ecs_vm *vm, frame *fr) #define ECS_DIRECT_THREADED 0 #endif +/* 非 ABC 格式只有 A 可扩展;IsJ 无寄存器操作数。 */ +static inline int ecs_has_bx(uint32_t op) +{ + switch (op) { + case OP_LoadI: case OP_LoadK: case OP_LoadG: case OP_StoreG: + case OP_NewArrE: case OP_NewSt: case OP_Img: case OP_Jpt: case OP_Jpf: + return 1; + default: + return 0; + } +} + +static inline int ecs_valid_wide(const uint8_t *code, uint32_t op) +{ + uint32_t hi = rd_u32(code + 4); + if (op == OP_Jmp || hi == 0 || (hi & 0xff000000u) != 0) return 0; + if (code[1] == 255 && code[4] == 255) return 0; + if (ecs_has_bx(op)) return (hi & 0xffff00u) == 0; + return !(code[2] == 255 && code[5] == 255) && !(code[3] == 255 && code[6] == 255); +} + #if !ECS_DIRECT_THREADED /* ---- 回退模式专用:格式类表 + 统一取指宏 ---- * 类别:ABC=三槽位;ABX=A+Bx16;SBXI=A+sBx16(LoadI);AJ=A+跳转 s16(Jpt/Jpf); @@ -1644,16 +1660,19 @@ static const uint8_t ecs_op_fmt[] = { vm->error_pc = (int32_t)idx; \ return ECS_ERR_OPCODE; \ } \ - opv = codebuf[pc]; \ - fmt = ecs_op_fmt[opv]; \ - op_size = (fmt >= FMT_WJ) ? 8 : 4; \ - if (pc + op_size > cend) \ + wide_bytes = (codebuf[pc] & ECS_OP_WIDE) ? 4u : 0u; \ + opv = codebuf[pc] & ~ECS_OP_WIDE; \ + op_size = ECS_OP_WORDS(opv) * 4u + wide_bytes; \ + if (opv > OP_CmpJ || op_size > cend - pc \ + || (wide_bytes && !ecs_valid_wide(codebuf + pc, opv))) \ { \ vm->steps = steps; \ vm->error_func = fr->func_index; \ vm->error_pc = (int32_t)idx; \ return ECS_ERR_OPCODE; \ } \ + fmt = ecs_op_fmt[opv]; \ + operand_high = wide_bytes ? rd_u32(codebuf + pc + 4) : 0; \ ext_ = 0; \ jmp_ = 0; \ switch (fmt) \ @@ -1679,9 +1698,15 @@ static const uint8_t ecs_op_fmt[] = { break; /* 跳转字延迟读取:仅转移成立时才碰(热环出口多数不成立) */\ default: /* FMT_WEXT */ \ a_ = codebuf[pc + 1]; b_ = codebuf[pc + 2]; c_ = codebuf[pc + 3]; \ - ext_ = rd_u32(codebuf + pc + 4); \ + ext_ = rd_u32(codebuf + pc + 4 + wide_bytes); \ break; \ } \ + a_ |= (int32_t)((operand_high & 0xffu) << 8); \ + if (fmt == FMT_ABC || fmt >= FMT_WJ) \ + { \ + b_ |= (int32_t)(operand_high & 0xff00u); \ + c_ |= (int32_t)((operand_high >> 8) & 0xff00u); \ + } \ op_start = pc; \ op_index = idx; \ idx++; \ @@ -1739,7 +1764,7 @@ int ecs_vm_run(ecs_vm *vm) int32_t steps = vm->steps; uint32_t opv = 0, op_start = 0, op_size = 4, op_index = 0; int32_t a_ = 0, b_ = 0, c_ = 0, jmp_ = 0; - uint32_t ext_ = 0; + uint32_t ext_ = 0, wide_bytes = 0, operand_high = 0; #if !ECS_DIRECT_THREADED uint8_t fmt = FMT_ABC; #endif @@ -1784,9 +1809,11 @@ int ecs_vm_run(ecs_vm *vm) if (vm->cancel_flag) { vm->steps = steps; fr->ret_pc = pc; fr->ret_idx = idx; return ECS_CANCELLED; } \ if (++steps >= vm->budget) { vm->steps = 0; fr->ret_pc = pc; fr->ret_idx = idx; return ECS_YIELD; } \ if (pc >= cend) { vm->steps = steps; vm->error_func = fr->func_index; vm->error_pc = (int32_t)idx; return ECS_ERR_OPCODE; } \ - opv = codebuf[pc]; \ - op_size = (uint32_t)ECS_OP_WORDS(opv) * 4u; \ - if (opv > OP_CmpJ || pc + op_size > cend) { vm->steps = steps; vm->error_func = fr->func_index; vm->error_pc = (int32_t)idx; return ECS_ERR_OPCODE; } \ + wide_bytes = (codebuf[pc] & ECS_OP_WIDE) ? 4u : 0u; \ + opv = codebuf[pc] & ~ECS_OP_WIDE; \ + op_size = (uint32_t)ECS_OP_WORDS(opv) * 4u + wide_bytes; \ + if (opv > OP_CmpJ || op_size > cend - pc || (wide_bytes && !ecs_valid_wide(codebuf + pc, opv))) { vm->steps = steps; vm->error_func = fr->func_index; vm->error_pc = (int32_t)idx; return ECS_ERR_OPCODE; } \ + operand_high = wide_bytes ? rd_u32(codebuf + pc + 4) : 0; \ op_start = pc; op_index = idx; idx++; pc += op_size; \ } while (0) #define VM_CASE(op) L_##op: @@ -1801,13 +1828,14 @@ int ecs_vm_run(ecs_vm *vm) /* 操作数取指(handler 按自身格式特化;回退模式由 VM_FETCH 统一解码,此处为空) */ #if ECS_DIRECT_THREADED -#define FETCH_OPS_ABC() do { a_ = codebuf[op_start + 1]; b_ = codebuf[op_start + 2]; c_ = codebuf[op_start + 3]; } while (0) -#define FETCH_OPS_ABX() do { a_ = codebuf[op_start + 1]; b_ = codebuf[op_start + 2] | codebuf[op_start + 3] << 8; c_ = 0; } while (0) -#define FETCH_OPS_SBXI() do { a_ = codebuf[op_start + 1]; b_ = (int16_t)(codebuf[op_start + 2] | codebuf[op_start + 3] << 8); c_ = 0; } while (0) -#define FETCH_OPS_AJ() do { a_ = codebuf[op_start + 1]; jmp_ = (int16_t)(codebuf[op_start + 2] | codebuf[op_start + 3] << 8); b_ = c_ = 0; } while (0) +#define FETCH_A() (codebuf[op_start + 1] | (int32_t)((operand_high & 0xffu) << 8)) +#define FETCH_OPS_ABC() do { a_ = FETCH_A(); b_ = codebuf[op_start + 2] | (int32_t)(operand_high & 0xff00u); c_ = codebuf[op_start + 3] | (int32_t)((operand_high >> 8) & 0xff00u); } while (0) +#define FETCH_OPS_ABX() do { a_ = FETCH_A(); b_ = codebuf[op_start + 2] | codebuf[op_start + 3] << 8; c_ = 0; } while (0) +#define FETCH_OPS_SBXI() do { a_ = FETCH_A(); b_ = (int16_t)(codebuf[op_start + 2] | codebuf[op_start + 3] << 8); c_ = 0; } while (0) +#define FETCH_OPS_AJ() do { a_ = FETCH_A(); jmp_ = (int16_t)(codebuf[op_start + 2] | codebuf[op_start + 3] << 8); b_ = c_ = 0; } while (0) #define FETCH_OPS_J() do { jmp_ = (int32_t)((codebuf[op_start + 1] | codebuf[op_start + 2] << 8 | codebuf[op_start + 3] << 16) << 8) >> 8; a_ = b_ = c_ = 0; } while (0) -#define FETCH_OPS_WJ() do { a_ = codebuf[op_start + 1]; b_ = codebuf[op_start + 2]; c_ = codebuf[op_start + 3]; } while (0) -#define FETCH_OPS_WEXT() do { a_ = codebuf[op_start + 1]; b_ = codebuf[op_start + 2]; c_ = codebuf[op_start + 3]; ext_ = rd_u32(codebuf + op_start + 4); } while (0) +#define FETCH_OPS_WJ() FETCH_OPS_ABC() +#define FETCH_OPS_WEXT() do { FETCH_OPS_ABC(); ext_ = rd_u32(codebuf + op_start + 4 + wide_bytes); } while (0) #else #define FETCH_OPS_ABC() do { } while (0) #define FETCH_OPS_ABX() do { } while (0) @@ -2316,7 +2344,7 @@ VM_CASE(OP_Call) frame *nf = frame_at(vm, vm->frame_off[vm->depth - 1]); nf->ret_pc = 0; nf->ret_idx = 0; - nf->ret_slot = (uint8_t)c_; + nf->ret_slot = (uint16_t)c_; for (int32_t i = 0; i < b_; i++) { ecs_value copied = deep_copy_cow(vm, R[a_ + i]); @@ -2825,7 +2853,7 @@ VM_CASE(OP_ForStep) int32_t next = (int32_t)((uint32_t)R[a_].i32 + 1u); store_fresh(vm, &R[c_], make_int(next)); if (next > R[b_].i32) - pc = op_start + op_size + (uint32_t)rd_u32(codebuf + op_start + 4); /* 出口字延迟读 */ + pc = op_start + op_size + (uint32_t)rd_u32(codebuf + op_start + 4 + wide_bytes); /* 出口字延迟读 */ VM_NEXT(); } VM_CASE(OP_CmpJ) /* 比较跳转融合(P2′):kind=typeBlock*6+op;数据字 = 跳转字节偏移 */ @@ -2840,7 +2868,7 @@ VM_CASE(OP_CmpJ) /* 比较跳转融合(P2′):kind=typeBlock*6+op;数 default: { int64_t l = box_l(vm, R[a_]), r = box_l(vm, R[b_]); taken = cmp_kind6_i64(l, r, c_); } break; } if (taken) - pc = op_start + op_size + (uint32_t)rd_u32(codebuf + op_start + 4); + pc = op_start + op_size + (uint32_t)rd_u32(codebuf + op_start + 4 + wide_bytes); VM_NEXT(); } @@ -2863,5 +2891,6 @@ VM_CASE(OP_CmpJ) /* 比较跳转融合(P2′):kind=typeBlock*6+op;数 #undef FETCH_OPS_J #undef FETCH_OPS_WJ #undef FETCH_OPS_WEXT +#undef FETCH_A #endif } \ No newline at end of file diff --git a/src/EasyCon.Vm/native/ecs_vm.h b/src/EasyCon.Vm/native/ecs_vm.h index 2845615e..356910f4 100644 --- a/src/EasyCon.Vm/native/ecs_vm.h +++ b/src/EasyCon.Vm/native/ecs_vm.h @@ -4,8 +4,8 @@ * C99,仅依赖 libc(malloc/free/realloc/memcpy/memset/snprintf/strtod/getenv—— * 仅用于加载期表分配与镜像读入;运行期零分配,见 docs/ZeroAllocVm.md P2/P3)。 * 嵌入式移植:把本文件与 ecs_vm.c 加入工程,或经宏重定向内存函数。 - * 容器格式:ECSC v2(docs/SingleStreamFormat.md §3);指令流 = v3 定长编码—— - * 4B(Iabc/ABx/AsBx/IsJ)或 8B(Ext/IabcJ = iABC + u32 数据字),尺寸由 ecs_op_words 给出, + * 容器格式:ECX1 format=2 / ABI=2(docs/VM2.md §3.1);指令流 = v4 操作数扩展编码。 + * 基础 4B/8B 由 ecs_op_words 给出;操作码 bit7 置位时头字后另加 4B 操作数高位字。 * 跳转偏移单位 = 字节(目标 = 指令起始 + 本指令字节数 + delta)。 */ #ifndef ECS_VM_H @@ -19,16 +19,17 @@ extern "C" { #endif /* ---- 容器 ABI(与 C# EcsSyscall.AbiRevision / ECSC manifest.abi_rev 对齐) ---- */ -#define ECS_ABI_REV 1 /* 镜像格式修订号(与 C# EcsSyscall.AbiRevision 对齐; +#define ECS_ABI_REV 2 /* 镜像格式修订号(与 C# EcsSyscall.AbiRevision 对齐; * 全新实现无历史分级,格式语义变化时直接改值) */ /* ---- 帧内寄存器布局(与 C# EcsRegisters 一一对应,双端手工镜像) ---- - * 帧模型 = Dalvik/Lua 式每函数栈窗口:帧长 = 函数寄存器高水位(funcdef.nslots ≤ 255, - * 指令域 u8 是唯一上限),调用时在共享值栈上开窗、返回时弹出。 */ -#define ECS_REG_MAX 255 /* 寄存器号上限(指令 A/B/C 域 u8) */ + * 帧模型 = Dalvik/Lua 式每函数栈窗口:帧长 = 函数寄存器高水位(funcdef.nslots ≤ 65535), + * 调用时在共享值栈上开窗、返回时弹出;实际容量由 ECS_FRAME_SEG_BYTES 限制。 */ +#define ECS_REG_MAX 65535 /* 帧槽数上限(nslots u16;最大寄存器号 65534) */ #define ECS_REG_RET 0 /* R0 = 返回值固定落点(Call 的 C=0) */ #define ECS_REG_ARG_BASE 1 /* R1 起 = NParams 个参数(Call 从 A 指定的出站窗深拷贝) */ -#define ECS_REG_ARG_COUNT (ECS_REG_MAX - ECS_REG_ARG_BASE) +#define ECS_REG_ARG_COUNT 254 /* 参数计数的独立 ABI 限制 */ +#define ECS_OP_WIDE 0x80u /* 参数后按需保留出站/数组分块窗口,再分配临时值;位置由编译器编码进指令。 */ #define ECSC_MAGIC 0x43534345u /* "ECSC" */ #define ECSC_VERSION 2 /* v2 = 定长指令编码(v1 = varint 基流,互不兼容) */ @@ -60,6 +61,8 @@ enum { *(typeBlock:0=i32,1=u32,2=f64,3=i64;op:0=Eq,1=Neq,2=Lt,3=Le,4=Gt,5=Ge)*/ }; +typedef char ecs_opcode_fits_seven_bits[(OP_CmpJ < ECS_OP_WIDE) ? 1 : -1]; + /* ---- 文件族 syscall 编号(uvm32 式编号 ABI,与 C# EcsSyscall 一致,docs/VM2.md §9.1) ---- * CallN 的 EXT 字旗标置位时,低 31 位 = 编号,直接引擎内建分发,不经原生名表。 * 文件实现仍转发 host->native(传规范名);caps 缺失行为与名表时代逐字一致。 */ @@ -82,7 +85,7 @@ enum { #define ECS_FEAT_FILE 0x8u #define ECS_FEAT_AUDIO 0x20u /* AUDIO_LOAD/SCORE/UNLOAD require a background-audio host. */ -/* 指令字数(1 = 4B,2 = 8B 带 u32 数据字)——单一事实源,与 C# EcsFormat.WordCount / +/* 基础指令字数(1 = 4B,2 = 8B 带 u32 数据字;不计操作数扩展字),与 C# EcsFormat.WordCount / * docs/VM2.md §4.1 对齐(新增指令双端各改一处表 + 执行/发射语义)。数据字是「数据」: * EXT 的目标 ID/槽位/时长/坐标,ForStep/CmpJ 的跳转偏移,WaitI/KeyI 的时长—— * 数值可能恰好等于某个操作码,一切线性扫描必须按字数步进跳过(历史 F4 缺陷铁律)。 */ diff --git a/test/EasyCon.Tests/Bytecode/ArgumentWindowTests.cs b/test/EasyCon.Tests/Bytecode/ArgumentWindowTests.cs index 4e72e83f..afffbb3b 100644 --- a/test/EasyCon.Tests/Bytecode/ArgumentWindowTests.cs +++ b/test/EasyCon.Tests/Bytecode/ArgumentWindowTests.cs @@ -189,15 +189,15 @@ public void WrongArity_WithEightParameterDeclaration_ReportsArgumentMismatch() } [Test] - public void CombinedParameterAndOutgoingWindows_OverFrameBudget_ReportsOverflow() + public void CombinedParameterAndOutgoingWindows_Over255_RunCorrectly() { string forwarded = string.Join(", ", Enumerable.Range(0, 128).Select(i => $"$p{i}")); - string source = $"FUNC relay({Parameters(128)}):INT\nRETURN relay({forwarded})\nENDFUNC\n" + + string source = WeightedFunction(128) + $"FUNC relay({Parameters(128)}):INT\nRETURN weighted({forwarded})\nENDFUNC\n" + $"RETURN relay({Arguments(128)})\n"; CompileResult result = Compilation.CompileSource(source, ScriptCompileProfiles.Interactive(optimize: false)); - Assert.That(result.Diagnostics.Any(d => d.Message.Contains("寄存器溢出", StringComparison.Ordinal)), Is.True); - Assert.That(result.Diagnostics.Any(d => d.Code == DiagnosticCodes.ParamWindowOverflow), Is.False); - Assert.That(result.Image, Is.Null); + Assert.That(result.Diagnostics.Where(d => d.IsError), Is.Empty, string.Join("\n", result.Diagnostics)); + Assert.That(result.Image!.Functions.Single(f => f.Name == "relay").NSlots, Is.GreaterThan(255)); + AssertResult(EcsContainer.ReadImage(EcsContainer.WriteImage(result.Image)), Enumerable.Range(1, 128).Sum(i => i * i)); } [Test] diff --git a/test/EasyCon.Tests/Bytecode/ArrayTemplateTests.cs b/test/EasyCon.Tests/Bytecode/ArrayTemplateTests.cs index fbcae184..08d171d1 100644 --- a/test/EasyCon.Tests/Bytecode/ArrayTemplateTests.cs +++ b/test/EasyCon.Tests/Bytecode/ArrayTemplateTests.cs @@ -216,7 +216,7 @@ FUNC probe(): INT [Test] public void OversizedSlots_SerializeAndRoundTrip() { - // 档位塌缩(SingleStreamFormat §2 R-5):槽位字段 u16 + varint 操作数无上限, + // 槽位字段 u16 + v4 按需扩展操作数, // >255 槽函数可编译、可序列化、可 round-trip;容量判定移到烧录前预检(宿主档案)。 var image = new EcxImage { diff --git a/test/EasyCon.Tests/Bytecode/BytecodeSizeAnalysisTests.cs b/test/EasyCon.Tests/Bytecode/BytecodeSizeAnalysisTests.cs index 09e9bd81..7b285f60 100644 --- a/test/EasyCon.Tests/Bytecode/BytecodeSizeAnalysisTests.cs +++ b/test/EasyCon.Tests/Bytecode/BytecodeSizeAnalysisTests.cs @@ -104,11 +104,11 @@ public void Guangshu_DebugStrip_CoreUnchangedAndSmaller() Assert.DoesNotThrow(() => EcsContainer.ReadImage(stripped), "剥离后镜像仍可加载"); } - /// v3 定长密度实测:4B/8B 指令分布与每条指令平均字节数(基线记录性指标,baselines/perf.md)。 + /// v4 密度实测:4B/8B/12B 指令分布与每条指令平均字节数。 [Test] public void Fixed_Density_CorpusAndExamples() { - int fourByte = 0, eightByte = 0, instructions = 0, totalBytes = 0; + int fourByte = 0, eightByte = 0, twelveByte = 0, instructions = 0, totalBytes = 0; foreach (var path in CorpusSources()) { var result = Compilation.CompileFile(path, new CompileOptions { UseDiskCache = false }); @@ -121,16 +121,21 @@ public void Fixed_Density_CorpusAndExamples() instructions += f.Instructions.Count; foreach (var ins in f.Instructions) { - if (EcsFormat.WordCount(ins.Op) == 1) fourByte++; else eightByte++; + switch (InstructionCodec.InstructionSize(ins)) + { + case 4: fourByte++; break; + case 8: eightByte++; break; + case 12: twelveByte++; break; + } } } } TestContext.Out.WriteLine( - $"v3 定长密度: {instructions} 条指令, {totalBytes} B 代码流, 平均 {totalBytes / Math.Max(1, instructions):F2} B/指令;" + - $"4B 指令占 {100.0 * fourByte / Math.Max(1, fourByte + eightByte):F1}%({fourByte}/{fourByte + eightByte},8B={eightByte})"); + $"v4 密度: {instructions} 条指令, {totalBytes} B 代码流, 平均 {totalBytes / (double)Math.Max(1, instructions):F2} B/指令;" + + $"4B={fourByte},8B={eightByte},12B={twelveByte}"); Assert.That(instructions, Is.GreaterThan(0)); Assert.That(totalBytes, Is.GreaterThan(0)); - // 定长下界:4B/条;含数据字指令 8B/条——密度记录进 baselines/perf.md + Assert.That(totalBytes, Is.EqualTo(fourByte * 4 + eightByte * 8 + twelveByte * 12)); } internal static string[] CorpusSources() diff --git a/test/EasyCon.Tests/Bytecode/CvmP0Tests.cs b/test/EasyCon.Tests/Bytecode/CvmP0Tests.cs index 17968c1f..c9aa2326 100644 --- a/test/EasyCon.Tests/Bytecode/CvmP0Tests.cs +++ b/test/EasyCon.Tests/Bytecode/CvmP0Tests.cs @@ -379,18 +379,31 @@ public async Task WideDivisionByZero_PreservesErrorCodeAndInstructionIndex() } [Test] - public async Task FixedLegacyEcx_ExecutesUnchangedOnBothInterpretersAndBudget() + public async Task LegacyContainer_IsRejected_WhileNarrowInstructionBytesRemainStable() { string path = Path.Combine(CorpusAssert.RepoRoot(), "test", "EasyCon.Tests", "Bytecode", "native", "p0-legacy.ecx"); byte[] bytes = File.ReadAllBytes(path); Assert.That(Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(bytes)), Is.EqualTo("681429131EB83F03762413542F2C5B8BC113AA681365C3D4614AB693E0654089")); - EcxImage image = EcsContainer.ReadImage(bytes); + Assert.That(() => EcsContainer.ReadImage(bytes), Throws.TypeOf()); + foreach (string probe in new[] { _normal, _budget }) + { + (int exit, _, string stderr) = await RunBytes(probe, "run", bytes); + Assert.That(exit, Is.EqualTo(EcxInterpreter.ERR_IMAGE), stderr); + } + + // This frozen fixture contains only narrow instructions. Updating its test-only header + // proves the instruction payload stayed byte-for-byte stable across the format boundary. + byte[] current = (byte[])bytes.Clone(); + BitConverter.GetBytes(EcsContainer.FlatFormat).CopyTo(current, 4); + BitConverter.GetBytes((ushort)EcsSyscall.AbiRevision).CopyTo(current, 6); + EcsContainer.ResealCrc32(current); + EcxImage image = EcsContainer.ReadImage(current); List expected = Reference(image); Assert.That(expected, Is.EqualTo(new[] { "6:4010000000000000", "5:ffffffffffffffff" })); foreach (string probe in new[] { _normal, _budget }) { - (int exit, string stdout, string stderr) = await RunBytes(probe, "run", bytes); + (int exit, string stdout, string stderr) = await RunBytes(probe, "run", current); Assert.That(exit, Is.Zero, stderr); Assert.That(CvmRunner.SplitLines(stdout), Is.EqualTo(expected)); } diff --git a/test/EasyCon.Tests/Bytecode/InstructionFormatTests.cs b/test/EasyCon.Tests/Bytecode/InstructionFormatTests.cs index d08510af..fed7a704 100644 --- a/test/EasyCon.Tests/Bytecode/InstructionFormatTests.cs +++ b/test/EasyCon.Tests/Bytecode/InstructionFormatTests.cs @@ -114,7 +114,7 @@ public void ResultSlots_MatchVm2Section42() foreach (var op in writesA) Assert.That(EcsFormat.ResultSlot(op), Is.EqualTo(EcsResultSlot.A), $"{op}"); - // 写 R[C]:调用接收槽(C=255 无接收) + // 写 R[C]:调用接收槽(C=0 为 RET) Assert.That(EcsFormat.ResultSlot(EcsOpcode.Call), Is.EqualTo(EcsResultSlot.C)); Assert.That(EcsFormat.ResultSlot(EcsOpcode.CallN), Is.EqualTo(EcsResultSlot.C)); diff --git a/test/EasyCon.Tests/Bytecode/PcWideSlotTests.cs b/test/EasyCon.Tests/Bytecode/PcWideSlotTests.cs index 57fb7a64..333ee424 100644 --- a/test/EasyCon.Tests/Bytecode/PcWideSlotTests.cs +++ b/test/EasyCon.Tests/Bytecode/PcWideSlotTests.cs @@ -1,3 +1,4 @@ +using EasyCon.Core.Hosting; using EasyCon.Script; using EasyCon.Script.Bytecode; using EasyCon.Script.Modules; @@ -11,12 +12,12 @@ namespace EasyCon.Tests.Bytecode; /// 每函数寄存器帧契约:参数和出站窗口按需分配,NSlots ≤ MaxRegisters; /// 寄存器预算超出即报溢出诊断(Lua 风格,不 spill)。 /// Call/CallN 的接收槽 C 恒为寄存器号(0 = RET 固定落点),无哨兵值。 -/// 本套件锁:溢出编译期诊断 + RET 落点 + 指令编解码往返。 +/// 本套件锁:大帧编译运行 + 缓存/镜像往返 + RET 落点。 /// [TestFixture] public class RegisterFileContractTests { - static string BuildWideSource(int valueCount) + internal static string BuildWideSource(int valueCount) { var source = new StringBuilder(); source.AppendLine("_wideGlobal = 100000"); @@ -34,18 +35,33 @@ static string BuildWideSource(int valueCount) return source.ToString(); } - [Test] - public void WideScript_ExceedingRegisterBudget_FailsWithDiagnostic() + [TestCase(270, true)] + [TestCase(270, false)] + [TestCase(1024, true)] + [TestCase(1024, false)] + public void WideScript_CompilesRunsAndSurvivesImageAndModuleRoundTrips(int count, bool optimize) { - string source = BuildWideSource(270); - CompileResult result = Compilation.CompileSource(source, new CompileOptions + CompileResult result = Compilation.CompileSource(BuildWideSource(count), ScriptCompileProfiles.Interactive(optimize: optimize)); + Assert.That(result.Diagnostics.Where(d => d.IsError), Is.Empty, string.Join("\n", result.Diagnostics)); + Assert.That(result.Image, Is.Not.Null); + Assert.That(result.Image!.MaxSlots, Is.GreaterThan(255)); + ArgumentWindowTests.AssertResult(result.Image, 100001); + foreach (bool stripDebug in new[] { false, true }) { - UseDiskCache = false, - UseProcessCache = false, - }); - Assert.That(result.Diagnostics.Where(d => d.IsError), Is.Not.Empty, - "帧寄存器预算(EcsRegisters.MaxRegisters=255)超限应编译期拒绝(寄存器溢出诊断)"); - Assert.That(result.Image, Is.Null); + EcxImage restored = EcsContainer.ReadImage(EcsContainer.WriteImage(result.Image, stripDebug)); + ArgumentWindowTests.AssertResult(restored, 100001); + for (int i = 0; i < restored.Functions.Count; i++) + { + Assert.That(restored.Functions[i].Instructions, Is.EqualTo(result.Image.Functions[i].Instructions)); + if (!stripDebug) + Assert.That(restored.Functions[i].LineTable, Is.EqualTo(result.Image.Functions[i].LineTable)); + } + } + List modules = result.Artifacts.Select(m => EcsContainer.ReadModule(EcsContainer.WriteModule(m))).ToList(); + for (int m = 0; m < modules.Count; m++) + for (int f = 0; f < modules[m].Functions.Count; f++) + Assert.That(modules[m].Functions[f].LineTable, Is.EqualTo(result.Artifacts[m].Functions[f].LineTable)); + ArgumentWindowTests.AssertResult(EcxPipeline.Link(modules, false, false), 100001); } [Test] diff --git a/test/EasyCon.Tests/Bytecode/StreamInvariantTests.cs b/test/EasyCon.Tests/Bytecode/StreamInvariantTests.cs index 8b5f4075..7b2c379f 100644 --- a/test/EasyCon.Tests/Bytecode/StreamInvariantTests.cs +++ b/test/EasyCon.Tests/Bytecode/StreamInvariantTests.cs @@ -4,10 +4,9 @@ namespace EasyCon.Tests.Bytecode; /// -/// 编解码互逆验证网(docs/SingleStreamFormat.md §4 N2 + N6,v3 定长编码): -/// Lift(Project(x)) ≡ x(逐指令等价);Project(Lift(b)) == b(字节恒等)——定长布局下无条件下成立。 -/// 覆盖:corpus + 合成边界值用例(AsBx s16、ABx u16、IsJ s24、槽位 u8 + 255 无接收哨兵、 -/// 8B 数据字);截断拒读;跳转落点必须为指令起始字节;格式位宽越界响亮失败。 +/// 编解码互逆验证网(docs/VM2.md §3.1): +/// Lift(Project(x)) ≡ x(逐指令等价);Project(Lift(b)) == b(字节恒等)。 +/// 覆盖 corpus、立即数边界、数据字、截断和跳转落点;宽操作数另见 WideOperandTests。 /// [TestFixture] public class StreamInvariantTests @@ -40,7 +39,7 @@ public void Corpus_RoundTrip_InstructionAndByteIdentity() [Test] public void Synthetic_BoundaryValues_Jumps_Ext_RoundTrip() { - // 槽位上界 254 + 无接收哨兵 255、AsBx s16 两端、ABx u16 两端、正反向跳转链、8B 数据字 + // 窄寄存器、AsBx s16 两端、ABx u16 两端、正反向跳转链、8B 数据字 var code = new List { new(EcsOpcode.LoadI, 254, short.MinValue, 0), // AsBx s16 下界 @@ -65,8 +64,8 @@ public void Synthetic_BoundaryValues_Jumps_Ext_RoundTrip() [Test] public void Project_RejectsFormatWidthOverflows() { - // 槽位 > 254 → 响亮拒绝(v3 定长 u8) - var badSlot = new List { new(EcsOpcode.LoadI, 40000, 7, 0) }; + // 最大帧长为 65535,寄存器号不得达到 65535。 + var badSlot = new List { new(EcsOpcode.LoadI, EcsRegisters.MaxRegisters, 7, 0) }; Assert.That(() => InstructionCodec.Project(badSlot), Throws.TypeOf(), "槽位越界应拒绝"); // AsBx 立即数超 s16 → 响亮拒绝(发射侧 LoadK 归一,此处验证编解码层守卫) diff --git a/test/EasyCon.Tests/Bytecode/WideOperandTests.cs b/test/EasyCon.Tests/Bytecode/WideOperandTests.cs new file mode 100644 index 00000000..5a3ec4a7 --- /dev/null +++ b/test/EasyCon.Tests/Bytecode/WideOperandTests.cs @@ -0,0 +1,265 @@ +using EasyCon.Core.Hosting; +using EasyCon.Script; +using EasyCon.Script.Bytecode; +using EasyCon.Tests.Support; + +namespace EasyCon.Tests.Bytecode; + +[TestFixture] +public class WideOperandTests +{ + [Test] + public void NarrowEncoding_RemainsCompact_IncludingRegister255() + { + EcsInstruction[] code = [new(EcsOpcode.LoadI, 255, -2), new(EcsOpcode.Ret, 255, 0)]; + byte[] bytes = InstructionCodec.Project(code); + Assert.That(bytes, Is.EqualTo(new byte[] { (byte)EcsOpcode.LoadI, 255, 254, 255, (byte)EcsOpcode.Ret, 255, 0, 0 })); + Assert.That(InstructionCodec.Lift(bytes, 0, bytes.Length), Is.EqualTo(code)); + } + + [Test] + public void WideEncoding_PreservesOperandsImmediatesAndDataWords() + { + EcsInstruction[] code = + [ + new(EcsOpcode.LoadI, 256, short.MinValue), + new(EcsOpcode.LoadK, 65534, ushort.MaxValue), + new(EcsOpcode.AddI, 1, 256, 65534), + new(EcsOpcode.Slice, 300, 512, 1024, 65534), + new(EcsOpcode.Call, 512, 254, 65534, 0x12345678), + new(EcsOpcode.Ret, 65534, 0), + ]; + byte[] bytes = InstructionCodec.Project(code); + Assert.That(bytes.Length, Is.EqualTo(56)); + Assert.That(bytes[..8], Is.EqualTo(new byte[] { (byte)((byte)EcsOpcode.LoadI | 0x80), 0, 0, 128, 1, 0, 0, 0 })); + List restored = InstructionCodec.Lift(bytes, 0, bytes.Length); + Assert.That(restored, Is.EqualTo(code)); + Assert.That(InstructionCodec.Project(restored), Is.EqualTo(bytes)); + } + + [Test] + public void MixedWidthJumps_RoundTrip_AndRejectTargetsInsideExtension() + { + EcsInstruction[] code = + [ + new(EcsOpcode.Jmp, 0, 0, Jump: 2), + new(EcsOpcode.LoadI, 500, -123), + new(EcsOpcode.CmpJ, 500, 1, 0, Jump: -2), + new(EcsOpcode.Jpt, 500, 0, Jump: -3), + new(EcsOpcode.Jpf, 1, 0, Jump: -4), + new(EcsOpcode.Ret, 500, 0), + ]; + byte[] bytes = InstructionCodec.Project(code); + Assert.That(InstructionCodec.Lift(bytes, 0, bytes.Length), Is.EqualTo(code)); + bytes[1] = 4; // target byte 8 is the LoadI operand extension, not an instruction. + Assert.That(() => InstructionCodec.Lift(bytes, 0, bytes.Length), Throws.TypeOf()); + } + + static IEnumerable InvalidWideInstructions() + { + byte load = (byte)((byte)EcsOpcode.LoadI | 0x80); + yield return [load, 0, 1, 0]; // missing extension + yield return [load, 0, 1, 0, 1, 0, 0]; // truncated extension + yield return [load, 0, 1, 0, 0, 0, 0, 0]; // redundant extension + yield return [load, 0, 1, 0, 1, 0, 0, 1]; // reserved byte + yield return [load, 0, 1, 0, 1, 1, 0, 0]; // Bx cannot be extended + yield return [load, 255, 1, 0, 255, 0, 0, 0]; // register 65535 + yield return [(byte)((byte)EcsOpcode.Jmp | 0x80), 0, 0, 0, 1, 0, 0, 0]; + yield return [(byte)((byte)EcsOpcode.Call | 0x80), 0, 0, 0, 1, 0, 0, 0]; // missing data word + yield return [255, 0, 0, 0, 1, 0, 0, 0]; // unknown base opcode + } + + [TestCaseSource(nameof(InvalidWideInstructions))] + public void MalformedWideEncoding_IsRejected(byte[] bytes) + => Assert.That(() => InstructionCodec.Lift(bytes, 0, bytes.Length), Throws.TypeOf()); + + [Test] + public void MaximumFrame_RoundTripsAndRuns_OverflowIsRejected() + { + EcxImage image = Image(65535, [new(EcsOpcode.LoadI, 65534, 42), new(EcsOpcode.Ret, 65534, 0)]); + ArgumentWindowTests.AssertResult(EcsContainer.ReadImage(EcsContainer.WriteImage(image)), 42); + image.Functions[0].NSlots = 65536; + Assert.That(() => EcsContainer.WriteImage(image), Throws.TypeOf()); + } + + [Test] + public void PreviousFormatAndAbi_AreRejected() + { + byte[] bytes = EcsContainer.WriteImage(Image(1, [new(EcsOpcode.Ret0, 0, 0)])); + bytes[4] = 1; + EcsContainer.ResealCrc32(bytes); + Assert.That(() => EcsContainer.ReadImage(bytes), Throws.TypeOf()); + bytes[4] = (byte)EcsContainer.FlatFormat; + bytes[6] = 1; + EcsContainer.ResealCrc32(bytes); + Assert.That(() => EcsContainer.ReadImage(bytes), Throws.TypeOf()); + + ModuleArtifact module = Compilation.CompileSource("RETURN 42", ScriptCompileProfiles.Interactive()).Artifacts[^1]; + byte[] cache = EcsContainer.WriteModule(module); + cache[6] = 1; + EcsContainer.ResealCrc32(cache); + Assert.That(() => EcsContainer.ReadModule(cache), Throws.TypeOf()); + } + + internal static EcxImage Image(int slots, List code) => new() + { + Entry = 0, + MaxSlots = slots, + Functions = [new EcsFunction { Name = "main", Module = "main", NSlots = slots, HasReturn = true, Instructions = code }], + }; +} + +[TestFixture(true)] +[TestFixture(false)] +public class WideOperandCvmTests(bool directThreading) +{ + string _directory = null!; + string _binary = null!; + + [OneTimeSetUp] + public void BuildVm() + { + _directory = Path.Combine(Path.GetTempPath(), $"EcsWide_{Guid.NewGuid():N}"); + Directory.CreateDirectory(_directory); + string? binary = CvmRunner.EnsureBuilt(_directory, directThreading); + if (binary == null) + Assert.Ignore("无 C99 编译器,跳过原生对照验证"); + _binary = binary!; + } + + [OneTimeTearDown] + public void Cleanup() + { + if (Directory.Exists(_directory)) + { + string temporaryRoot = Path.GetFullPath(Path.GetTempPath()); + Assert.That(Path.GetFullPath(_directory).StartsWith(temporaryRoot, StringComparison.OrdinalIgnoreCase), Is.True); + Directory.Delete(_directory, true); + } + } + + void AssertBoth(EcxImage image, int expected, params int[] waits) + { + EcxHost host = EcsTestHost.CreateRecording(); + ArgumentWindowTests.AssertResult(image, expected, host); + Assert.That(host.WaitLog, Is.EqualTo(waits.Select(ms => $"WAIT {ms}"))); + byte[] bytes = EcsContainer.WriteImage(image); + EcxHost restoredHost = EcsTestHost.CreateRecording(); + ArgumentWindowTests.AssertResult(EcsContainer.ReadImage(bytes), expected, restoredHost); + Assert.That(restoredHost.WaitLog, Is.EqualTo(waits.Select(ms => $"WAIT {ms}"))); + (int exitCode, string stdout, string stderr) = CvmRunner.Run(_binary, bytes, TestContext.CurrentContext.Test.ID, _directory); + Assert.That(exitCode, Is.EqualTo(0), stderr); + Assert.That(CvmRunner.SplitLines(stdout), Is.EqualTo(host.Lines)); + Assert.That(CvmRunner.ParseEventLogs(stderr), Is.EqualTo(EcsTestHost.EventLogGroups(host))); + } + + [TestCase(true)] + [TestCase(false)] + public void ThousandLiveValues_CompileAndRunOnBothVms(bool optimize) + { + CompileResult result = Compilation.CompileSource(RegisterFileContractTests.BuildWideSource(1024), + ScriptCompileProfiles.Interactive(optimize: optimize)); + Assert.That(result.Diagnostics.Where(d => d.IsError), Is.Empty, string.Join("\n", result.Diagnostics)); + Assert.That(result.Image!.MaxSlots, Is.GreaterThan(1024)); + AssertBoth(result.Image, 100001); + } + + [Test] + public void WideArgumentsAndReturnRegisters_PreserveCallerAndCalleeValues() + { + EcxImage image = WideOperandTests.Image(600, + [ + new(EcsOpcode.LoadI, 255, 20), + new(EcsOpcode.LoadI, 256, 22), + new(EcsOpcode.Call, 255, 2, 300, 1), + new(EcsOpcode.WaitV, 300, 0), + new(EcsOpcode.LoadI, 310, -1), + new(EcsOpcode.CallN, 310, 1, 511, EcsSyscall.CallFlag | EcsSyscall.Arg), + new(EcsOpcode.Len, 512, 511), + new(EcsOpcode.WaitV, 512, 0), + new(EcsOpcode.Ret, 300, 0), + ]); + image.Functions.Add(new EcsFunction + { + Name = "callee", + Module = "main", + NSlots = 600, + NParams = 2, + HasReturn = true, + Instructions = [new(EcsOpcode.Move, 512, 1), new(EcsOpcode.AddI, 513, 512, 2), new(EcsOpcode.Ret, 513, 0)], + }); + AssertBoth(image, 42, 42, 0); + } + + [Test] + public void WideLoopAndConditionalJumps_FollowMixedWidthLayout() + { + EcxImage image = WideOperandTests.Image(300, + [ + new(EcsOpcode.LoadI, 260, 0), + new(EcsOpcode.LoadI, 261, 3), + new(EcsOpcode.LoadI, 262, 0), + new(EcsOpcode.AddI, 262, 262, 260), + new(EcsOpcode.ForStep, 260, 261, 263, Jump: 3), + new(EcsOpcode.Move, 260, 263), + new(EcsOpcode.Nop, 0, 0), + new(EcsOpcode.Jmp, 0, 0, Jump: -5), + new(EcsOpcode.CmpJ, 262, 261, 4, Jump: 1), + new(EcsOpcode.Halt, 0, 0), + new(EcsOpcode.Jpt, 262, 0, Jump: 1), + new(EcsOpcode.Halt, 0, 0), + new(EcsOpcode.LoadBool, 264, 0), + new(EcsOpcode.Jpf, 264, 0, Jump: 1), + new(EcsOpcode.Halt, 0, 0), + new(EcsOpcode.WaitV, 262, 0), + new(EcsOpcode.Ret, 262, 0), + ]); + AssertBoth(image, 6, 6); + } + + [Test] + public void WideArrayOperandsAndSliceDataWord_PreserveContainerSemantics() + { + EcxImage image = WideOperandTests.Image(600, + [ + new(EcsOpcode.LoadI, 256, 10), + new(EcsOpcode.LoadI, 257, 20), + new(EcsOpcode.LoadI, 258, 30), + new(EcsOpcode.NewArrV, 300, 3, 256, (uint)EcsTypeCode.Int), + new(EcsOpcode.LoadI, 400, 1), + new(EcsOpcode.LoadI, 401, 3), + new(EcsOpcode.LoadI, 402, 0), + new(EcsOpcode.Slice, 450, 300, 400, 401), + new(EcsOpcode.GetI, 451, 450, 402), + new(EcsOpcode.Len, 452, 450), + new(EcsOpcode.WaitV, 451, 0), + new(EcsOpcode.WaitV, 452, 0), + new(EcsOpcode.NewArrE, 510, (int)EcsTypeCode.Int), + new(EcsOpcode.Len, 511, 510), + new(EcsOpcode.WaitV, 511, 0), + new(EcsOpcode.Ret, 451, 0), + ]); + AssertBoth(image, 20, 20, 2, 0); + } + + [Test] + public void MalformedExtension_IsRejectedByNativeVm() + { + byte[] bytes = EcsContainer.WriteImage(WideOperandTests.Image(300, + [new(EcsOpcode.LoadI, 256, 42), new(EcsOpcode.Ret, 256, 0)]), stripDebug: true); + int codeStart = bytes.Length - (int)BitConverter.ToUInt32(bytes, 16); + bytes[codeStart + 7] = 1; + EcsContainer.ResealCrc32(bytes); + (int exitCode, _, _) = CvmRunner.Run(_binary, bytes, "bad-wide", _directory); + Assert.That(exitCode, Is.EqualTo(EcxInterpreter.ERR_OPCODE)); + } + + [Test] + public void FrameExceedingDeviceMemory_ReportsPoolCapacity() + { + byte[] bytes = EcsContainer.WriteImage(WideOperandTests.Image(65535, + [new(EcsOpcode.LoadI, 65534, 42), new(EcsOpcode.Ret, 65534, 0)])); + (int exitCode, _, string stderr) = CvmRunner.Run(_binary, bytes, "large-frame", _directory); + Assert.That(exitCode, Is.EqualTo(18), stderr); // ECS_ERR_POOL: native frame arena exhausted. + } +} \ No newline at end of file diff --git a/test/EasyCon.Tests/Support/CvmRunner.cs b/test/EasyCon.Tests/Support/CvmRunner.cs index 6091c060..119f2bc5 100644 --- a/test/EasyCon.Tests/Support/CvmRunner.cs +++ b/test/EasyCon.Tests/Support/CvmRunner.cs @@ -70,7 +70,7 @@ internal static class CvmRunner } /// 在 workDir 现场编译 ecs-vm(幂等:二进制已存在即复用);无 cc 返回 null。 - public static string? EnsureBuilt(string workDir) + public static string? EnsureBuilt(string workDir, bool directThreading = true) { _ccPath ??= FindCc(); if (_ccPath == null) @@ -81,14 +81,16 @@ internal static class CvmRunner } var nativeDir = FindNativeDir(); Assert.That(nativeDir, Is.Not.Null, "未找到 ecs_vm.c 源目录"); - var binary = Path.Combine(workDir, OperatingSystem.IsWindows() ? "ecs-vm.exe" : "ecs-vm"); + string binaryName = directThreading ? "ecs-vm" : "ecs-vm-switch"; + var binary = Path.Combine(workDir, binaryName + (OperatingSystem.IsWindows() ? ".exe" : "")); if (File.Exists(binary)) return binary; var psi = new ProcessStartInfo { FileName = _ccPath, Arguments = $"-O2 -std=c99 -Wall -Wextra -Werror -o \"{binary}\" " + - $"\"{Path.Combine(nativeDir!, "ecs_vm.c")}\" \"{Path.Combine(nativeDir!, "ecs_main.c")}\" -lm", + $"\"{Path.Combine(nativeDir!, "ecs_vm.c")}\" \"{Path.Combine(nativeDir!, "ecs_main.c")}\" -lm" + + (directThreading ? "" : " -DECS_NO_DIRECT_THREADING"), RedirectStandardError = true, RedirectStandardOutput = true, UseShellExecute = false,