diff --git a/AGENTS.md b/AGENTS.md index a91ca0a2..5b0e0db5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -91,11 +91,12 @@ Rules to respect when changing code: "raw material" (`ScriptHostContext`: pad, frame/ROI/label delegates, console, args); the assembler owns adapter wrapping, defaults (Environment/Files/OCR/DNN) and resource teardown (`CapabilityLease`). - **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 - params, outbound windows and live values. Device fit is a burn-time capacity precheck - concern, not a profile. + (Desktop target, PC-only in-memory instructions; encodable modules may use process cache) + or `ScriptCompileProfiles.Distributable` (Portable target, obj/ content-addressed cache, + MCU-distributable ECX1 + v3 encoding). Desktop uses `PcInterpreter` and full integer register + operands; its image cannot be serialized. Portable keeps the shared 255-slot budget for + parameters, outbound windows and live values. Never widen the MCU codec/C VM to accommodate + PC scripts. Target belongs in the product/cache fingerprint, including failed compiles. - DI *container* is deliberately absent: composition is explicit constructor passing from `App.axaml.cs` (GUI) and `Program.cs` (CLI). - **AI tool layer: runtime knows capabilities, not integrations.** Tool constructors may only depend on diff --git a/docs/ModuleSystem.md b/docs/ModuleSystem.md index d33024ac..8be0d56b 100644 --- a/docs/ModuleSystem.md +++ b/docs/ModuleSystem.md @@ -75,7 +75,7 @@ interface_hash = SHA-256(CompilerVersion ## 5. 缓存系统(`Modules/ModuleCache.cs`) - **cacheKey** = SHA256(源码 ⊕ 文件模块的规范化源路径上下文 ⊕ Σ直接依赖接口哈希 ⊕ 编译器版本 ⊕ `CompileOptions.ProductFingerprint()`)。 - 影响产物的选项集中在 ProductFingerprint(Optimize / LegacySyntax / ExtVars);其余选项不进键。 + 影响产物的选项集中在 ProductFingerprint(Optimize / Target / ExtVars / syscall ABI / 产物代数);其余选项不进键。 源路径上下文用于保证 `EXTERN ... FROM "相对路径"` 始终按声明 `.ecs` 所在目录解析;内嵌 `std/vision` 与内存伪文件名使用空上下文,仍可跨项目共享。 `.eci` 预声明接口缓存也包含相同路径上下文,避免复用其他项目目录解析出的原生库绝对路径。 文件名 = `<模块名>-.ecm`,不同版本并存免锁竞争。 @@ -84,6 +84,9 @@ interface_hash = SHA-256(CompilerVersion - **错误缓存**:编译失败诊断持久化为 `.err` sidecar,同键命中即原样重报快速失败(ErrorHits 计数); 修复后源码变 → 新键自然失效。重放诊断统一经 `DiagnosticBag.FromMessage` 构造。 - **进程缓存**:仅 `UseDiskCache=false` 现编路径,存 EcmFormat 字节、命中反序列化出新实例(杜绝别名共享)。 +- **PC 扩展目标**:`Target=Desktop` 不建立/读取磁盘缓存,即使调用方设置 UseDiskCache; + 可编码的小模块仍使用带 Desktop 指纹的进程缓存,超过 255 槽的模块不序列化、不进入 ECM。 + Portable 的成功/失败缓存都不能命中 Desktop;`WriteModule` 和 Portable 链接入口拒绝扩展帧。 - **GC**:运行末扫描 obj/,删除引用闭包之外且 mtime 超龄(默认 30 天,可配)的 .ecm/.err。 ## 6. FFI 原生库路径 @@ -109,4 +112,4 @@ interface_hash = SHA-256(CompilerVersion `ModuleProjectTests`(管线/诊断/警告/GC,M7 区段)、`Cache_MerkleInvalidation` 等缓存组 (`ModuleCacheOptimizationTests`)、`ModuleInterfaceTests`(round-trip)、 `InterfaceCompletenessSnapshotTests`(§3 清单快照)、`NestedLibImport_ConstAndCrossCalls`、 -`LibConstant_Parity`、`Cache_StdVisionSharedAcrossProjects`。 \ No newline at end of file +`LibConstant_Parity`、`Cache_StdVisionSharedAcrossProjects`。 diff --git a/docs/Pipeline.md b/docs/Pipeline.md index 22561264..75f581a5 100644 --- a/docs/Pipeline.md +++ b/docs/Pipeline.md @@ -1,7 +1,8 @@ # 统一脚本编译链路 > C# 侧单一编译链路:**源码 → Parser → ProjectCompiler(模块图 + lib/ 自动加载)→ 每模块独立编译 → -> EcxImage**;桌面由 **EcxInterpreter** 执行 ECX 镜像,单片机由 **C VM** 执行同一二进制。 +> 链接后的完整操作数计划**。Desktop 由 **PcInterpreter** 执行内存计划;Portable 经原有 +> **ECX1/v3** 编码后由单片机 **C VM** 执行,不为 PC 扩展修改 MCU 格式。 > 关联:`docs/ModuleSystem.md`(模块/缓存)、`docs/VM2.md`(双端语义,对拍测试锁定)、 > 产物容器规格见 `EcsContainer`/链接器测试(`test/EasyCon.Tests/Bytecode/`)。 @@ -11,17 +12,25 @@ 源码 ─► Parser ─► ProjectCompiler(模块图 + lib/ 自动加载) │ 每模块:InterfaceScopeSynthesizer → Binder(模块单树绑定)→ SSA → 优化 → 编码 ▼ - EcxPipeline.Link ─► EcxImage(ECX 二进制) + EcxPipeline.Link ─► EcxImage(带目标标记的内存表示) ├─ 桌面:IScriptEngine(EasyScriptEngine)→ EcxVm 桥 │ (CapabilitySet 能力装配:输入/控制台/环境/文件/采集/视觉/OCR/推理) - │ → EcxInterpreter - └─ MCU :EcxWriter → .ecx → C VM(ecs-vm) + │ → PcInterpreter → InstructionExecutor + └─ MCU :Portable → EcsContainer → .ecx → C VM(ecs-vm) ``` 执行面(P6 收敛):GUI/CLI 经 `IScriptEngine.FromSource/LoadFile` 得到 `IScriptSession` (`Info` = CompileResult 诊断/镜像/符号),`Run(token, capabilities)` 以**能力集** (`EasyCon.Core.Capabilities.CapabilitySet`,宿主 = 能力实例集合,null = 不可用)装配 `EcxHost` 委托与评估桥——能力模型纯属 PC 装配层,VM 核不感知。 +`ScriptCompileProfiles.Interactive` 选择 Desktop:保留整数槽位编号、动态分配函数帧, +不经过 `InstructionCodec.Project`。`Distributable` 选择 Portable,仍在分配阶段检查 255 槽预算。 +两种目标共用前端、模块链接、指令语义与取消/宿主契约;`InstructionExecutor` 由 PC 专用入口 +与兼容的 `EcxInterpreter` 入口复用,不维护两份算术和引用计数实现。 +`EcxImage.Target` 标记 Desktop 时,`EcsContainer.WriteImage` 一律拒绝导出,即使该脚本很小; +GUI/CLI 导出必须从源码以 Distributable 重新编译。当前仍保留 254 参数及既有常量/表索引限制。 +这次只解耦函数寄存器容量,没有新增可分发的宽 ECX 格式,也没有实现 SSA 直接解释器。 + 入口函数 `
` = 依赖模块 `` 调用序列按拓扑序前插 `$eval` 本体, 顶层 RETURN 经 r0 透传;`` 承载各模块顶层语句与模块私有全局初始化。 @@ -61,7 +70,7 @@ | 内置函数能力清单(路由 + 特征位 + MCU 桩可用标记) | `Binding/BuiltinFunctions.Manifest`(单一登记;`Routes`/`IsIntrinsic` 派生;链接器特征扫描消费 FeatureBit;总表见 VM2.md §7.0) | | syscall 编号/镜像特征位 | `Bytecode/EcsOpcode.cs`(`EcsSyscall`/`EcsImageFeatures`)+ C 侧 `ECS_SYSCALL_*`/`ECS_FEAT_*` | | 代码线性扫描 | `Bytecode/InstructionScanner.cs`(回调式,链接器各 pass 共享) | -| RC 槽写 | C# `MoveToSlot/StoreFresh/OverwriteItem`(EcxInterpreter.cs);C `move_to/store_fresh`(ecs_vm.c) | +| RC 槽写 | C# `MoveToSlot/StoreFresh/OverwriteItem`(InstructionExecutor.cs);C `move_to/store_fresh`(ecs_vm.c) | | 缓存键选项 | `CompileOptions.ProductFingerprint()`;产物自检 `ModuleArtifact.IntegrityCheck()` | | 模块诊断构造 | `DiagnosticBag.FromMessage` | | 测试基建 | `test/EasyCon.Tests/Support/`(CvmRunner/EcsTestHost/CorpusAssert) | @@ -69,9 +78,9 @@ ## 5. 后续项(未实施) - **GUI 一键产 `.ecx` 并烧录**:方案见 `docs/McuBytecodeDelivery.md`(唯一入口 - `McuImageBuilder` + 预检 + 复用 `NintendoSwitch.Flash`;单流化后产物同形态, - GUI 会话镜像可直接序列化,CLI `compile` 已可用)。 -- ~~桌面路径缓存化~~(✅ 单流化档位塌缩后桌面路径已可用进程/磁盘缓存;GUI 默认仍现编)。 + `McuImageBuilder` + 预检 + 复用 `NintendoSwitch.Flash`;从源码重新执行 Portable 编译, + 不能直接序列化 Desktop 会话计划。CLI `compile` 与 GUI 导出使用 `EcxExportService`)。 +- 桌面进程缓存仅保留可编码的小模块;扩展帧模块每次重新编译,不写 ECM。 - ~~运行错误位置映射~~(✅ ECM v4 起产物带行号表,桌面路径 `EcxVm` 已实现 pc→源行; 行号调试块保留,MCU 发布镜像省略)。 - EcxInterpreter 性能:常量串 pinned(✅ S-20 已落地)/ 帧池复用(C VM ✅ ZeroAlloc P2)/ 取消检查三分点 diff --git a/docs/VM2.md b/docs/VM2.md index cbff215c..c1de8a92 100644 --- a/docs/VM2.md +++ b/docs/VM2.md @@ -1,7 +1,8 @@ # VM2 — ECS 字节码虚拟机规范 -> **定位**:ECS 脚本的编译目标与双端执行规范——C# `EcxInterpreter`(桌面)与 C `ecs_vm`(单片机) -> 消费同一 ECX 镜像。本文是**现状规范**:指令集、二进制格式、宿主 ABI、分层模型。 +> **定位**:ECS 脚本的编译目标与双端执行规范——PC `PcInterpreter` 执行 Desktop 内存计划, +> C `ecs_vm`(单片机)执行 Portable ECX 镜像。`EcxInterpreter` 保留为兼容/模拟入口。 +> 本文是**现状规范**:指令集、二进制格式、宿主 ABI、分层模型。 > 权威源:`EcsOpcode.cs` / `EcsFormat.cs`(C# 侧)与 `ecs_vm.h`(C 侧)逐项对齐; > 双端语义一致性由 corpus/FullChain/ABI 对拍测试锁定(`test/EasyCon.Tests/Bytecode/`);编译链路见 `docs/Pipeline.md`; > 模块系统见 `docs/ModuleSystem.md`。冲突时以代码为准并修本文。 @@ -30,7 +31,9 @@ | `src/EasyCon.Script/Bytecode/EcxLinker.cs` | 模块合并、死函数/死存储/死元素消除、特征掩码、入口合成 | | `src/EasyCon.Script/Bytecode/DeadStoreSweep.cs` | 链接期死存储清扫(反向活跃性,防线 3) | | `src/EasyCon.Script/Bytecode/EcxWriter.cs` | ECX 序列化(stripDebug 产 MCU 发布版) | -| `src/EasyCon.Script/Bytecode/EcxInterpreter.cs` | C# 解释器 + `EcxHost`(桌面参考宿主) | +| `src/EasyCon.Script/Bytecode/PcInterpreter.cs` | Desktop 专用内存指令执行入口 | +| `src/EasyCon.Script/Bytecode/InstructionExecutor.cs` | 共用指令语义内核 + `EcxHost`(桌面参考宿主) | +| `src/EasyCon.Script/Bytecode/EcxInterpreter.cs` | ECX 模拟与已有 API 的兼容入口 | | `src/EasyCon.Script/Modules/*` | 模块图/独立编译/接口缓存(`docs/ModuleSystem.md`) | | `src/EasyCon.Vm/native/ecs_vm.h|.c` | C 虚拟机公共 API + 实现(加载器/堆/解释器,纯调度) | | `src/EasyCon.Vm/native/ecs_main.c` | CLI harness = MCU 参考宿主(L2 参考桩 + L1 回调) | @@ -133,7 +136,7 @@ ToStr ToInt`。 > **单流化 + v3 定长(已实施)**:内存唯一表示为解码形态 `EcsInstruction`(完整 int32 操作数, > Jump = 相对指令下标);文件的 **v3 定长指令流**(ECX1 `.text`)与内存按上表一一对应, > 投影/抬升由 `InstructionCodec` 单处换算(线格式跳转 = 相对**字节**偏移:目标 = 指令起始 + -> 本指令字节数 + delta)。操作数位宽由格式锁定:槽位 u8 ≤254(寄存器 ≤255 帧长上限,无哨兵值—— +> 本指令字节数 + delta)。Portable 操作数位宽由格式锁定:槽位 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)。 @@ -142,10 +145,15 @@ ToStr ToInt`。 恰好等于某个操作码,一切线性扫描必须按 `EcsFormat.WordCount` 步进跳过(历史 F4 缺陷铁律, v3 随定长布局回归)。 -容量上限(R-3):槽位上限 254 已前移到**编译期**(v3 槽位 u8,与宿主档案 `ECS_MAX_SLOTS=255` +容量上限(R-3):Portable 槽位上限 254 已前移到**编译期**(v3 槽位 u8,与宿主档案 `ECS_MAX_SLOTS=255` 对齐);加载期校验保留(nslots ≤ 宿主档案)。Jpt/Jpf 偏移越界由编码器跳转反转模板消化, 超 s16 由 `Project` 响亮失败。 +Desktop 不投影为 v3 字节流,`PcInterpreter` 通过共用的 `InstructionExecutor` 直接消费完整 int +寄存器操作数,动态开窗,不继承 255 槽预算。该计划带 Desktop 目标标记,`WriteImage` 拒绝导出; +扩展模块也不能写入 ECM。MCU 指令/格式版本、C VM ABI 和固件均不变。参数上限仍为 254; +下文涉及 u8 槽位和 255 帧预算时描述的是 Portable 编译目标。 + ### 3.2 指令表 权威枚举 = `EcsOpcode.cs`(C 侧 `ecs_vm.h` 数值一一对应)。分组摘录(`R[a]` = 帧槽): @@ -497,4 +505,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/src/EasyCon.Core/Hosting/ScriptCompileProfiles.cs b/src/EasyCon.Core/Hosting/ScriptCompileProfiles.cs index 59469315..8c01f55b 100644 --- a/src/EasyCon.Core/Hosting/ScriptCompileProfiles.cs +++ b/src/EasyCon.Core/Hosting/ScriptCompileProfiles.cs @@ -9,15 +9,12 @@ namespace EasyCon.Core.Hosting; /// 避免 GUI / CLI 各自手写 字面量而悄悄漂移。 /// /// -/// 单流化后(docs/VM2.md §3.1):所有产物都是同一形态(ECX1 平铺 + v3 定长指令编码), -/// **没有宽窄之分**——原 Desktop/Portable 的格式维度随 EnablePcWideSlots -/// 一起删除。当前定长指令编码的函数帧预算为 255 槽;参数与出站窗口按需分配, -/// 桌面与 MCU 共用该预算及同一条可序列化指令流。 -/// 「能不能装进设备」是资源问题,由烧录前的容量预检判定(docs/McuBytecodeDelivery.md,计划中), -/// 不再由编译档位表达。剩余的两档只是**缓存策略**(与产物内容无关,不进缓存键): +/// PC 与 MCU 共用语言语义和完整操作数指令表示,但不共用编码容量限制。 +/// Interactive 生成 PC 内存执行计划;Distributable 生成可序列化的 ECX。 +/// 两种目标进入不同缓存键,PC 扩展模块不写入 ECM,避免混入 MCU 分发链。 /// /// -/// :现编(不落盘,进程级缓存兜底)——run/编辑器等高频重编路径。 +/// :PC 现编,不落盘;可编码的小模块仍可使用进程缓存。 /// :obj/ 内容寻址缓存——compile/分发产物路径,重复编译零成本。 /// /// @@ -30,6 +27,7 @@ public static class ScriptCompileProfiles /// false 时跳过 SSA 优化(IR 观测用)。 public static CompileOptions Interactive(IEnumerable? extVars = null, bool optimize = true) => new() { + Target = ScriptTarget.Desktop, ExtVars = ToSet(extVars), UseDiskCache = false, UseProcessCache = true, @@ -42,6 +40,7 @@ public static class ScriptCompileProfiles /// 外部变量名集合(识图标签名等),null = 无。 public static CompileOptions Distributable(IEnumerable? extVars = null) => new() { + Target = ScriptTarget.Portable, ExtVars = ToSet(extVars), UseDiskCache = true, UseProcessCache = true, diff --git a/src/EasyCon.Core/Runner/EcxVm.cs b/src/EasyCon.Core/Runner/EcxVm.cs index 5fa4a95c..a754b3e6 100644 --- a/src/EasyCon.Core/Runner/EcxVm.cs +++ b/src/EasyCon.Core/Runner/EcxVm.cs @@ -69,7 +69,12 @@ public static Value Run(EcxImage image, CapabilitySet capabilities, Cancellation ?? throw new InvalidOperationException("当前宿主未提供脚本音频能力 AUDIO_LOAD/AUDIO_SCORE/AUDIO_UNLOAD;请使用已连接游戏音频的桌面 GUI。"); audio = service.BeginRun(token); } - int code = EcxInterpreter.Run(image, host, out _, out var errorFunc, out var errorPc, out var result, token); + int errorFunc; + int errorPc; + Value result; + int code = image.Target == ScriptTarget.Desktop + ? PcInterpreter.Run(image, host, out _, out errorFunc, out errorPc, out result, token) + : EcxInterpreter.Run(image, host, out _, out errorFunc, out errorPc, out result, token); if (code == EcxInterpreter.OK) { audio?.CheckHealth(token); diff --git a/src/EasyCon.Script/Bytecode/BytecodeEncoder.cs b/src/EasyCon.Script/Bytecode/BytecodeEncoder.cs index 28fdcd93..2c2416c9 100644 --- a/src/EasyCon.Script/Bytecode/BytecodeEncoder.cs +++ b/src/EasyCon.Script/Bytecode/BytecodeEncoder.cs @@ -33,6 +33,7 @@ public int Add(EcsConst c) /// public sealed class ModuleEncodeContext { + public ScriptTarget Target; public required SsaProgram Program; public required string ModuleName; /// 本模块函数 → 模块局部 fid(值相等键,A-07)。 @@ -297,14 +298,16 @@ 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 = 无接收哨兵); - /// StickSet/StickP 的 B/C 是摇杆坐标(满 u8 0..255),不按槽位收紧。 - static void CheckIabcOperands(EcsOpcode op, int a, int b, int c) + /// Portable 槽位受 v3 编码约束;Desktop 保留整数槽位编号。 + /// StickSet/StickP 的 B/C 恒为摇杆坐标(u8 0..255),不因 PC 扩展而放宽。 + void CheckIabcOperands(EcsOpcode op, int a, int b, int c) { - if (a is < 0 or > 254) ThrowOp(op, $"A 槽位越界: {a}"); + int lastSlot = RegisterLimit - 1; + if (a < 0 || a > lastSlot) 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}"); + int bLimit = coord || _ctx.Target == ScriptTarget.Portable ? 255 : lastSlot; + if (b < 0 || b > bLimit) ThrowOp(op, $"B 越界: {b}"); + if (c < -1 || c > (coord ? 255 : lastSlot)) ThrowOp(op, $"C 越界: {c}"); } static void ThrowOp(EcsOpcode op, string what) @@ -369,14 +372,16 @@ void EmitJump(EcsOpcode op, int cond, int kind, object target) // ---- 主流程 ---- + int RegisterLimit => _ctx.Target == ScriptTarget.Desktop ? int.MaxValue : EcsRegisters.MaxRegisters; + public void Run() { AssignSlots(); EmitBlocks(); Patch(); - if (_poolNext > EcsRegisters.MaxRegisters) - throw Fail($"寄存器溢出:函数 '{_symbol.Name}' 的寄存器需求 {_poolNext} 超过帧上限 {EcsRegisters.MaxRegisters}(拆分函数或简化深表达式)"); + if (_poolNext > RegisterLimit) + throw Fail($"寄存器溢出:函数 '{_symbol.Name}' 的寄存器需求 {_poolNext} 超过帧上限 {RegisterLimit}(拆分函数或简化深表达式)"); Result = new EcsFunction { diff --git a/src/EasyCon.Script/Bytecode/BytecodeModule.cs b/src/EasyCon.Script/Bytecode/BytecodeModule.cs index b903f371..ed7a179e 100644 --- a/src/EasyCon.Script/Bytecode/BytecodeModule.cs +++ b/src/EasyCon.Script/Bytecode/BytecodeModule.cs @@ -145,6 +145,8 @@ public sealed class EcsExport /// public sealed class ModuleArtifact { + /// 包含只能由 PC 执行的扩展帧,禁止写入 ECM 缓存。 + public bool RequiresPcExecution => Functions.Any(f => f.NSlots > EcsRegisters.MaxRegisters); /// 缓存读出完整性自检(disk/process 两条 TryLoad 路径共享): /// 接口区存在且接口哈希与内容自洽;损坏/被篡改 → false(调用方按未命中重编)。 public bool IntegrityCheck() @@ -177,10 +179,12 @@ public bool IntegrityCheck() } /// -/// 链接后可执行镜像(ECX 的内存表示),纯 C 虚拟机的唯一输入。 +/// 链接后可执行计划。Portable 可投影为 C VM 的 ECX;Desktop 仅供 PC 扩展执行。 /// public sealed class EcxImage { + /// Desktop 是仅供 PC 执行的内存计划,不是可分发的 ECX 镜像。 + public ScriptTarget Target { get; internal set; } public List Consts = new(); public List Structs = new(); public List Globals = new(); diff --git a/src/EasyCon.Script/Bytecode/EcsContainerFormat.cs b/src/EasyCon.Script/Bytecode/EcsContainerFormat.cs index 13d3dffa..4659aa8e 100644 --- a/src/EasyCon.Script/Bytecode/EcsContainerFormat.cs +++ b/src/EasyCon.Script/Bytecode/EcsContainerFormat.cs @@ -27,6 +27,8 @@ public static class EcsContainer /// public static byte[] WriteImage(EcxImage image, bool stripDebug = false, bool withCrc = true) { + if (image.Target == ScriptTarget.Desktop) + throw new BytecodeException([new BytecodeDiagnostic("PC 扩展执行计划不能导出为 ECX;请使用 Portable 目标重新编译", null, 0)]); // .text 投影(函数 code_off 依赖字节布局) var codeOffsets = new uint[image.Functions.Count]; var codeBuf = new List(); @@ -133,6 +135,8 @@ public static byte[] WriteImage(EcxImage image, bool stripDebug = false, bool wi /// public static byte[] WriteModule(ModuleArtifact module) { + if (module.RequiresPcExecution) + throw new BytecodeException([new BytecodeDiagnostic("PC 扩展帧不能写入 ECM 模块缓存", module.Name, 0)]); // .text 投影(函数 code_off 依赖字节布局) var codeOffsets = new uint[module.Functions.Count]; var codeBuf = new List(); diff --git a/src/EasyCon.Script/Bytecode/EcxInterpreter.cs b/src/EasyCon.Script/Bytecode/EcxInterpreter.cs index 39df5713..d77f99e9 100644 --- a/src/EasyCon.Script/Bytecode/EcxInterpreter.cs +++ b/src/EasyCon.Script/Bytecode/EcxInterpreter.cs @@ -1,1520 +1,69 @@ -using EasyCon.Script.Runtime; using EasyCon.Script.Symbols; -using System.Runtime.InteropServices; -using System.Text; namespace EasyCon.Script.Bytecode; /// -/// 模拟解释器宿主:与 C VM 的 ecs_host vtable 同构。 -/// -/// 平台能力位(对齐 ecs_vm.h 的 ECS_CAP_*)。 -[Flags] -public enum EcsCaps -{ - None = 0, - Print = 1 << 0, - Alert = 1 << 1, - Beep = 1 << 2, - File = 1 << 3, - Capture = 1 << 4, - Ffi = 1 << 5, - Stdin = 1 << 6, - AllDesktop = Print | Alert | Beep | File | Capture | Ffi | Stdin, - AllMcu = None, // 单片机:全部忽略,脚本副作用仅按键/摇杆/延时 -} - -/// -/// 宿主原生的堆上下文:解释器内部堆(字符串/数组句柄)的受控出口。 -/// CallN 原生回调经此读取字符串/复合实参、回写字符串结果—— -/// 镜像里的句柄值离开解释器堆没有意义,宿主不得自行解释。 -/// -public abstract class EcxNativeContext -{ - /// 字符串解引用(非字符串句柄返回空串)。 - public abstract string Str(TaggedValue v); - - /// 分配字符串,返回可回传给镜像的句柄值。 - public abstract TaggedValue Str(string s); - - /// 完整转换为 Value(字符串解引用、数组逐元素复制;结构体返回 Void)。 - public abstract Value ToValue(TaggedValue v); - - /// Value → TaggedValue(标量/字符串;数组/结构体不支持,返回 Void)。 - public abstract TaggedValue FromValue(Value v); -} - -public sealed class EcxHost -{ - public EcsCaps Caps { get; set; } = EcsCaps.AllDesktop; - /// S-14 行断协议状态(FWRITE/ALERT 尾部 '\' 挂起下一行换行;宿主所有)。 - public bool PendingBreak; - public readonly List WaitLog = new(); - public readonly List KeyLog = new(); - public readonly List KeyStateLog = new(); - public readonly List StickSetLog = new(); - public readonly List StickClickLog = new(); - public readonly List AmiiboLog = new(); - public readonly List BeepLog = new(); - public readonly List Lines = new(); - public readonly List Alerts = new(); - - public Action WaitMs { get; set; } = _ => { }; - public Action Key { get; set; } = (_, _) => { }; - public Action KeyState { get; set; } = (_, _) => { }; - public Action StickSet { get; set; } = (_, _, _) => { }; - public Action StickClick { get; set; } = (_, _, _, _) => { }; - public Func ImgLabel { get; set; } = _ => -1; - public Func Rand { get; set; } = _ => 0; - public Func TimeMs { get; set; } = () => 0; - public Action Beep { get; set; } = (_, _) => { }; - /// S-13 AMIIBO:槽位选择回调(0–19 合法,越界宿主静默——参考实现已判)。 - public Action Amiibo { get; set; } = _ => { }; - /// FWRITE stdout 行断协议的落点(message 已按协议剥尾、newline 已解析)。 - public Action Print { get; set; } = (_, _) => { }; - public Action Alert { get; set; } = _ => { }; - public Func ReadLine { get; set; } = () => ""; - /// - /// 扩展原生(ENCODE/JQ/采集卡洞/FFI);null 或返回 null = 未实现。 - /// 字符串实参/返回值经 解引用/分配——镜像句柄离开解释器堆无意义。 - /// - public Func? Native { get; set; } - /// - /// L2 平台 syscall 处理器(编号语义,docs/VM2.md §9.1);缺省为桌面参考实现 - /// (行断协议 + Caps 门控 + 文件族转发 Native)。返回 null = 未实现。 - /// - public Func? Syscall { get; set; } - /// - /// S-21 strict_caps(默认 off):off = 能力缺失按 缺省值表 - /// 降级(feats 仅元信息);on = 恢复响亮行为(L2/L3 miss 即 ERR_NOSUCHNATIVE, - /// 图像标签 ERR_IL)——迁移保底、诊断模式、「同宿主能力集」对拍严格档。 - /// - public bool StrictCaps { get; set; } - public string[] Args { get; set; } = []; - public string AppDir { get; set; } = ""; - - public EcxHost() - { - Syscall = ReferenceSyscall; // 桌面参考实现;专用宿主可整体替换 - } - - /// - /// L2 平台 syscall 桌面参考实现(编号语义的唯一 C# 权威,与 C harness 桩逐字对齐; - /// 由 VM 核 CallNative/CallSyscall 平移而来): - /// FWRITE/FREAD 行断协议 + Caps 门控;文件实现转发 (规范名,BuiltinMap)。 - /// 返回 null = 未实现(调用方报 ERR_NOSUCHNATIVE)。 - /// - private TaggedValue? ReferenceSyscall(int id, TaggedValue[] args, EcxNativeContext ctx) - { - switch (id) - { - case EcsSyscall.FWrite: - return FWriteReference(args[0].I64, args, ctx); - case EcsSyscall.FRead: - if (args[0].I64 == 0) - return Caps.HasFlag(EcsCaps.Stdin) ? ctx.Str(ReadLine()) : ctx.Str(""); - return Caps.HasFlag(EcsCaps.File) - ? Native?.Invoke(EcsSyscall.Names[id], args, ctx) - : ctx.Str(""); - case EcsSyscall.Alert: - if (Caps.HasFlag(EcsCaps.Alert)) - { - // 行断协议对齐(与 FWRITE stdout / 金标准 ImplAlert 一致):剥尾 `\` 续行 - var alert = ctx.Str(args[0]); - Alert(alert.EndsWith('\\') ? alert[..^1] : alert); - } - return TaggedValue.Void; // 无 Alert 能力:静默忽略 - case EcsSyscall.Arg: - { - var i = args[0].I32; - return ctx.Str(i >= 0 && i < Args.Length ? Args[i] : ""); - } - case EcsSyscall.Env: - return ctx.Str(Environment.GetEnvironmentVariable(ctx.Str(args[0])) ?? ""); - case EcsSyscall.App: - return ctx.Str(AppDir); - case EcsSyscall.Time: - return TaggedValue.FromInt(TimeMs()); - case EcsSyscall.OcrConf: - return Native?.Invoke(EcsSyscall.Names[id], args, ctx) ?? TaggedValue.FromInt(0); - case EcsSyscall.FOpen: - case EcsSyscall.FClose: - case EcsSyscall.FEof: - case EcsSyscall.FileExists: - // 文件族其余:句柄/存在性语义在 BuiltinMap(IFileSystem);缺 File 能力 → 缺省值表 - return Caps.HasFlag(EcsCaps.File) - ? Native?.Invoke(EcsSyscall.Names[id], args, ctx) - : EcsCapabilityDefaults.GetSyscallDefault(id, args, ctx); - case EcsSyscall.Beep: - if (Caps.HasFlag(EcsCaps.Beep)) - Beep(args[0].I32, args[1].I32); - return TaggedValue.Void; // 无 Beep 能力:静默忽略 - case EcsSyscall.Amiibo: - if (args[0].I32 is >= 0 and <= 19) // S-13:槽位 0–19,越界静默(宿主内判) - Amiibo(args[0].I32); - return TaggedValue.Void; - default: - return null; // 未知编号 → ERR_NOSUCHNATIVE - } - } - - /// FWRITE:句柄 1=stdout 行断协议;0=no-op 返 len;2=print(newline);>2=文件族转发。 - private TaggedValue? FWriteReference(long handle, TaggedValue[] args, EcxNativeContext ctx) - { - var s = ctx.Str(args[1]); - if (handle == 1) - { - if (Caps.HasFlag(EcsCaps.Print)) - { - var output = s.EndsWith('\\') ? s[..^1] : s; - Print(output, !PendingBreak); - PendingBreak = s.EndsWith('\\'); - } - return TaggedValue.FromInt(s.Length); - } - if (handle == 0) - return TaggedValue.FromInt(s.Length); - if (handle == 2) - { - if (Caps.HasFlag(EcsCaps.Print)) - Print(s, true); - return TaggedValue.FromInt(s.Length); - } - return Caps.HasFlag(EcsCaps.File) - ? Native?.Invoke(EcsSyscall.Names[EcsSyscall.FWrite], args, ctx) - : TaggedValue.FromInt(-1); - } - - /// 开启事件与输出记录(默认委托 → *Log 列表,格式与金标准 mock 一致)。 - public void EnableRecording() - { - var buf = new StringBuilder(); - WaitMs = ms => WaitLog.Add($"WAIT {ms}"); - Key = (k, d) => KeyLog.Add($"KEY {k} {d}"); - KeyState = (k, d) => KeyStateLog.Add($"KEYST {k} {d}"); - StickSet = (s, x, y) => StickSetLog.Add($"STICK {s} {x} {y}"); - StickClick = (s, x, y, d) => StickClickLog.Add($"STICKC {s} {x} {y} {d}"); - Amiibo = i => AmiiboLog.Add($"AMIIBO {i}"); - Beep = (f, d) => BeepLog.Add($"BEEP {f} {d}"); - Print = (s, nl) => - { - buf.Append(s); - if (nl) - { - Lines.Add(buf.ToString()); - buf.Clear(); - } - }; - Alert = s => Alerts.Add(s); - } -} - -/// -/// ECX 模拟解释器:字节码语义的 C# 参考实现——双端语义由 corpus/FullChain 对拍测试锁定。 -/// 在 C 虚拟机落地前承担"模拟验证";C VM 完成后作为交叉对比的第二方。 +/// ECX 模拟执行兼容入口。Desktop 计划分派到 PC 专用入口;两端共用指令语义内核, +/// 不复制算术、数组、引用计数和取消逻辑。可分发编码约束仍由 Portable 编译及容器检查。 /// public sealed class EcxInterpreter { - sealed class SimArray - { - public byte ElemTag; - public List Items = new(); - } - - sealed class SimStruct - { - public EcsStructLayout Layout = null!; - public TaggedValue[] Slots = null!; - /// GetF 嵌套视图:本结构是父槽区切片的拷贝,写入须经 - /// 回写父槽(对齐 v1 EcsStruct.GetNested 的原生内存写穿透语义)。 - public SimStruct? ViewParent; - public int ViewOffset; - } - - sealed class Frame - { - public EcsFunction Fn = null!; - public int Pc; - public TaggedValue[] Slots = null!; - public int RetSlot = -1; - } - - readonly EcxImage _image; - readonly EcxHost _host; - readonly List _heap = new(); // string / SimArray / SimStruct;索引+1 = 句柄 - readonly List _refCounts = new(); // 与 _heap 平行:槽位对象引用计数(ecs_vm.c §4.2 rc / VM2.md §8.4) - readonly List _freeSlots = new(); // 已释放下标自由链(复用;句柄 = 列表下标,恒不移除/移动元素) - readonly List _frames = new(); - readonly List _framePool = new(); // 弹帧回收池:Slots 容量只增,出租时 Array.Clear 复位 - readonly TaggedValue[] _globals; - readonly EcxNativeContext _nativeCtx; - CancellationToken _token; // 当前 Run 的取消令牌(Step 装载) - int _steps; - int _budget = 1_000_000; - - public int ErrorFunc { get; private set; } = -1; - public int ErrorPc { get; private set; } = -1; - - // ---- 堆引用计数统计(internal 只读,测试/诊断观测)---- - internal int HeapLiveCount { get; private set; } // 非空槽位数(存活对象) - internal int HeapHighWater { get; private set; } // 存活峰值 - internal long HeapAllocTotal { get; private set; } - internal long HeapReleaseTotal { get; private set; } - internal int HeapFreeListCount => _freeSlots.Count; - - /// 计数自洽性:alloc − release = live = 非空槽位数(槽 0 为 null 哨兵),且无负计数。 - internal bool HeapCountsConsistent - { - get - { - if (HeapAllocTotal - HeapReleaseTotal != HeapLiveCount) - return false; - int live = 0; - for (int i = 1; i < _heap.Count; i++) - if (_heap[i] is not null) - live++; - if (live != HeapLiveCount || live != _heap.Count - 1 - _freeSlots.Count) - return false; - foreach (var rc in _refCounts) - if (rc < 0) - return false; - return true; - } - } - - /// 入口函数(<main>/$eval)的返回值(顶层 RETURN;无返回值为 Void)。 - public TaggedValue Result { get; private set; } - - public const int OK = 0; - public const int YIELD = 1; - public const int CANCELLED = 2; - // 错误码 ABI 与 native/ecs_vm.h 结果码枚举一致(VmHeaderContractTests 锁定头文件侧) - public const int ERR_IMAGE = 3; - public const int ERR_OPCODE = 4; - public const int ERR_SLOT = 5; - public const int ERR_TYPE = 6; - public const int ERR_INDEX = 7; - public const int ERR_DIVZERO = 8; - public const int ERR_DEPTH = 9; - public const int ERR_NOSUCHNATIVE = 10; - public const int ERR_HOST = 11; - public const int ERR_OOM = 12; - public const int ERR_IL = 13; - public const int ERR_FEAT = 14; - - /// 调用深度上限(与 C 侧 ECS_MAX_CALL_DEPTH 一致:深递归双端同样报 ERR_DEPTH, - /// 避免同一镜像在 MCU 报错而 PC 端 OOM 的对拍分裂)。帧为每函数栈窗口(典型高水位 - /// 20–40 槽 ≈ 0.3–0.6KB),64 深度在 C VM 帧段(128KB)内余量充足;深递归由尾递归消除(TRE)兜底。 - public const int MaxCallDepth = 64; - - EcxInterpreter(EcxImage image, EcxHost host) - { - _image = image; - _host = host; - _globals = new TaggedValue[image.Globals.Count]; - _heap.Add(null); // 句柄 0 = null - _refCounts.Add(0); // 与 _heap 平行(哨兵槽位) - _nativeCtx = new NativeContext(this); - } + readonly InstructionExecutor _executor; + + EcxInterpreter(InstructionExecutor executor) => _executor = executor; + + public int ErrorFunc => _executor.ErrorFunc; + public int ErrorPc => _executor.ErrorPc; + public TaggedValue Result => _executor.Result; + internal int HeapLiveCount => _executor.HeapLiveCount; + internal int HeapHighWater => _executor.HeapHighWater; + internal long HeapAllocTotal => _executor.HeapAllocTotal; + internal long HeapReleaseTotal => _executor.HeapReleaseTotal; + internal int HeapFreeListCount => _executor.HeapFreeListCount; + internal bool HeapCountsConsistent => _executor.HeapCountsConsistent; + internal static bool CopyOnWrite + { + get => InstructionExecutor.CopyOnWrite; + set => InstructionExecutor.CopyOnWrite = value; + } + + public const int OK = InstructionExecutor.OK; + public const int YIELD = InstructionExecutor.YIELD; + public const int CANCELLED = InstructionExecutor.CANCELLED; + public const int ERR_IMAGE = InstructionExecutor.ERR_IMAGE; + public const int ERR_OPCODE = InstructionExecutor.ERR_OPCODE; + public const int ERR_SLOT = InstructionExecutor.ERR_SLOT; + public const int ERR_TYPE = InstructionExecutor.ERR_TYPE; + public const int ERR_INDEX = InstructionExecutor.ERR_INDEX; + public const int ERR_DIVZERO = InstructionExecutor.ERR_DIVZERO; + public const int ERR_DEPTH = InstructionExecutor.ERR_DEPTH; + public const int ERR_NOSUCHNATIVE = InstructionExecutor.ERR_NOSUCHNATIVE; + public const int ERR_HOST = InstructionExecutor.ERR_HOST; + public const int ERR_OOM = InstructionExecutor.ERR_OOM; + public const int ERR_IL = InstructionExecutor.ERR_IL; + public const int ERR_FEAT = InstructionExecutor.ERR_FEAT; + public const int MaxCallDepth = InstructionExecutor.MaxCallDepth; public static int Run(EcxImage image, EcxHost host, CancellationToken token = default) => Run(image, host, out _, out _, out _, out _, token); - /// 执行并取回入口函数返回值(顶层 RETURN 语义,与金标准 SsaEvaluator.Evaluate 一致)。 public static int Run(EcxImage image, EcxHost host, out Value result, CancellationToken token = default) => Run(image, host, out _, out _, out _, out result, token); - /// 执行镜像直到完成/取消/错误;出错时经 errorFunc/errorPc 报告现场。 public static int Run(EcxImage image, EcxHost host, out int errorFunc, out int errorPc, CancellationToken token = default) => Run(image, host, out _, out errorFunc, out errorPc, out _, token); - /// 执行并返回解释器实例(堆引用计数统计观测用;独立命名避免与 out Value 重载二义)。 - public static int RunWithVm(EcxImage image, EcxHost host, out EcxInterpreter vm, CancellationToken token = default) - { - vm = new EcxInterpreter(image, host); - int code; - while (true) - { - code = vm.Step(token); - if (code != YIELD) - break; - } - return code; - } + public static int Run(EcxImage image, EcxHost host, out TaggedValue rawResult, out int errorFunc, + out int errorPc, out Value result, CancellationToken token = default) + => image.Target == ScriptTarget.Desktop + ? PcInterpreter.Run(image, host, out rawResult, out errorFunc, out errorPc, out result, token) + : InstructionExecutor.Run(image, host, out rawResult, out errorFunc, out errorPc, out result, token); - /// 执行镜像直到完成/取消/错误;返回错误码、入口返回值与出错现场(与 C VM 对齐)。 - public static int Run(EcxImage image, EcxHost host, out TaggedValue rawResult, out int errorFunc, out int errorPc, out Value result, CancellationToken token = default) + /// 保留原有诊断观测接口,两种目标都使用同一引用计数语义。 + public static int RunWithVm(EcxImage image, EcxHost host, out EcxInterpreter vm, CancellationToken token = default) { - var vm = new EcxInterpreter(image, host); - int code; - while (true) - { - code = vm.Step(token); - if (code != YIELD) - break; - } - rawResult = vm.Result; - result = vm.ToPublicValue(vm.Result); - errorFunc = vm.ErrorFunc; - errorPc = vm.ErrorPc; + int code = InstructionExecutor.RunWithVm(image, host, out InstructionExecutor executor, token); + vm = new EcxInterpreter(executor); return code; } - - // ---- 堆 ---- - - /// 分配堆槽位:自由链优先复用下标(句柄 = 列表下标,释放 = 对象置 null + 下标入自由链, - /// 恒不移除/移动元素)。出生引用 rc=1 归属接收槽(调用方以 StoreFresh 直写,不再 retain)。 - int Store(object obj) - { - HeapAllocTotal++; - int h; - if (_freeSlots.Count > 0) - { - h = _freeSlots[^1]; - _freeSlots.RemoveAt(_freeSlots.Count - 1); - _heap[h] = obj; - _refCounts[h] = 1; - } - else - { - h = _heap.Count; - _heap.Add(obj); - _refCounts.Add(1); - } - HeapLiveCount++; - if (HeapLiveCount > HeapHighWater) - HeapHighWater = HeapLiveCount; - return h; - } - - /// 静态(pinned)常量串的数据源(S-20):直读镜像常量池,零拷贝零分配(对齐 C 侧 str_or_null)。 - string? StaticStr(int kx) => kx >= 0 && kx < _image.Consts.Count ? _image.Consts[kx].Str ?? "" : null; - - string Str(int h) => h < 0 ? StaticStr(h & 0x7FFFFFFF) ?? "" : h > 0 && _heap[h] is string s ? s : throw new SimError(ERR_TYPE, "解引用非字符串句柄"); - string? StrOrNull(int h) => h < 0 ? StaticStr(h & 0x7FFFFFFF) : h > 0 && _heap[h] is string s ? s : null; - SimArray Arr(int h) => _heap[h] as SimArray ?? throw new SimError(ERR_TYPE, "解引用非数组句柄"); - SimStruct St(int h) => _heap[h] as SimStruct ?? throw new SimError(ERR_TYPE, "解引用非结构体句柄"); - - TaggedValue NewString(string s) => TaggedValue.FromStringHandle(Store(s)); - - /// retain(对齐 ecs_vm.c):句柄值引用 +1;null 哨兵/已释放槽静默容忍(Str/Arr/St 守卫同源)。 - void Retain(in TaggedValue v) - { - if (!v.IsHandle) - return; - var h = v.Handle; - if (h > 0 && h < _heap.Count && _heap[h] is not null) - _refCounts[h]++; - } - - /// release:计数归零时递归释放子引用,随后对象置 null 并入自由链(下标可复用)。 - /// 重复释放已释放槽位为 no-op(对齐 C VM heap_obj NULL 守卫)。 - void Release(in TaggedValue v) - { - if (!v.IsHandle) - return; - ReleaseHandle(v.Handle); - } - - void ReleaseHandle(int h) - { - if (h <= 0 || h >= _heap.Count) - return; - var obj = _heap[h]; - if (obj is null) - return; - if (--_refCounts[h] > 0) - return; - switch (obj) - { - case SimArray arr: - for (int i = 0; i < arr.Items.Count; i++) - Release(arr.Items[i]); - break; - case SimStruct st: - for (int i = 0; i < st.Slots.Length; i++) - Release(st.Slots[i]); - // 槽位残留句柄清零:结构体可能经 ViewParent 被后死的视图回写, - // 清零使回写路径的 release/retain 落在 Void 上(句柄复用后不误伤新对象) - Array.Clear(st.Slots); - break; - } - _heap[h] = null; - _refCounts[h] = 0; - _freeSlots.Add(h); - HeapLiveCount--; - HeapReleaseTotal++; - } - - /// 【槽写规则】任何帧槽 / 容器元素 / 结构体槽的写入必须经过 - /// / / / - /// 之一(裸写 = 跳过旧值释放,破坏 RC 双端锁步)。 - /// 唯一例外:全新分配、尚无旧值的槽区(如新帧实参槽)可直接写。 - /// 槽位覆写(move_to 语义,对齐 ecs_vm.c):旧值引用释放;新值为其他活跃槽位的借用 → retain 补引用。 - /// 同柄自赋值(槽位与新值同句柄)为 no-op:槽位已持有引用,先释放会悬垂。 - void MoveToSlot(ref TaggedValue slot, TaggedValue v) - { - if (v.IsHandle && slot.IsHandle && v.Handle == slot.Handle) - return; - Release(slot); - slot = v; - Retain(v); - } - - /// 槽位写入「出生引用」值(Store 新建对象 / DeepCopy 结果 / 标量):仅释放旧值—— - /// 出生引用(Store 时 rc=1 / DeepCopy 的 retain)即本槽位的引用。 - void StoreFresh(ref TaggedValue slot, TaggedValue v) - { - Release(slot); - slot = v; - } - - /// 容器元素覆写(SetI 语义,对齐 ecs_vm.c):旧元素释放,写入值 retain(借用补引用)。 - void OverwriteItem(List items, int idx, TaggedValue v) - { - var old = items[idx]; - if (v.IsHandle && old.IsHandle && v.Handle == old.Handle) - return; - Release(old); - Retain(v); - items[idx] = v; - } - - /// 弹帧并释放被弹帧槽位持有的全部句柄(Ret/Ret0,对齐 ecs_vm.c 帧退出语义); - /// 帧体回收进池(出租时复位)。 - void PopFrame(Frame frame) - { - _frames.RemoveAt(_frames.Count - 1); - var slots = frame.Slots; - for (int i = 0; i < slots.Length; i++) - Release(slots[i]); - _framePool.Add(frame); - } - - /// S-01 深拷贝:数组/结构体一层新容器(出生引用,子项 retain);字符串共享(retain 补接收槽引用)。 - /// 返回值恰携带一个接收槽引用,接收方以 StoreFresh 直写槽位。 - TaggedValue DeepCopy(TaggedValue v) - { - if (!v.IsHandle) - return v; - switch (v.Tag) - { - case EcsTag.Array: - { - var src = Arr(v.Handle); - var copy = new SimArray { ElemTag = src.ElemTag, Items = new List(src.Items.Count) }; - for (int i = 0; i < src.Items.Count; i++) - { - copy.Items.Add(src.Items[i]); - Retain(src.Items[i]); - } - return TaggedValue.FromArrayHandle(Store(copy)); - } - case EcsTag.Struct: - { - var src = St(v.Handle); - var copy = new SimStruct { Layout = src.Layout, Slots = (TaggedValue[])src.Slots.Clone() }; - for (int i = 0; i < copy.Slots.Length; i++) - Retain(copy.Slots[i]); - return TaggedValue.FromStructHandle(Store(copy)); - } - default: - Retain(v); // 字符串共享 - return v; - } - } - - /// COW 开关(运行时常量;ECS_COW=0 关闭,供全量测试矩阵双态验证)。 - internal static bool CopyOnWrite { get; set; } = Environment.GetEnvironmentVariable("ECS_COW") != "0"; - - /// COW(move-on-unique,Q1-A):句柄唯一引用(rc==1,恒为出生临时槽持有)时共享移交, - /// 跳过容器深拷贝;否则维持 S-17 深拷贝。可观察值语义逐字不变——变量/全局槽永不与另一 - /// 变量/全局共享容器(共享只发生在「出生临时槽 ↔ 接收槽」之间,临时槽对程序不可见)。 - TaggedValue DeepCopyCopyOnWrite(TaggedValue v) - { - if (CopyOnWrite && v.IsHandle) - { - var h = v.Handle; - if (h > 0 && h < _heap.Count && _heap[h] is not null && _refCounts[h] == 1) - { - Retain(v); // 接收槽补引用;出生临时槽的既持引用随其消亡释放 - return v; - } - } - return DeepCopy(v); - } - - // ---- TOSTR 两层上下文(S-07/S-08)---- - - string ToStringTop(TaggedValue v) - { - switch (v.Tag) - { - case EcsTag.Bool: return v.I32 != 0 ? "true" : "false"; - case EcsTag.Byte: - case EcsTag.Int: - case EcsTag.UInt: - return v.I32.ToString(); - case EcsTag.UInt64: return unchecked((ulong)v.I64).ToString(); - case EcsTag.Ptr: return v.I64.ToString(); - case EcsTag.Double: return v.F64.ToString(); - case EcsTag.String: return StrOrNull(v.Handle) ?? ""; - case EcsTag.Array: - case EcsTag.Struct: return ToStringNested(v); - default: return ""; - } - } - - string ToStringNested(TaggedValue v) - { - switch (v.Tag) - { - case EcsTag.Array: - { - var arr = Arr(v.Handle); - var sb = new StringBuilder("["); - for (int i = 0; i < arr.Items.Count; i++) - { - if (i > 0) sb.Append(", "); - sb.Append(ToStringNested(arr.Items[i])); - } - sb.Append(']'); - return sb.ToString(); - } - case EcsTag.Struct: - return $"struct:{St(v.Handle).Layout.Name}"; - case EcsTag.Ptr: - return $"0x{v.I64:X}"; - case EcsTag.Bool: return v.I32 != 0 ? "true" : "false"; - case EcsTag.UInt64: return unchecked((ulong)v.I64).ToString(); - case EcsTag.String: return StrOrNull(v.Handle) ?? ""; - case EcsTag.Double: return v.F64.ToString(); - case EcsTag.Void: return "void"; - default: return v.I32.ToString(); - } - } - - // ---- 主循环 ---- - - /// 帧出租(每函数栈窗口):帧长 = 函数寄存器高水位(NSlots ≤255), - /// 池空新建;复用时槽位 Array.Clear 复位为 Void(StoreFresh「无旧值」直写契约), - /// 容量不足则扩容(容量只增,峰值深度即池上限——池随弹帧回收)。 - Frame RentFrame(int nslots) - { - if (_framePool.Count > 0) - { - var f = _framePool[^1]; - _framePool.RemoveAt(_framePool.Count - 1); - if (f.Slots.Length < nslots) - f.Slots = new TaggedValue[nslots]; // 新数组自带零值 - else - Array.Clear(f.Slots); - return f; - } - return new Frame { Slots = new TaggedValue[nslots] }; - } - - /// 常量池串 → 静态(pinned)值(契约 S-20 / docs/ZeroAllocVm.md §2):**不再新建堆对象**, - /// 直接返回指向镜像常量池的句柄(bit31 置位);不参与 RC,因此无需驻留表、无需补引用。 - /// 语义不变:串不可变、EqS/Cont/GetI/Len/Cat/TOSTR 一律按内容处理 - /// ( / 同时服务动态与静态句柄)。 - TaggedValue InternedString(int kx) => TaggedValue.FromStaticString(kx); - - int Step(CancellationToken token) - { - _token = token; - if (_frames.Count == 0) - { - var fn = _image.Functions[_image.Entry]; - var f = RentFrame(fn.NSlots); - f.Fn = fn; - f.Pc = 0; - f.RetSlot = -1; - _frames.Add(f); - } - if (_token.IsCancellationRequested) - return CANCELLED; - - // 预算计数局部化:字段读改写 → JIT 寄存器驻留(每指令省 2 次内存访问; - // 预算值 Run 期内不变、_steps 仅本方法读写,回写点 = 切片边界) - var steps = _steps; - var budget = _budget; - // 取指通路局部化(P0b):frame/指令流 span/R/pc 提升为循环局部,JIT 可驻留寄存器, - // 每指令省 4 次 _frames/frame 字段链解引用。不变量:指令流运行期只读(链接后不可变), - // span 恒指向当前帧函数的指令列表;换帧点(Call/Ret/Ret0)置 frameSwitched 由循环头重载; - // 切片出口(YIELD/宿主后 CANCELLED)写回 frame.Pc——Run 吸收 YIELD 后重入 Step 续跑。 - Frame frame = null!; - ReadOnlySpan code = default; - TaggedValue[] R = null!; - int pc = 0; - bool frameSwitched = true; - while (true) - { - if (frameSwitched) - { - frame = _frames[^1]; - code = CollectionsMarshal.AsSpan(frame.Fn.Instructions); - R = frame.Slots; - pc = frame.Pc; - frameSwitched = false; - } - - // 取消检查合并:每指令只做预算计数(1 分支);token 在入口、预算边界与宿主调用后检查 - // (纯计算最坏 1M 步 ≈ 十几毫秒感知;等待/按键型脚本在宿主调用返回后立即感知) - if (++steps >= budget) - { - frame.Pc = pc; - _steps = 0; - if (_token.IsCancellationRequested) - return CANCELLED; - return YIELD; - } - - int instructionPc = pc; - // 单一分派臂(SingleStreamFormat §2):解码形态是唯一指令表示, - // 不再有 Code/PcCode 双流与宽窄两条取指路径。 - var ins = code[pc++]; - var op = ins.Op; - int a = ins.A; - int b = ins.B; - int c = ins.C; - uint ext = ins.Ext; - - try - { - switch (op) - { - case EcsOpcode.Nop: break; - - // ---- 常量与移动 ---- - case EcsOpcode.LoadI: - StoreFresh(ref R[a], TaggedValue.FromInt(b)); - break; - case EcsOpcode.LoadK: - { - int kx = b; - var k = _image.Consts[kx]; - if (k.Tag == EcsTag.String) - MoveToSlot(ref R[a], InternedString(kx)); // 驻留串:借用 + retain,槽旧值照常释放 - else - StoreFresh(ref R[a], k.Tag switch - { - EcsTag.Double => TaggedValue.FromDouble(k.Float64), - EcsTag.UInt64 => TaggedValue.FromUInt64(unchecked((ulong)k.Int64)), - EcsTag.Ptr => TaggedValue.FromPtr(k.Int64), - EcsTag.UInt => TaggedValue.FromUInt(unchecked((uint)k.Int64)), - _ => TaggedValue.FromInt((int)k.Int64), - }); - break; - } - case EcsOpcode.LoadBool: - StoreFresh(ref R[a], TaggedValue.FromBool(b != 0)); - break; - case EcsOpcode.Move: - MoveToSlot(ref R[a], R[b]); - break; - case EcsOpcode.SetVar: - StoreFresh(ref R[a], DeepCopyCopyOnWrite(R[b])); - break; - case EcsOpcode.LoadG: - MoveToSlot(ref R[a], _globals[b]); - break; - case EcsOpcode.StoreG: - { - var nv = DeepCopyCopyOnWrite(R[a]); - int gx = b; - Release(_globals[gx]); - _globals[gx] = nv; - break; - } - - case EcsOpcode.Jmp: - pc += ins.Jump; - break; - case EcsOpcode.Jpt: - pc += R[a].I32 != 0 ? ins.Jump : 0; - break; - case EcsOpcode.Jpf: - pc += R[a].I32 == 0 ? ins.Jump : 0; - break; - - // ---- 比较跳转融合(P2′):R[a] kind R[b] 成立 → 跳 Jump;否则落入后继 ---- - case EcsOpcode.CmpJ: - { - bool taken; - switch (c / 6) - { - case 0: { int l = R[a].I32, r = R[b].I32; taken = (c % 6) switch { 0 => l == r, 1 => l != r, 2 => l < r, 3 => l <= r, 4 => l > r, _ => l >= r }; break; } - case 1: { uint l = (uint)R[a].I32, r = (uint)R[b].I32; taken = (c % 6) switch { 0 => l == r, 1 => l != r, 2 => l < r, 3 => l <= r, 4 => l > r, _ => l >= r }; break; } - case 2: { double l = R[a].F64, r = R[b].F64; taken = (c % 6) switch { 0 => l == r, 1 => l != r, 2 => l < r, 3 => l <= r, 4 => l > r, _ => l >= r }; break; } - default: { long l = R[a].I64, r = R[b].I64; taken = (c % 6) switch { 0 => l == r, 1 => l != r, 2 => l < r, 3 => l <= r, 4 => l > r, _ => l >= r }; break; } - } - if (taken) - pc += ins.Jump; - break; - } - - // ---- FOR 快速路径:tmp=i+1 写入 R[c];tmp>limit → 跳 Jump(出循环,$i 保持 ==upper);否则落入后继 ---- - // 信任字节码:跳转目标不校验 - case EcsOpcode.ForStep: - { - int next = R[a].I32 + 1; - StoreFresh(ref R[c], TaggedValue.FromInt(next)); - if (next > R[b].I32) - pc = instructionPc + 1 + ins.Jump; - break; - } - - // ---- 调用 ---- - // 每函数栈窗口帧:实参从调用方 STAGING(A)深拷贝播种进被调方 - // R1 起的参数窗(S-17 语义);C = 接收寄存器(0 = RET 固定落点) - case EcsOpcode.Call: - { - uint target = ext; - var callee = _image.Functions[(int)target]; - if (_frames.Count >= MaxCallDepth) - throw new SimError(ERR_DEPTH, $"调用深度超过上限 {MaxCallDepth}"); - var nf = RentFrame(callee.NSlots); - nf.Fn = callee; - nf.RetSlot = c; - nf.Pc = 0; - for (int i = 0; i < b; i++) - StoreFresh(ref nf.Slots[EcsRegisters.ArgBase + i], DeepCopyCopyOnWrite(R[a + i])); // S-17 实参(COW:唯一引用移交) - frame.Pc = pc; // 调用者续跑点写回(Ret 时经重载读回) - _frames.Add(nf); - frameSwitched = true; - break; - } - case EcsOpcode.CallN: - { - uint target = ext; - var args = new TaggedValue[b]; // 实参按值快照:原生看不见帧槽 - for (int i = 0; i < b; i++) - args[i] = R[a + i]; - TaggedValue ret; - if ((target & EcsSyscall.CallFlag) != 0) - { - // L2:编号 syscall,语义在宿主参考实现(VM 核纯调度) - int id = (int)(target & 0x7FFFFFFFu); - var resolved = _host.Syscall?.Invoke(id, args, _nativeCtx); - if (resolved is null) - { - // S-21:miss → 缺省值表降级;strict 或表外编号 → 响亮 - resolved = EcsCapabilityDefaults.GetSyscallDefault(id, args, _nativeCtx); - if (resolved is null || _host.StrictCaps) - throw new SimError(ERR_NOSUCHNATIVE, $"syscall 未实现: 编号 {id}"); - } - ret = resolved.Value; - } - else - { - ret = HostNative(_image.Natives[(int)target].Name, args); // L3 名表路径:FFI/采集洞/ENCODE/JQ(信任字节码,目标不校验) - } - if (c >= 0) - StoreFresh(ref R[c], ret); // 接收寄存器(0 = RET 固定落点);原生返回值恒为新建对象/标量(EcxNativeContext 契约),出生引用即接收槽引用 - if (_token.IsCancellationRequested) - { - frame.Pc = pc; - return CANCELLED; // 宿主调用后即时取消(原生内含 PRINT/READ/WAIT 类长延迟) - } - break; - } - case EcsOpcode.Ret: - { - var val = R[a]; - if (_frames.Count == 1) - { - Result = DeepCopyCopyOnWrite(val); // 入口返回值(顶层 RETURN):Result 持独立引用,跨弹帧存活 - PopFrame(frame); - return OK; - } - TaggedValue copy = DeepCopyCopyOnWrite(val); // S-17 返回(COW:唯一引用移交;先于弹帧:val 所属槽位将被释放) - int retSlot = frame.RetSlot; - PopFrame(frame); - StoreFresh(ref _frames[^1].Slots[retSlot], copy); - frameSwitched = true; - break; - } - case EcsOpcode.Ret0: - PopFrame(frame); - if (_frames.Count == 0) - return OK; - frameSwitched = true; - break; - - default: - if (ExecOther(op, a, b, c, ext, R)) - { - frame.Pc = pc; - return CANCELLED; - } - break; - } - } - catch (SimError e) - { - if (e.Code == OK) - return OK; // Halt 正常停机:不留错误现场(与 C VM 对齐) - ErrorFunc = _image.Functions.IndexOf(frame.Fn); - ErrorPc = instructionPc; - return e.Code; - } - } - } - - /// 非控制流/非调用的算术与数据指令。返回 true = 本指令调用了宿主且取消已请求, - /// 调用方立即终止(等待/按键类长延迟宿主调用的取消感知点)。 - bool ExecOther(EcsOpcode op, int a, int b, int c, uint ext, TaggedValue[] R) - { - switch (op) - { - // ---- 算术(S-02/S-05)---- - case EcsOpcode.AddI: StoreFresh(ref R[a], TaggedValue.FromInt(R[b].I32 + R[c].I32)); break; - case EcsOpcode.SubI: StoreFresh(ref R[a], TaggedValue.FromInt(R[b].I32 - R[c].I32)); break; - case EcsOpcode.MulI: StoreFresh(ref R[a], TaggedValue.FromInt(R[b].I32 * R[c].I32)); break; - case EcsOpcode.DivI: RequireDivisor(R[c]); StoreFresh(ref R[a], TaggedValue.FromInt(R[b].I32 / R[c].I32)); break; - case EcsOpcode.ModI: RequireDivisor(R[c]); StoreFresh(ref R[a], TaggedValue.FromInt(R[b].I32 % R[c].I32)); break; - case EcsOpcode.RDivI: RequireDivisor(R[c]); StoreFresh(ref R[a], TaggedValue.FromInt((R[b].I32 + R[c].I32 / 2) / R[c].I32)); break; - - case EcsOpcode.AddU: StoreFresh(ref R[a], TaggedValue.FromUInt(unchecked((uint)R[b].I32 + (uint)R[c].I32))); break; - case EcsOpcode.SubU: StoreFresh(ref R[a], TaggedValue.FromUInt(unchecked((uint)R[b].I32 - (uint)R[c].I32))); break; - case EcsOpcode.MulU: StoreFresh(ref R[a], TaggedValue.FromUInt(unchecked((uint)R[b].I32 * (uint)R[c].I32))); break; - case EcsOpcode.DivU: RequireDivisor(R[c]); StoreFresh(ref R[a], TaggedValue.FromUInt((uint)R[b].I32 / (uint)R[c].I32)); break; - case EcsOpcode.ModU: RequireDivisor(R[c]); StoreFresh(ref R[a], TaggedValue.FromUInt((uint)R[b].I32 % (uint)R[c].I32)); break; - - case EcsOpcode.AddL: StoreFresh(ref R[a], TaggedValue.FromUInt64(unchecked((ulong)R[b].I64 + (ulong)R[c].I64))); break; - case EcsOpcode.SubL: StoreFresh(ref R[a], TaggedValue.FromUInt64(unchecked((ulong)R[b].I64 - (ulong)R[c].I64))); break; - case EcsOpcode.MulL: StoreFresh(ref R[a], TaggedValue.FromUInt64(unchecked((ulong)R[b].I64 * (ulong)R[c].I64))); break; - case EcsOpcode.DivL: RequireDivisor(R[c]); StoreFresh(ref R[a], TaggedValue.FromUInt64((ulong)R[b].I64 / (ulong)R[c].I64)); break; - case EcsOpcode.ModL: RequireDivisor(R[c]); StoreFresh(ref R[a], TaggedValue.FromUInt64((ulong)R[b].I64 % (ulong)R[c].I64)); break; - - case EcsOpcode.AddD: StoreFresh(ref R[a], TaggedValue.FromDouble(R[b].F64 + R[c].F64)); break; - case EcsOpcode.SubD: StoreFresh(ref R[a], TaggedValue.FromDouble(R[b].F64 - R[c].F64)); break; - case EcsOpcode.MulD: StoreFresh(ref R[a], TaggedValue.FromDouble(R[b].F64 * R[c].F64)); break; - case EcsOpcode.DivD: StoreFresh(ref R[a], TaggedValue.FromDouble(R[b].F64 / R[c].F64)); break; - - // ---- 位运算(S-03 掩码)---- - case EcsOpcode.BandI: StoreFresh(ref R[a], TaggedValue.FromInt(R[b].I32 & R[c].I32)); break; - case EcsOpcode.BorI: StoreFresh(ref R[a], TaggedValue.FromInt(R[b].I32 | R[c].I32)); break; - case EcsOpcode.BxorI: StoreFresh(ref R[a], TaggedValue.FromInt(R[b].I32 ^ R[c].I32)); break; - case EcsOpcode.ShlI: StoreFresh(ref R[a], TaggedValue.FromInt(R[b].I32 << (R[c].I32 & 31))); break; - case EcsOpcode.ShrI: StoreFresh(ref R[a], TaggedValue.FromInt(R[b].I32 >> (R[c].I32 & 31))); break; - case EcsOpcode.BnotI: StoreFresh(ref R[a], TaggedValue.FromInt(~R[b].I32)); break; - - // ---- 比较 ---- - // 比较族直写(P0):委托间接调用 + CompareTo 是热点路径纯开销; - // 浮点直接用 C# 比较符(NaN → false),与 C VM 的 C 语义一致 - //(原 CmpD 的 CompareTo 把 NaN 排在最前,与 C VM 存在潜在分歧) - case EcsOpcode.EqI: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].I32 == R[c].I32)); break; - case EcsOpcode.LtI: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].I32 < R[c].I32)); break; - case EcsOpcode.LeI: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].I32 <= R[c].I32)); break; - case EcsOpcode.GtI: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].I32 > R[c].I32)); break; - case EcsOpcode.GeI: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].I32 >= R[c].I32)); break; - case EcsOpcode.EqU: StoreFresh(ref R[a], TaggedValue.FromBool((uint)R[b].I32 == (uint)R[c].I32)); break; - case EcsOpcode.LtU: StoreFresh(ref R[a], TaggedValue.FromBool((uint)R[b].I32 < (uint)R[c].I32)); break; - case EcsOpcode.LeU: StoreFresh(ref R[a], TaggedValue.FromBool((uint)R[b].I32 <= (uint)R[c].I32)); break; - case EcsOpcode.GtU: StoreFresh(ref R[a], TaggedValue.FromBool((uint)R[b].I32 > (uint)R[c].I32)); break; - case EcsOpcode.GeU: StoreFresh(ref R[a], TaggedValue.FromBool((uint)R[b].I32 >= (uint)R[c].I32)); break; - case EcsOpcode.EqD: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].F64 == R[c].F64)); break; - case EcsOpcode.LtD: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].F64 < R[c].F64)); break; - case EcsOpcode.LeD: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].F64 <= R[c].F64)); break; - case EcsOpcode.GtD: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].F64 > R[c].F64)); break; - case EcsOpcode.GeD: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].F64 >= R[c].F64)); break; - case EcsOpcode.EqL: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].I64 == R[c].I64)); break; - case EcsOpcode.LtL: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].I64 < R[c].I64)); break; - case EcsOpcode.LeL: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].I64 <= R[c].I64)); break; - case EcsOpcode.GtL: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].I64 > R[c].I64)); break; - case EcsOpcode.GeL: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].I64 >= R[c].I64)); break; - case EcsOpcode.EqS: StoreFresh(ref R[a], TaggedValue.FromBool(string.Equals(StrOrNull(R[b].Handle) ?? "", StrOrNull(R[c].Handle) ?? "", StringComparison.Ordinal))); break; - case EcsOpcode.EqP: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].I64 == R[c].I64)); break; - - case EcsOpcode.Not: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].I32 == 0)); break; - case EcsOpcode.NegI: StoreFresh(ref R[a], TaggedValue.FromInt(-R[b].I32)); break; - case EcsOpcode.NegD: StoreFresh(ref R[a], TaggedValue.FromDouble(-R[b].F64)); break; - case EcsOpcode.Conv: StoreFresh(ref R[a], Conv((EcsConvKind)c, R[b])); break; - - // ---- 数组/字符串 ---- - case EcsOpcode.NewArrV: - { - var arr = new SimArray { ElemTag = (byte)ext }; - for (int i = 0; i < b; i++) - { - arr.Items.Add(R[c + i]); - Retain(R[c + i]); // 新容器持有元素引用 - } - StoreFresh(ref R[a], TaggedValue.FromArrayHandle(Store(arr))); - break; - } - case EcsOpcode.NewArrE: // ABx:元素类型码在 Bx - StoreFresh(ref R[a], TaggedValue.FromArrayHandle(Store(new SimArray { ElemTag = (byte)b }))); - break; - case EcsOpcode.GetI: - { - var container = R[b]; - var idx = R[c].I32; - if (container.IsString) - { - var s = StrOrNull(container.Handle) ?? ""; - if (idx < 0 || idx >= s.Length) throw new SimError(ERR_INDEX, $"字符串索引越界 {idx}/{s.Length}"); - StoreFresh(ref R[a], NewString(s[idx].ToString())); - } - else - { - var arr = Arr(container.Handle); - if (idx < 0 || idx >= arr.Items.Count) throw new SimError(ERR_INDEX, $"数组索引越界 {idx}/{arr.Items.Count}"); - var item = CoerceRead(arr.ElemTag, arr.Items[idx]); - Retain(item); // S-10 读共享:接收槽补引用 - StoreFresh(ref R[a], item); - } - break; - } - case EcsOpcode.SetI: - { - var arr = Arr(R[b].Handle); - var idx = R[c].I32; - if (idx < 0 || idx >= arr.Items.Count) throw new SimError(ERR_INDEX, $"数组索引越界 {idx}/{arr.Items.Count}"); - OverwriteItem(arr.Items, idx, CoerceWrite(arr.ElemTag, R[a])); - break; - } - case EcsOpcode.Slice: - { - var container = R[b]; - int start = R[c].I32; - int end; - if (container.IsString) - { - var s = StrOrNull(container.Handle) ?? ""; - end = ext == 0xFFFFFFFF ? s.Length : R[(int)ext].I32; - if (start > s.Length || end > s.Length || start > end) throw new SimError(ERR_INDEX, "切片越界"); - StoreFresh(ref R[a], NewString(s[start..end])); - } - else - { - var arr = Arr(container.Handle); - end = ext == 0xFFFFFFFF ? arr.Items.Count : R[(int)ext].I32; - if (start > arr.Items.Count || end > arr.Items.Count || start > end) throw new SimError(ERR_INDEX, "切片越界"); - var sliced = new SimArray { ElemTag = arr.ElemTag, Items = arr.Items.GetRange(start, end - start) }; - for (int i = 0; i < sliced.Items.Count; i++) - Retain(sliced.Items[i]); // 新容器持有元素引用 - StoreFresh(ref R[a], TaggedValue.FromArrayHandle(Store(sliced))); - } - break; - } - case EcsOpcode.Cont: - { - var item = R[b]; - var container = R[c]; - if (container.IsString) - { - var s = StrOrNull(container.Handle) ?? ""; - var sub = StrOrNull(item.Handle) ?? throw new SimError(ERR_TYPE, "in 运算类型不匹配"); - StoreFresh(ref R[a], TaggedValue.FromBool(s.Contains(sub, StringComparison.Ordinal))); - } - else if (container.IsArray) - { - var arr = Arr(container.Handle); - if (item.Tag != arr.ElemTag) - { - StoreFresh(ref R[a], TaggedValue.FromBool(false)); // S-09 元素类型不匹配 → false - } - else - { - bool found = false; - foreach (var it in arr.Items) - if (ValueEquals(it, item)) { found = true; break; } - StoreFresh(ref R[a], TaggedValue.FromBool(found)); - } - } - else - { - throw new SimError(ERR_TYPE, "in 容器类型非法"); - } - break; - } - case EcsOpcode.Append: - { - var src = Arr(R[b].Handle); - var dst = new SimArray { ElemTag = src.ElemTag, Items = new List(src.Items) { R[c] } }; - for (int i = 0; i < dst.Items.Count; i++) - Retain(dst.Items[i]); // 新容器持有元素引用 - StoreFresh(ref R[a], TaggedValue.FromArrayHandle(Store(dst))); - break; - } - case EcsOpcode.Cat: - { - var l = R[b]; - var r = R[c]; - if (l.IsString || r.IsString) - { - StoreFresh(ref R[a], NewString(ToStringTop(l) + ToStringTop(r))); - } - else if (l.IsArray && r.IsArray) - { - var la = Arr(l.Handle); - var ra = Arr(r.Handle); - if (la.ElemTag != ra.ElemTag) throw new SimError(ERR_TYPE, "拼接元素类型不一致"); - var merged = new SimArray { ElemTag = la.ElemTag, Items = new List(la.Items) }; - merged.Items.AddRange(ra.Items); - for (int i = 0; i < merged.Items.Count; i++) - Retain(merged.Items[i]); // 新容器持有元素引用 - StoreFresh(ref R[a], TaggedValue.FromArrayHandle(Store(merged))); - } - else - { - throw new SimError(ERR_TYPE, "CAT 仅支持字符串/数组"); - } - break; - } - case EcsOpcode.Len: - { - var v = R[b]; - if (v.IsString) StoreFresh(ref R[a], TaggedValue.FromInt((StrOrNull(v.Handle) ?? "").Length)); - else if (v.IsArray) StoreFresh(ref R[a], TaggedValue.FromInt(Arr(v.Handle).Items.Count)); - else throw new SimError(ERR_TYPE, "LEN 类型非法"); - break; - } - - // ---- 结构体 ---- - case EcsOpcode.NewSt: // ABx:类型表索引在 Bx - { - var layout = _image.Structs[b]; - var st = new SimStruct { Layout = layout, Slots = new TaggedValue[layout.SlotCount] }; - ZeroFill(layout, st.Slots); - StoreFresh(ref R[a], TaggedValue.FromStructHandle(Store(st))); - break; - } - case EcsOpcode.GetF: - StoreFresh(ref R[a], GetField(R[b], c)); // GetField 返回值恒携带一个接收槽引用 - break; - case EcsOpcode.PutF: PutField(R[b], c, R[a]); break; - case EcsOpcode.GetFI: - { - var st = St(R[b].Handle); - var f = st.Layout.Fields[c]; - if (f.Kind != EcsFieldKind.FixedArray) throw new SimError(ERR_TYPE, "GETFI 仅支持固定数组字段"); - var item = CoerceRead((byte)f.Type, st.Slots[f.SlotOffset + R[(int)ext].I32]); - Retain(item); // 读共享:接收槽补引用(标量 no-op) - StoreFresh(ref R[a], item); - break; - } - case EcsOpcode.PutFI: - { - var st = St(R[b].Handle); - var f = st.Layout.Fields[c]; - if (f.Kind != EcsFieldKind.FixedArray) throw new SimError(ERR_TYPE, "PUTFI 仅支持固定数组字段"); - WriteStructSlot(st, f.SlotOffset + R[(int)ext].I32, CoerceWrite((byte)f.Type, R[a])); - break; - } - - // ---- 域操作(宿主调用:返回取消状态,取消感知点)---- - case EcsOpcode.WaitI: _host.WaitMs(unchecked((int)ext)); return _token.IsCancellationRequested; - case EcsOpcode.WaitV: _host.WaitMs(R[a].I32); return _token.IsCancellationRequested; - case EcsOpcode.KeyI: _host.Key(a, unchecked((int)ext)); return _token.IsCancellationRequested; - case EcsOpcode.KeyV: _host.Key(a, R[b].I32); return _token.IsCancellationRequested; - case EcsOpcode.KeySt: _host.KeyState(a, b); return _token.IsCancellationRequested; - case EcsOpcode.StickSet: _host.StickSet(a, b, c); return _token.IsCancellationRequested; - case EcsOpcode.StickP: - { - _host.StickClick(a, b, c, unchecked((int)ext)); - return _token.IsCancellationRequested; - } - case EcsOpcode.StickPv: - { - _host.StickClick(a, (int)(ext & 0xFF), (int)((ext >> 16) & 0xFF), R[c].I32); - return _token.IsCancellationRequested; - } - case EcsOpcode.Img: // ABx:标签名 = 常量池[Bx](EcsOpcode.cs 注释为权威) - { - if (_host.StrictCaps) - throw new SimError(ERR_IL, $"图像标签能力缺失(strict_caps): {_image.Consts[b].Str}"); - var name = _image.Consts[b].Str ?? ""; - StoreFresh(ref R[a], TaggedValue.FromInt(_host.ImgLabel(name))); - return _token.IsCancellationRequested; - } - case EcsOpcode.Rand: - { - var max = R[b].I32; - if (max < 0) throw new SimError(ERR_INDEX, "RAND 参数为负"); - StoreFresh(ref R[a], TaggedValue.FromInt(max == 0 ? 0 : _host.Rand(max))); - return _token.IsCancellationRequested; - } - - case EcsOpcode.Halt: - throw new SimError(OK, "halt"); - default: - throw new SimError(ERR_OPCODE, $"未实现操作码 {op}"); - } - return false; - } - - void ZeroFill(EcsStructLayout layout, TaggedValue[] slots) - { - foreach (var f in layout.Fields) - { - switch (f.Kind) - { - case EcsFieldKind.FixedArray: - for (int i = 0; i < f.Count; i++) - slots[f.SlotOffset + i] = ZeroOf(f.ElementType); - break; - case EcsFieldKind.NestedStruct: - { - var nested = _image.Structs[f.NestedSid]; - var tmp = new TaggedValue[nested.SlotCount]; - ZeroFill(nested, tmp); - Array.Copy(tmp, 0, slots, f.SlotOffset, nested.SlotCount); - break; - } - default: - slots[f.SlotOffset] = ZeroOf(f.Type); - break; - } - } - } - - static TaggedValue ZeroOf(EcsTypeCode t) => t switch - { - EcsTypeCode.Double => TaggedValue.FromDouble(0), - EcsTypeCode.UInt64 => TaggedValue.FromUInt64(0), - EcsTypeCode.Ptr => TaggedValue.FromPtr(0), - EcsTypeCode.String => TaggedValue.FromStringHandle(0), - _ => TaggedValue.FromInt(0), - }; - - static TaggedValue CoerceRead(byte elemTag, TaggedValue v) - { - if (elemTag == EcsTag.Byte) return TaggedValue.FromByte((byte)v.I32); - if (elemTag == EcsTag.Bool) return TaggedValue.FromBool(v.I32 != 0); - return v; - } - - static TaggedValue CoerceWrite(byte elemTag, TaggedValue v) - { - if (elemTag == EcsTag.Bool) return TaggedValue.FromBool(v.I32 != 0); - return v; - } - - TaggedValue GetField(TaggedValue obj, int fieldIdx) - { - var st = St(obj.Handle); - var f = st.Layout.Fields[fieldIdx]; - switch (f.Kind) - { - case EcsFieldKind.Scalar: - { - var v = CoerceRead((byte)f.Type, st.Slots[f.SlotOffset]); - Retain(v); // 接收槽引用(标量 no-op;字符串字段共享 +1) - return v; - } - case EcsFieldKind.FixedArray: - { - var arr = new SimArray { ElemTag = (byte)f.ElementType, Items = new List(st.Slots[f.SlotOffset..(f.SlotOffset + f.Count)]) }; - for (int i = 0; i < arr.Items.Count; i++) - Retain(arr.Items[i]); // 新容器持有元素引用 - return TaggedValue.FromArrayHandle(Store(arr)); - } - case EcsFieldKind.NestedStruct: - { - var nested = _image.Structs[f.NestedSid]; - // 嵌套视图:切片拷贝 + 回写父槽(写穿透,v1 EcsStruct.GetNested 语义) - var view = new SimStruct - { - Layout = nested, - Slots = st.Slots[f.SlotOffset..(f.SlotOffset + nested.SlotCount)].ToArray(), - ViewParent = st, - ViewOffset = f.SlotOffset, - }; - for (int i = 0; i < view.Slots.Length; i++) - Retain(view.Slots[i]); // 视图按普通堆对象计数:槽区是拷贝快照,视图持有自己的引用 - return TaggedValue.FromStructHandle(Store(view)); - } - default: - throw new SimError(ERR_TYPE, "Boxed 字段访问不受支持"); - } - } - - void PutField(TaggedValue obj, int fieldIdx, TaggedValue value) - { - var st = St(obj.Handle); - var f = st.Layout.Fields[fieldIdx]; - switch (f.Kind) - { - case EcsFieldKind.Scalar: - WriteStructSlot(st, f.SlotOffset, CoerceWrite((byte)f.Type, value)); - break; - case EcsFieldKind.NestedStruct: - { - var src = St(value.Handle); - // src 布局须匹配字段的嵌套类型(而非父结构体总槽数;与 GetF 取嵌套布局一致) - var nested = _image.Structs[f.NestedSid]; - if (src.Slots.Length != nested.SlotCount) - throw new SimError(ERR_TYPE, "嵌套结构体布局不匹配"); - for (int i = 0; i < src.Slots.Length; i++) - MoveToSlot(ref st.Slots[f.SlotOffset + i], src.Slots[i]); - PropagateToParent(st); - break; - } - default: - throw new SimError(ERR_TYPE, "该字段种类不支持整体赋值"); - } - } - - /// 结构体槽写入;嵌套视图沿 ViewParent 链回写父槽区(写穿透,逐槽 release/retain 计数)。 - void WriteStructSlot(SimStruct st, int idx, TaggedValue v) - { - MoveToSlot(ref st.Slots[idx], v); - PropagateToParent(st); - } - - /// 视图槽区整体回写父槽区(逐槽 move_to;同柄槽 no-op 快路径,多数槽无变化)。 - /// 视图槽区整体回写父槽区(逐槽 move_to;同柄槽 no-op 快路径,多数槽无变化)。 - /// 目标偏移 = cur.ViewOffset(对齐改造前 Array.Copy 的写穿透位置)。 - void PropagateToParent(SimStruct view) - { - for (var cur = view; cur.ViewParent is { } parent; cur = parent) - { - var region = cur.Slots; - var parentSlots = parent.Slots; - for (int i = 0; i < region.Length; i++) - MoveToSlot(ref parentSlots[cur.ViewOffset + i], region[i]); - } - } - - int LayoutSlotCount(SimStruct st) => st.Layout.SlotCount; - - TaggedValue Conv(EcsConvKind kind, TaggedValue v) - { - switch (kind) - { - case EcsConvKind.IntToDouble: return TaggedValue.FromDouble(v.I32); - case EcsConvKind.DoubleToInt: return TaggedValue.FromInt(SaturateD2I(v.F64)); - case EcsConvKind.IntToUInt: return TaggedValue.FromUInt(unchecked((uint)v.I32)); - case EcsConvKind.UIntToInt: return TaggedValue.FromInt(v.I32); - case EcsConvKind.IntToByte: return TaggedValue.FromByte((byte)v.I32); - case EcsConvKind.BoolToInt: return TaggedValue.FromInt(v.I32); - case EcsConvKind.IntToUInt64: return TaggedValue.FromUInt64(unchecked((ulong)(long)v.I32)); - case EcsConvKind.UIntToUInt64: return TaggedValue.FromUInt64(unchecked((uint)v.I32)); - case EcsConvKind.UInt64ToInt: return TaggedValue.FromInt(unchecked((int)v.I64)); - case EcsConvKind.IntToPtr: return TaggedValue.FromPtr(v.I32); - case EcsConvKind.PtrToInt: return TaggedValue.FromInt(unchecked((int)v.I64)); - case EcsConvKind.UInt64ToPtr: return TaggedValue.FromPtr(v.I64); - case EcsConvKind.PtrToUInt64: return TaggedValue.FromUInt64(unchecked((ulong)v.I64)); - case EcsConvKind.DoubleToUInt64: return TaggedValue.FromUInt64(unchecked((ulong)SaturateD2L(v.F64))); - case EcsConvKind.UInt64ToDouble: return TaggedValue.FromDouble(unchecked((double)(ulong)v.I64)); - case EcsConvKind.ToStr: return NewString(ToStringTop(v)); - case EcsConvKind.ToInt: - if (v.Tag is EcsTag.Bool or EcsTag.Byte or EcsTag.Int or EcsTag.UInt) - return TaggedValue.FromInt(v.I32); - if (v.Tag == EcsTag.Double) - return TaggedValue.FromInt(SaturateD2I(v.F64)); - if (v.Tag == EcsTag.String) - return TaggedValue.FromInt(EcsConvText.ParseIntLiteral(StrOrNull(v.Handle))); // PC 端:数字字符串解析,失败 0 - throw new SimError(ERR_TYPE, "无法转换为 int"); - default: - throw new SimError(ERR_TYPE, $"未知转换 {kind}"); - } - } - - static int SaturateD2I(double d) - { - if (double.IsNaN(d)) return 0; - if (d >= int.MaxValue) return int.MaxValue; - if (d <= int.MinValue) return int.MinValue; - return (int)d; - } - - static long SaturateD2L(double d) - { - if (double.IsNaN(d)) return 0; - if (d >= long.MaxValue) return long.MaxValue; - if (d <= long.MinValue) return long.MinValue; - return (long)d; - } - - static void RequireDivisor(TaggedValue v) - { - if (v.I32 == 0 && v.Tag != EcsTag.Double) - throw new SimError(ERR_DIVZERO, "整数除零"); - } - - bool ValueEquals(TaggedValue x, TaggedValue y) - { - if (x.Tag != y.Tag) - return false; - if (x.IsString) - { - var sx = StrOrNull(x.Handle); - var sy = StrOrNull(y.Handle); - return string.Equals(sx, sy, StringComparison.Ordinal); - } - if (x.Tag == EcsTag.Double) - return x.F64 == y.F64; - return x.I64 == y.I64; - } - - TaggedValue HostNative(string name, TaggedValue[] args) - { - // S-21:miss(无处理器/未实现名)→ strict 恢复响亮;非 strict 查缺省值表(表外未知名仍响亮) - if (_host.Native is { } native && native(name, args, _nativeCtx) is { } ret) - return ret; - if (_host.StrictCaps) - throw new SimError(ERR_NOSUCHNATIVE, $"原生函数未实现: {name}"); - return EcsCapabilityDefaults.GetNativeDefault(name, _nativeCtx) - ?? throw new SimError(ERR_NOSUCHNATIVE, $"原生函数未注册: {name}"); - } - - /// 堆上下文实现:宿主原生回调读写解释器堆的唯一通道。 - sealed class NativeContext(EcxInterpreter vm) : EcxNativeContext - { - public override string Str(TaggedValue v) => v.IsString ? vm.StrOrNull(v.Handle) ?? "" : ""; - - public override TaggedValue Str(string s) => vm.NewString(s); - - public override Value ToValue(TaggedValue v) - { - if (v.IsString) - return Value.FromString(vm.StrOrNull(v.Handle) ?? ""); - if (v.IsArray) - { - var arr = vm.Arr(v.Handle); - var items = new List(arr.Items.Count); - foreach (var item in arr.Items) - items.Add(ToValue(item)); - var elemType = ElemScriptType(arr.ElemTag); - return Value.FromArray(ScriptArray.Create(elemType, items), elemType); - } - return v.ToValue(); - } - - public override TaggedValue FromValue(Value v) - { - var t = v.Type; - if (t.Equals(ScriptType.String)) - return vm.NewString(v.AsString()); - return TaggedValue.FromValue(v); // 标量;数组/结构体原生不返回 - } - - static ScriptType ElemScriptType(byte tag) => tag switch - { - EcsTag.Bool => ScriptType.Bool, - EcsTag.Byte => ScriptType.Byte, - EcsTag.UInt => ScriptType.UInt, - EcsTag.Double => ScriptType.Double, - EcsTag.UInt64 => ScriptType.UInt64, - EcsTag.String => ScriptType.String, - EcsTag.Ptr => ScriptType.Ptr, - _ => ScriptType.Int, - }; - } - - /// 入口返回值转公共 Value:字符串解引用、数组转 ScriptArray、结构体重建为 - /// EcsStruct(堆在返回前仍存活)。堆外无句柄语义,转换必须在此完成。 - Value ToPublicValue(TaggedValue v) => ToPublicValue(v, new Dictionary()); - - Value ToPublicValue(TaggedValue v, Dictionary structs) - { - if (v.IsString) - return Value.FromString(StrOrNull(v.Handle) ?? ""); - if (v.IsArray) - { - var arr = Arr(v.Handle); - var items = new List(arr.Items.Count); - foreach (var item in arr.Items) - items.Add(ToPublicValue(item, structs)); - var elemType = ElemScriptTypeOf(arr.ElemTag); - return Value.FromArray(ScriptArray.Create(elemType, items, new SimpleStringStore()), elemType); - } - if (v.IsStruct) - return Value.FromStruct(PublicStruct(St(v.Handle), structs)); - return v.ToValue(); - } - - static ScriptType ElemScriptTypeOf(byte tag) => tag switch - { - EcsTag.Bool => ScriptType.Bool, - EcsTag.Byte => ScriptType.Byte, - EcsTag.UInt => ScriptType.UInt, - EcsTag.Double => ScriptType.Double, - EcsTag.String => ScriptType.String, - EcsTag.Ptr => ScriptType.Ptr, - _ => ScriptType.Int, - }; - - /// SimStruct → EcsStruct(按镜像类型表重建定义;句柄槽逐字段展开)。 - EcsStruct PublicStruct(SimStruct st, Dictionary structs) - { - if (structs.TryGetValue(st, out var cached)) - return cached; - var def = PublicStructDef(st.Layout, []); - var es = new EcsStruct(def); - structs[st] = es; - for (int i = 0; i < st.Layout.Fields.Length; i++) - { - var f = st.Layout.Fields[i]; - var defField = def.Fields[i]; - switch (f.Kind) - { - case EcsFieldKind.FixedArray: - for (int e = 0; e < f.Count; e++) - es.SetFieldElement(defField, e, PublicRaw(st.Slots[f.SlotOffset + e], TypeLayout.GetElementType(defField.FieldType))); - break; - case EcsFieldKind.NestedStruct: - { - var nestedLayout = _image.Structs[f.NestedSid]; - var nested = new SimStruct - { - Layout = nestedLayout, - Slots = st.Slots[f.SlotOffset..(f.SlotOffset + nestedLayout.SlotCount)].ToArray(), - }; - es.SetField(defField, PublicStruct(nested, structs)); - break; - } - default: - es.SetField(defField, PublicRaw(st.Slots[f.SlotOffset], defField.FieldType)); - break; - } - } - return es; - } - - EcsStructDef PublicStructDef(EcsStructLayout layout, Dictionary defs) - { - if (defs.TryGetValue(layout, out var cached)) - return cached; - var def = new EcsStructDef { Name = layout.Name }; - defs[layout] = def; - foreach (var f in layout.Fields) - { - var fieldType = f.Kind switch - { - EcsFieldKind.FixedArray => new ArrayType(ElemScriptTypeOf((byte)f.ElementType), f.Count), - EcsFieldKind.NestedStruct => new StructType(PublicStructDef(_image.Structs[f.NestedSid], defs)), - _ => ElemScriptTypeOf((byte)f.Type), - }; - def.Fields.Add(new EcsFieldDef { Name = f.Name, FieldType = fieldType }); - } - StructLayout.Calculate(def); - return def; - } - - object PublicRaw(TaggedValue v, ScriptType type) => type switch - { - _ when type.Equals(ScriptType.Bool) => v.I32 != 0, - _ when type.Equals(ScriptType.Byte) => (byte)v.I32, - _ when type.Equals(ScriptType.UInt) => unchecked((uint)v.I32), - _ when type.Equals(ScriptType.UInt64) => unchecked((ulong)v.I64), - _ when type.Equals(ScriptType.Ptr) => new IntPtr(v.I64), - _ when type.Equals(ScriptType.Double) => v.F64, - _ when type.Equals(ScriptType.String) => StrOrNullPub(v), - _ => v.I32, - }; - - string? StrOrNullPub(TaggedValue v) => v.IsString ? StrOrNull(v.Handle) : null; - - - sealed class SimError : Exception - { - public readonly int Code; - public SimError(int code, string message) : base(message) { Code = code; } - } } \ No newline at end of file diff --git a/src/EasyCon.Script/Bytecode/EcxLinker.cs b/src/EasyCon.Script/Bytecode/EcxLinker.cs index 0d00505d..83273379 100644 --- a/src/EasyCon.Script/Bytecode/EcxLinker.cs +++ b/src/EasyCon.Script/Bytecode/EcxLinker.cs @@ -17,10 +17,13 @@ internal static class EcxPipeline { // ============ 阶段二:链接模块产物 → EcxImage ============ - public static EcxImage Link(IReadOnlyList artifacts, bool keyAction, bool needIL) + public static EcxImage Link(IReadOnlyList artifacts, bool keyAction, bool needIL, + ScriptTarget target = ScriptTarget.Portable) { if (artifacts.Count == 0) throw new BytecodeException(new[] { new BytecodeDiagnostic("没有可链接的模块", null, 0) }); + if (target == ScriptTarget.Portable && artifacts.Any(a => a.RequiresPcExecution)) + throw new BytecodeException(new[] { new BytecodeDiagnostic("PC 扩展帧不能链接为可分发 ECX;请使用 Portable 目标重新编译", null, 0) }); const uint ImportFlag = 0x80000000u; @@ -222,6 +225,7 @@ public static EcxImage Link(IReadOnlyList artifacts, bool keyAct var image = new EcxImage { + Target = target, Consts = imageConsts, Structs = globalStructs, Globals = imageGlobals, diff --git a/src/EasyCon.Script/Bytecode/EcxModuleEncoder.cs b/src/EasyCon.Script/Bytecode/EcxModuleEncoder.cs index d85f9ac5..985f7c75 100644 --- a/src/EasyCon.Script/Bytecode/EcxModuleEncoder.cs +++ b/src/EasyCon.Script/Bytecode/EcxModuleEncoder.cs @@ -29,7 +29,7 @@ public static ModuleArtifact CompileWholeProgramAsModule( IReadOnlySet? externalFunctions, - bool hasInit, bool isMain) + bool hasInit, bool isMain, ScriptTarget target = ScriptTarget.Portable) { @@ -67,6 +67,8 @@ public static ModuleArtifact CompileWholeProgramAsModule( ExternalFunctions = externalFunctions, + Target = target, + }; diff --git a/src/EasyCon.Script/Bytecode/InstructionExecutor.cs b/src/EasyCon.Script/Bytecode/InstructionExecutor.cs new file mode 100644 index 00000000..4d852ef0 --- /dev/null +++ b/src/EasyCon.Script/Bytecode/InstructionExecutor.cs @@ -0,0 +1,1520 @@ +using EasyCon.Script.Runtime; +using EasyCon.Script.Symbols; +using System.Runtime.InteropServices; +using System.Text; + +namespace EasyCon.Script.Bytecode; + +/// +/// 模拟解释器宿主:与 C VM 的 ecs_host vtable 同构。 +/// +/// 平台能力位(对齐 ecs_vm.h 的 ECS_CAP_*)。 +[Flags] +public enum EcsCaps +{ + None = 0, + Print = 1 << 0, + Alert = 1 << 1, + Beep = 1 << 2, + File = 1 << 3, + Capture = 1 << 4, + Ffi = 1 << 5, + Stdin = 1 << 6, + AllDesktop = Print | Alert | Beep | File | Capture | Ffi | Stdin, + AllMcu = None, // 单片机:全部忽略,脚本副作用仅按键/摇杆/延时 +} + +/// +/// 宿主原生的堆上下文:解释器内部堆(字符串/数组句柄)的受控出口。 +/// CallN 原生回调经此读取字符串/复合实参、回写字符串结果—— +/// 镜像里的句柄值离开解释器堆没有意义,宿主不得自行解释。 +/// +public abstract class EcxNativeContext +{ + /// 字符串解引用(非字符串句柄返回空串)。 + public abstract string Str(TaggedValue v); + + /// 分配字符串,返回可回传给镜像的句柄值。 + public abstract TaggedValue Str(string s); + + /// 完整转换为 Value(字符串解引用、数组逐元素复制;结构体返回 Void)。 + public abstract Value ToValue(TaggedValue v); + + /// Value → TaggedValue(标量/字符串;数组/结构体不支持,返回 Void)。 + public abstract TaggedValue FromValue(Value v); +} + +public sealed class EcxHost +{ + public EcsCaps Caps { get; set; } = EcsCaps.AllDesktop; + /// S-14 行断协议状态(FWRITE/ALERT 尾部 '\' 挂起下一行换行;宿主所有)。 + public bool PendingBreak; + public readonly List WaitLog = new(); + public readonly List KeyLog = new(); + public readonly List KeyStateLog = new(); + public readonly List StickSetLog = new(); + public readonly List StickClickLog = new(); + public readonly List AmiiboLog = new(); + public readonly List BeepLog = new(); + public readonly List Lines = new(); + public readonly List Alerts = new(); + + public Action WaitMs { get; set; } = _ => { }; + public Action Key { get; set; } = (_, _) => { }; + public Action KeyState { get; set; } = (_, _) => { }; + public Action StickSet { get; set; } = (_, _, _) => { }; + public Action StickClick { get; set; } = (_, _, _, _) => { }; + public Func ImgLabel { get; set; } = _ => -1; + public Func Rand { get; set; } = _ => 0; + public Func TimeMs { get; set; } = () => 0; + public Action Beep { get; set; } = (_, _) => { }; + /// S-13 AMIIBO:槽位选择回调(0–19 合法,越界宿主静默——参考实现已判)。 + public Action Amiibo { get; set; } = _ => { }; + /// FWRITE stdout 行断协议的落点(message 已按协议剥尾、newline 已解析)。 + public Action Print { get; set; } = (_, _) => { }; + public Action Alert { get; set; } = _ => { }; + public Func ReadLine { get; set; } = () => ""; + /// + /// 扩展原生(ENCODE/JQ/采集卡洞/FFI);null 或返回 null = 未实现。 + /// 字符串实参/返回值经 解引用/分配——镜像句柄离开解释器堆无意义。 + /// + public Func? Native { get; set; } + /// + /// L2 平台 syscall 处理器(编号语义,docs/VM2.md §9.1);缺省为桌面参考实现 + /// (行断协议 + Caps 门控 + 文件族转发 Native)。返回 null = 未实现。 + /// + public Func? Syscall { get; set; } + /// + /// S-21 strict_caps(默认 off):off = 能力缺失按 缺省值表 + /// 降级(feats 仅元信息);on = 恢复响亮行为(L2/L3 miss 即 ERR_NOSUCHNATIVE, + /// 图像标签 ERR_IL)——迁移保底、诊断模式、「同宿主能力集」对拍严格档。 + /// + public bool StrictCaps { get; set; } + public string[] Args { get; set; } = []; + public string AppDir { get; set; } = ""; + + public EcxHost() + { + Syscall = ReferenceSyscall; // 桌面参考实现;专用宿主可整体替换 + } + + /// + /// L2 平台 syscall 桌面参考实现(编号语义的唯一 C# 权威,与 C harness 桩逐字对齐; + /// 由 VM 核 CallNative/CallSyscall 平移而来): + /// FWRITE/FREAD 行断协议 + Caps 门控;文件实现转发 (规范名,BuiltinMap)。 + /// 返回 null = 未实现(调用方报 ERR_NOSUCHNATIVE)。 + /// + private TaggedValue? ReferenceSyscall(int id, TaggedValue[] args, EcxNativeContext ctx) + { + switch (id) + { + case EcsSyscall.FWrite: + return FWriteReference(args[0].I64, args, ctx); + case EcsSyscall.FRead: + if (args[0].I64 == 0) + return Caps.HasFlag(EcsCaps.Stdin) ? ctx.Str(ReadLine()) : ctx.Str(""); + return Caps.HasFlag(EcsCaps.File) + ? Native?.Invoke(EcsSyscall.Names[id], args, ctx) + : ctx.Str(""); + case EcsSyscall.Alert: + if (Caps.HasFlag(EcsCaps.Alert)) + { + // 行断协议对齐(与 FWRITE stdout / 金标准 ImplAlert 一致):剥尾 `\` 续行 + var alert = ctx.Str(args[0]); + Alert(alert.EndsWith('\\') ? alert[..^1] : alert); + } + return TaggedValue.Void; // 无 Alert 能力:静默忽略 + case EcsSyscall.Arg: + { + var i = args[0].I32; + return ctx.Str(i >= 0 && i < Args.Length ? Args[i] : ""); + } + case EcsSyscall.Env: + return ctx.Str(Environment.GetEnvironmentVariable(ctx.Str(args[0])) ?? ""); + case EcsSyscall.App: + return ctx.Str(AppDir); + case EcsSyscall.Time: + return TaggedValue.FromInt(TimeMs()); + case EcsSyscall.OcrConf: + return Native?.Invoke(EcsSyscall.Names[id], args, ctx) ?? TaggedValue.FromInt(0); + case EcsSyscall.FOpen: + case EcsSyscall.FClose: + case EcsSyscall.FEof: + case EcsSyscall.FileExists: + // 文件族其余:句柄/存在性语义在 BuiltinMap(IFileSystem);缺 File 能力 → 缺省值表 + return Caps.HasFlag(EcsCaps.File) + ? Native?.Invoke(EcsSyscall.Names[id], args, ctx) + : EcsCapabilityDefaults.GetSyscallDefault(id, args, ctx); + case EcsSyscall.Beep: + if (Caps.HasFlag(EcsCaps.Beep)) + Beep(args[0].I32, args[1].I32); + return TaggedValue.Void; // 无 Beep 能力:静默忽略 + case EcsSyscall.Amiibo: + if (args[0].I32 is >= 0 and <= 19) // S-13:槽位 0–19,越界静默(宿主内判) + Amiibo(args[0].I32); + return TaggedValue.Void; + default: + return null; // 未知编号 → ERR_NOSUCHNATIVE + } + } + + /// FWRITE:句柄 1=stdout 行断协议;0=no-op 返 len;2=print(newline);>2=文件族转发。 + private TaggedValue? FWriteReference(long handle, TaggedValue[] args, EcxNativeContext ctx) + { + var s = ctx.Str(args[1]); + if (handle == 1) + { + if (Caps.HasFlag(EcsCaps.Print)) + { + var output = s.EndsWith('\\') ? s[..^1] : s; + Print(output, !PendingBreak); + PendingBreak = s.EndsWith('\\'); + } + return TaggedValue.FromInt(s.Length); + } + if (handle == 0) + return TaggedValue.FromInt(s.Length); + if (handle == 2) + { + if (Caps.HasFlag(EcsCaps.Print)) + Print(s, true); + return TaggedValue.FromInt(s.Length); + } + return Caps.HasFlag(EcsCaps.File) + ? Native?.Invoke(EcsSyscall.Names[EcsSyscall.FWrite], args, ctx) + : TaggedValue.FromInt(-1); + } + + /// 开启事件与输出记录(默认委托 → *Log 列表,格式与金标准 mock 一致)。 + public void EnableRecording() + { + var buf = new StringBuilder(); + WaitMs = ms => WaitLog.Add($"WAIT {ms}"); + Key = (k, d) => KeyLog.Add($"KEY {k} {d}"); + KeyState = (k, d) => KeyStateLog.Add($"KEYST {k} {d}"); + StickSet = (s, x, y) => StickSetLog.Add($"STICK {s} {x} {y}"); + StickClick = (s, x, y, d) => StickClickLog.Add($"STICKC {s} {x} {y} {d}"); + Amiibo = i => AmiiboLog.Add($"AMIIBO {i}"); + Beep = (f, d) => BeepLog.Add($"BEEP {f} {d}"); + Print = (s, nl) => + { + buf.Append(s); + if (nl) + { + Lines.Add(buf.ToString()); + buf.Clear(); + } + }; + Alert = s => Alerts.Add(s); + } +} + +/// +/// PC 执行与 ECX 模拟共用的指令语义内核。完整整数操作数直接执行, +/// 不经过 ECX 编码、投影或字节截断;目标与分发约束由入口和容器负责。 +/// +internal sealed class InstructionExecutor +{ + sealed class SimArray + { + public byte ElemTag; + public List Items = new(); + } + + sealed class SimStruct + { + public EcsStructLayout Layout = null!; + public TaggedValue[] Slots = null!; + /// GetF 嵌套视图:本结构是父槽区切片的拷贝,写入须经 + /// 回写父槽(对齐 v1 EcsStruct.GetNested 的原生内存写穿透语义)。 + public SimStruct? ViewParent; + public int ViewOffset; + } + + sealed class Frame + { + public EcsFunction Fn = null!; + public int Pc; + public TaggedValue[] Slots = null!; + public int RetSlot = -1; + } + + readonly EcxImage _image; + readonly EcxHost _host; + readonly List _heap = new(); // string / SimArray / SimStruct;索引+1 = 句柄 + readonly List _refCounts = new(); // 与 _heap 平行:槽位对象引用计数(ecs_vm.c §4.2 rc / VM2.md §8.4) + readonly List _freeSlots = new(); // 已释放下标自由链(复用;句柄 = 列表下标,恒不移除/移动元素) + readonly List _frames = new(); + readonly List _framePool = new(); // 弹帧回收池:Slots 容量只增,出租时 Array.Clear 复位 + readonly TaggedValue[] _globals; + readonly EcxNativeContext _nativeCtx; + CancellationToken _token; // 当前 Run 的取消令牌(Step 装载) + int _steps; + int _budget = 1_000_000; + + public int ErrorFunc { get; private set; } = -1; + public int ErrorPc { get; private set; } = -1; + + // ---- 堆引用计数统计(internal 只读,测试/诊断观测)---- + internal int HeapLiveCount { get; private set; } // 非空槽位数(存活对象) + internal int HeapHighWater { get; private set; } // 存活峰值 + internal long HeapAllocTotal { get; private set; } + internal long HeapReleaseTotal { get; private set; } + internal int HeapFreeListCount => _freeSlots.Count; + + /// 计数自洽性:alloc − release = live = 非空槽位数(槽 0 为 null 哨兵),且无负计数。 + internal bool HeapCountsConsistent + { + get + { + if (HeapAllocTotal - HeapReleaseTotal != HeapLiveCount) + return false; + int live = 0; + for (int i = 1; i < _heap.Count; i++) + if (_heap[i] is not null) + live++; + if (live != HeapLiveCount || live != _heap.Count - 1 - _freeSlots.Count) + return false; + foreach (var rc in _refCounts) + if (rc < 0) + return false; + return true; + } + } + + /// 入口函数(<main>/$eval)的返回值(顶层 RETURN;无返回值为 Void)。 + public TaggedValue Result { get; private set; } + + public const int OK = 0; + public const int YIELD = 1; + public const int CANCELLED = 2; + // 错误码 ABI 与 native/ecs_vm.h 结果码枚举一致(VmHeaderContractTests 锁定头文件侧) + public const int ERR_IMAGE = 3; + public const int ERR_OPCODE = 4; + public const int ERR_SLOT = 5; + public const int ERR_TYPE = 6; + public const int ERR_INDEX = 7; + public const int ERR_DIVZERO = 8; + public const int ERR_DEPTH = 9; + public const int ERR_NOSUCHNATIVE = 10; + public const int ERR_HOST = 11; + public const int ERR_OOM = 12; + public const int ERR_IL = 13; + public const int ERR_FEAT = 14; + + /// 调用深度上限(与 C 侧 ECS_MAX_CALL_DEPTH 一致:深递归双端同样报 ERR_DEPTH, + /// 避免同一镜像在 MCU 报错而 PC 端 OOM 的对拍分裂)。帧为每函数栈窗口(典型高水位 + /// 20–40 槽 ≈ 0.3–0.6KB),64 深度在 C VM 帧段(128KB)内余量充足;深递归由尾递归消除(TRE)兜底。 + public const int MaxCallDepth = 64; + + InstructionExecutor(EcxImage image, EcxHost host) + { + _image = image; + _host = host; + _globals = new TaggedValue[image.Globals.Count]; + _heap.Add(null); // 句柄 0 = null + _refCounts.Add(0); // 与 _heap 平行(哨兵槽位) + _nativeCtx = new NativeContext(this); + } + + public static int Run(EcxImage image, EcxHost host, CancellationToken token = default) + => Run(image, host, out _, out _, out _, out _, token); + + /// 执行并取回入口函数返回值(顶层 RETURN 语义,与金标准 SsaEvaluator.Evaluate 一致)。 + public static int Run(EcxImage image, EcxHost host, out Value result, CancellationToken token = default) + => Run(image, host, out _, out _, out _, out result, token); + + /// 执行镜像直到完成/取消/错误;出错时经 errorFunc/errorPc 报告现场。 + public static int Run(EcxImage image, EcxHost host, out int errorFunc, out int errorPc, CancellationToken token = default) + => Run(image, host, out _, out errorFunc, out errorPc, out _, token); + + /// 执行并返回解释器实例(堆引用计数统计观测用;独立命名避免与 out Value 重载二义)。 + public static int RunWithVm(EcxImage image, EcxHost host, out InstructionExecutor vm, CancellationToken token = default) + { + vm = new InstructionExecutor(image, host); + int code; + while (true) + { + code = vm.Step(token); + if (code != YIELD) + break; + } + return code; + } + + /// 执行镜像直到完成/取消/错误;返回错误码、入口返回值与出错现场(与 C VM 对齐)。 + public static int Run(EcxImage image, EcxHost host, out TaggedValue rawResult, out int errorFunc, out int errorPc, out Value result, CancellationToken token = default) + { + var vm = new InstructionExecutor(image, host); + int code; + while (true) + { + code = vm.Step(token); + if (code != YIELD) + break; + } + rawResult = vm.Result; + result = vm.ToPublicValue(vm.Result); + errorFunc = vm.ErrorFunc; + errorPc = vm.ErrorPc; + return code; + } + + // ---- 堆 ---- + + /// 分配堆槽位:自由链优先复用下标(句柄 = 列表下标,释放 = 对象置 null + 下标入自由链, + /// 恒不移除/移动元素)。出生引用 rc=1 归属接收槽(调用方以 StoreFresh 直写,不再 retain)。 + int Store(object obj) + { + HeapAllocTotal++; + int h; + if (_freeSlots.Count > 0) + { + h = _freeSlots[^1]; + _freeSlots.RemoveAt(_freeSlots.Count - 1); + _heap[h] = obj; + _refCounts[h] = 1; + } + else + { + h = _heap.Count; + _heap.Add(obj); + _refCounts.Add(1); + } + HeapLiveCount++; + if (HeapLiveCount > HeapHighWater) + HeapHighWater = HeapLiveCount; + return h; + } + + /// 静态(pinned)常量串的数据源(S-20):直读镜像常量池,零拷贝零分配(对齐 C 侧 str_or_null)。 + string? StaticStr(int kx) => kx >= 0 && kx < _image.Consts.Count ? _image.Consts[kx].Str ?? "" : null; + + string Str(int h) => h < 0 ? StaticStr(h & 0x7FFFFFFF) ?? "" : h > 0 && _heap[h] is string s ? s : throw new SimError(ERR_TYPE, "解引用非字符串句柄"); + string? StrOrNull(int h) => h < 0 ? StaticStr(h & 0x7FFFFFFF) : h > 0 && _heap[h] is string s ? s : null; + SimArray Arr(int h) => _heap[h] as SimArray ?? throw new SimError(ERR_TYPE, "解引用非数组句柄"); + SimStruct St(int h) => _heap[h] as SimStruct ?? throw new SimError(ERR_TYPE, "解引用非结构体句柄"); + + TaggedValue NewString(string s) => TaggedValue.FromStringHandle(Store(s)); + + /// retain(对齐 ecs_vm.c):句柄值引用 +1;null 哨兵/已释放槽静默容忍(Str/Arr/St 守卫同源)。 + void Retain(in TaggedValue v) + { + if (!v.IsHandle) + return; + var h = v.Handle; + if (h > 0 && h < _heap.Count && _heap[h] is not null) + _refCounts[h]++; + } + + /// release:计数归零时递归释放子引用,随后对象置 null 并入自由链(下标可复用)。 + /// 重复释放已释放槽位为 no-op(对齐 C VM heap_obj NULL 守卫)。 + void Release(in TaggedValue v) + { + if (!v.IsHandle) + return; + ReleaseHandle(v.Handle); + } + + void ReleaseHandle(int h) + { + if (h <= 0 || h >= _heap.Count) + return; + var obj = _heap[h]; + if (obj is null) + return; + if (--_refCounts[h] > 0) + return; + switch (obj) + { + case SimArray arr: + for (int i = 0; i < arr.Items.Count; i++) + Release(arr.Items[i]); + break; + case SimStruct st: + for (int i = 0; i < st.Slots.Length; i++) + Release(st.Slots[i]); + // 槽位残留句柄清零:结构体可能经 ViewParent 被后死的视图回写, + // 清零使回写路径的 release/retain 落在 Void 上(句柄复用后不误伤新对象) + Array.Clear(st.Slots); + break; + } + _heap[h] = null; + _refCounts[h] = 0; + _freeSlots.Add(h); + HeapLiveCount--; + HeapReleaseTotal++; + } + + /// 【槽写规则】任何帧槽 / 容器元素 / 结构体槽的写入必须经过 + /// / / / + /// 之一(裸写 = 跳过旧值释放,破坏 RC 双端锁步)。 + /// 唯一例外:全新分配、尚无旧值的槽区(如新帧实参槽)可直接写。 + /// 槽位覆写(move_to 语义,对齐 ecs_vm.c):旧值引用释放;新值为其他活跃槽位的借用 → retain 补引用。 + /// 同柄自赋值(槽位与新值同句柄)为 no-op:槽位已持有引用,先释放会悬垂。 + void MoveToSlot(ref TaggedValue slot, TaggedValue v) + { + if (v.IsHandle && slot.IsHandle && v.Handle == slot.Handle) + return; + Release(slot); + slot = v; + Retain(v); + } + + /// 槽位写入「出生引用」值(Store 新建对象 / DeepCopy 结果 / 标量):仅释放旧值—— + /// 出生引用(Store 时 rc=1 / DeepCopy 的 retain)即本槽位的引用。 + void StoreFresh(ref TaggedValue slot, TaggedValue v) + { + Release(slot); + slot = v; + } + + /// 容器元素覆写(SetI 语义,对齐 ecs_vm.c):旧元素释放,写入值 retain(借用补引用)。 + void OverwriteItem(List items, int idx, TaggedValue v) + { + var old = items[idx]; + if (v.IsHandle && old.IsHandle && v.Handle == old.Handle) + return; + Release(old); + Retain(v); + items[idx] = v; + } + + /// 弹帧并释放被弹帧槽位持有的全部句柄(Ret/Ret0,对齐 ecs_vm.c 帧退出语义); + /// 帧体回收进池(出租时复位)。 + void PopFrame(Frame frame) + { + _frames.RemoveAt(_frames.Count - 1); + var slots = frame.Slots; + for (int i = 0; i < slots.Length; i++) + Release(slots[i]); + _framePool.Add(frame); + } + + /// S-01 深拷贝:数组/结构体一层新容器(出生引用,子项 retain);字符串共享(retain 补接收槽引用)。 + /// 返回值恰携带一个接收槽引用,接收方以 StoreFresh 直写槽位。 + TaggedValue DeepCopy(TaggedValue v) + { + if (!v.IsHandle) + return v; + switch (v.Tag) + { + case EcsTag.Array: + { + var src = Arr(v.Handle); + var copy = new SimArray { ElemTag = src.ElemTag, Items = new List(src.Items.Count) }; + for (int i = 0; i < src.Items.Count; i++) + { + copy.Items.Add(src.Items[i]); + Retain(src.Items[i]); + } + return TaggedValue.FromArrayHandle(Store(copy)); + } + case EcsTag.Struct: + { + var src = St(v.Handle); + var copy = new SimStruct { Layout = src.Layout, Slots = (TaggedValue[])src.Slots.Clone() }; + for (int i = 0; i < copy.Slots.Length; i++) + Retain(copy.Slots[i]); + return TaggedValue.FromStructHandle(Store(copy)); + } + default: + Retain(v); // 字符串共享 + return v; + } + } + + /// COW 开关(运行时常量;ECS_COW=0 关闭,供全量测试矩阵双态验证)。 + internal static bool CopyOnWrite { get; set; } = Environment.GetEnvironmentVariable("ECS_COW") != "0"; + + /// COW(move-on-unique,Q1-A):句柄唯一引用(rc==1,恒为出生临时槽持有)时共享移交, + /// 跳过容器深拷贝;否则维持 S-17 深拷贝。可观察值语义逐字不变——变量/全局槽永不与另一 + /// 变量/全局共享容器(共享只发生在「出生临时槽 ↔ 接收槽」之间,临时槽对程序不可见)。 + TaggedValue DeepCopyCopyOnWrite(TaggedValue v) + { + if (CopyOnWrite && v.IsHandle) + { + var h = v.Handle; + if (h > 0 && h < _heap.Count && _heap[h] is not null && _refCounts[h] == 1) + { + Retain(v); // 接收槽补引用;出生临时槽的既持引用随其消亡释放 + return v; + } + } + return DeepCopy(v); + } + + // ---- TOSTR 两层上下文(S-07/S-08)---- + + string ToStringTop(TaggedValue v) + { + switch (v.Tag) + { + case EcsTag.Bool: return v.I32 != 0 ? "true" : "false"; + case EcsTag.Byte: + case EcsTag.Int: + case EcsTag.UInt: + return v.I32.ToString(); + case EcsTag.UInt64: return unchecked((ulong)v.I64).ToString(); + case EcsTag.Ptr: return v.I64.ToString(); + case EcsTag.Double: return v.F64.ToString(); + case EcsTag.String: return StrOrNull(v.Handle) ?? ""; + case EcsTag.Array: + case EcsTag.Struct: return ToStringNested(v); + default: return ""; + } + } + + string ToStringNested(TaggedValue v) + { + switch (v.Tag) + { + case EcsTag.Array: + { + var arr = Arr(v.Handle); + var sb = new StringBuilder("["); + for (int i = 0; i < arr.Items.Count; i++) + { + if (i > 0) sb.Append(", "); + sb.Append(ToStringNested(arr.Items[i])); + } + sb.Append(']'); + return sb.ToString(); + } + case EcsTag.Struct: + return $"struct:{St(v.Handle).Layout.Name}"; + case EcsTag.Ptr: + return $"0x{v.I64:X}"; + case EcsTag.Bool: return v.I32 != 0 ? "true" : "false"; + case EcsTag.UInt64: return unchecked((ulong)v.I64).ToString(); + case EcsTag.String: return StrOrNull(v.Handle) ?? ""; + case EcsTag.Double: return v.F64.ToString(); + case EcsTag.Void: return "void"; + default: return v.I32.ToString(); + } + } + + // ---- 主循环 ---- + + /// 帧出租(每函数栈窗口):帧长 = 目标编译器确认的寄存器高水位, + /// 池空新建;复用时槽位 Array.Clear 复位为 Void(StoreFresh「无旧值」直写契约), + /// 容量不足则扩容(容量只增,峰值深度即池上限——池随弹帧回收)。 + Frame RentFrame(int nslots) + { + if (_framePool.Count > 0) + { + var f = _framePool[^1]; + _framePool.RemoveAt(_framePool.Count - 1); + if (f.Slots.Length < nslots) + f.Slots = new TaggedValue[nslots]; // 新数组自带零值 + else + Array.Clear(f.Slots); + return f; + } + return new Frame { Slots = new TaggedValue[nslots] }; + } + + /// 常量池串 → 静态(pinned)值(契约 S-20 / docs/ZeroAllocVm.md §2):**不再新建堆对象**, + /// 直接返回指向镜像常量池的句柄(bit31 置位);不参与 RC,因此无需驻留表、无需补引用。 + /// 语义不变:串不可变、EqS/Cont/GetI/Len/Cat/TOSTR 一律按内容处理 + /// ( / 同时服务动态与静态句柄)。 + TaggedValue InternedString(int kx) => TaggedValue.FromStaticString(kx); + + int Step(CancellationToken token) + { + _token = token; + if (_frames.Count == 0) + { + var fn = _image.Functions[_image.Entry]; + var f = RentFrame(fn.NSlots); + f.Fn = fn; + f.Pc = 0; + f.RetSlot = -1; + _frames.Add(f); + } + if (_token.IsCancellationRequested) + return CANCELLED; + + // 预算计数局部化:字段读改写 → JIT 寄存器驻留(每指令省 2 次内存访问; + // 预算值 Run 期内不变、_steps 仅本方法读写,回写点 = 切片边界) + var steps = _steps; + var budget = _budget; + // 取指通路局部化(P0b):frame/指令流 span/R/pc 提升为循环局部,JIT 可驻留寄存器, + // 每指令省 4 次 _frames/frame 字段链解引用。不变量:指令流运行期只读(链接后不可变), + // span 恒指向当前帧函数的指令列表;换帧点(Call/Ret/Ret0)置 frameSwitched 由循环头重载; + // 切片出口(YIELD/宿主后 CANCELLED)写回 frame.Pc——Run 吸收 YIELD 后重入 Step 续跑。 + Frame frame = null!; + ReadOnlySpan code = default; + TaggedValue[] R = null!; + int pc = 0; + bool frameSwitched = true; + while (true) + { + if (frameSwitched) + { + frame = _frames[^1]; + code = CollectionsMarshal.AsSpan(frame.Fn.Instructions); + R = frame.Slots; + pc = frame.Pc; + frameSwitched = false; + } + + // 取消检查合并:每指令只做预算计数(1 分支);token 在入口、预算边界与宿主调用后检查 + // (纯计算最坏 1M 步 ≈ 十几毫秒感知;等待/按键型脚本在宿主调用返回后立即感知) + if (++steps >= budget) + { + frame.Pc = pc; + _steps = 0; + if (_token.IsCancellationRequested) + return CANCELLED; + return YIELD; + } + + int instructionPc = pc; + // 单一分派臂(SingleStreamFormat §2):解码形态是唯一指令表示, + // 不再有 Code/PcCode 双流与宽窄两条取指路径。 + var ins = code[pc++]; + var op = ins.Op; + int a = ins.A; + int b = ins.B; + int c = ins.C; + uint ext = ins.Ext; + + try + { + switch (op) + { + case EcsOpcode.Nop: break; + + // ---- 常量与移动 ---- + case EcsOpcode.LoadI: + StoreFresh(ref R[a], TaggedValue.FromInt(b)); + break; + case EcsOpcode.LoadK: + { + int kx = b; + var k = _image.Consts[kx]; + if (k.Tag == EcsTag.String) + MoveToSlot(ref R[a], InternedString(kx)); // 驻留串:借用 + retain,槽旧值照常释放 + else + StoreFresh(ref R[a], k.Tag switch + { + EcsTag.Double => TaggedValue.FromDouble(k.Float64), + EcsTag.UInt64 => TaggedValue.FromUInt64(unchecked((ulong)k.Int64)), + EcsTag.Ptr => TaggedValue.FromPtr(k.Int64), + EcsTag.UInt => TaggedValue.FromUInt(unchecked((uint)k.Int64)), + _ => TaggedValue.FromInt((int)k.Int64), + }); + break; + } + case EcsOpcode.LoadBool: + StoreFresh(ref R[a], TaggedValue.FromBool(b != 0)); + break; + case EcsOpcode.Move: + MoveToSlot(ref R[a], R[b]); + break; + case EcsOpcode.SetVar: + StoreFresh(ref R[a], DeepCopyCopyOnWrite(R[b])); + break; + case EcsOpcode.LoadG: + MoveToSlot(ref R[a], _globals[b]); + break; + case EcsOpcode.StoreG: + { + var nv = DeepCopyCopyOnWrite(R[a]); + int gx = b; + Release(_globals[gx]); + _globals[gx] = nv; + break; + } + + case EcsOpcode.Jmp: + pc += ins.Jump; + break; + case EcsOpcode.Jpt: + pc += R[a].I32 != 0 ? ins.Jump : 0; + break; + case EcsOpcode.Jpf: + pc += R[a].I32 == 0 ? ins.Jump : 0; + break; + + // ---- 比较跳转融合(P2′):R[a] kind R[b] 成立 → 跳 Jump;否则落入后继 ---- + case EcsOpcode.CmpJ: + { + bool taken; + switch (c / 6) + { + case 0: { int l = R[a].I32, r = R[b].I32; taken = (c % 6) switch { 0 => l == r, 1 => l != r, 2 => l < r, 3 => l <= r, 4 => l > r, _ => l >= r }; break; } + case 1: { uint l = (uint)R[a].I32, r = (uint)R[b].I32; taken = (c % 6) switch { 0 => l == r, 1 => l != r, 2 => l < r, 3 => l <= r, 4 => l > r, _ => l >= r }; break; } + case 2: { double l = R[a].F64, r = R[b].F64; taken = (c % 6) switch { 0 => l == r, 1 => l != r, 2 => l < r, 3 => l <= r, 4 => l > r, _ => l >= r }; break; } + default: { long l = R[a].I64, r = R[b].I64; taken = (c % 6) switch { 0 => l == r, 1 => l != r, 2 => l < r, 3 => l <= r, 4 => l > r, _ => l >= r }; break; } + } + if (taken) + pc += ins.Jump; + break; + } + + // ---- FOR 快速路径:tmp=i+1 写入 R[c];tmp>limit → 跳 Jump(出循环,$i 保持 ==upper);否则落入后继 ---- + // 信任字节码:跳转目标不校验 + case EcsOpcode.ForStep: + { + int next = R[a].I32 + 1; + StoreFresh(ref R[c], TaggedValue.FromInt(next)); + if (next > R[b].I32) + pc = instructionPc + 1 + ins.Jump; + break; + } + + // ---- 调用 ---- + // 每函数栈窗口帧:实参从调用方 STAGING(A)深拷贝播种进被调方 + // R1 起的参数窗(S-17 语义);C = 接收寄存器(0 = RET 固定落点) + case EcsOpcode.Call: + { + uint target = ext; + var callee = _image.Functions[(int)target]; + if (_frames.Count >= MaxCallDepth) + throw new SimError(ERR_DEPTH, $"调用深度超过上限 {MaxCallDepth}"); + var nf = RentFrame(callee.NSlots); + nf.Fn = callee; + nf.RetSlot = c; + nf.Pc = 0; + for (int i = 0; i < b; i++) + StoreFresh(ref nf.Slots[EcsRegisters.ArgBase + i], DeepCopyCopyOnWrite(R[a + i])); // S-17 实参(COW:唯一引用移交) + frame.Pc = pc; // 调用者续跑点写回(Ret 时经重载读回) + _frames.Add(nf); + frameSwitched = true; + break; + } + case EcsOpcode.CallN: + { + uint target = ext; + var args = new TaggedValue[b]; // 实参按值快照:原生看不见帧槽 + for (int i = 0; i < b; i++) + args[i] = R[a + i]; + TaggedValue ret; + if ((target & EcsSyscall.CallFlag) != 0) + { + // L2:编号 syscall,语义在宿主参考实现(VM 核纯调度) + int id = (int)(target & 0x7FFFFFFFu); + var resolved = _host.Syscall?.Invoke(id, args, _nativeCtx); + if (resolved is null) + { + // S-21:miss → 缺省值表降级;strict 或表外编号 → 响亮 + resolved = EcsCapabilityDefaults.GetSyscallDefault(id, args, _nativeCtx); + if (resolved is null || _host.StrictCaps) + throw new SimError(ERR_NOSUCHNATIVE, $"syscall 未实现: 编号 {id}"); + } + ret = resolved.Value; + } + else + { + ret = HostNative(_image.Natives[(int)target].Name, args); // L3 名表路径:FFI/采集洞/ENCODE/JQ(信任字节码,目标不校验) + } + if (c >= 0) + StoreFresh(ref R[c], ret); // 接收寄存器(0 = RET 固定落点);原生返回值恒为新建对象/标量(EcxNativeContext 契约),出生引用即接收槽引用 + if (_token.IsCancellationRequested) + { + frame.Pc = pc; + return CANCELLED; // 宿主调用后即时取消(原生内含 PRINT/READ/WAIT 类长延迟) + } + break; + } + case EcsOpcode.Ret: + { + var val = R[a]; + if (_frames.Count == 1) + { + Result = DeepCopyCopyOnWrite(val); // 入口返回值(顶层 RETURN):Result 持独立引用,跨弹帧存活 + PopFrame(frame); + return OK; + } + TaggedValue copy = DeepCopyCopyOnWrite(val); // S-17 返回(COW:唯一引用移交;先于弹帧:val 所属槽位将被释放) + int retSlot = frame.RetSlot; + PopFrame(frame); + StoreFresh(ref _frames[^1].Slots[retSlot], copy); + frameSwitched = true; + break; + } + case EcsOpcode.Ret0: + PopFrame(frame); + if (_frames.Count == 0) + return OK; + frameSwitched = true; + break; + + default: + if (ExecOther(op, a, b, c, ext, R)) + { + frame.Pc = pc; + return CANCELLED; + } + break; + } + } + catch (SimError e) + { + if (e.Code == OK) + return OK; // Halt 正常停机:不留错误现场(与 C VM 对齐) + ErrorFunc = _image.Functions.IndexOf(frame.Fn); + ErrorPc = instructionPc; + return e.Code; + } + } + } + + /// 非控制流/非调用的算术与数据指令。返回 true = 本指令调用了宿主且取消已请求, + /// 调用方立即终止(等待/按键类长延迟宿主调用的取消感知点)。 + bool ExecOther(EcsOpcode op, int a, int b, int c, uint ext, TaggedValue[] R) + { + switch (op) + { + // ---- 算术(S-02/S-05)---- + case EcsOpcode.AddI: StoreFresh(ref R[a], TaggedValue.FromInt(R[b].I32 + R[c].I32)); break; + case EcsOpcode.SubI: StoreFresh(ref R[a], TaggedValue.FromInt(R[b].I32 - R[c].I32)); break; + case EcsOpcode.MulI: StoreFresh(ref R[a], TaggedValue.FromInt(R[b].I32 * R[c].I32)); break; + case EcsOpcode.DivI: RequireDivisor(R[c]); StoreFresh(ref R[a], TaggedValue.FromInt(R[b].I32 / R[c].I32)); break; + case EcsOpcode.ModI: RequireDivisor(R[c]); StoreFresh(ref R[a], TaggedValue.FromInt(R[b].I32 % R[c].I32)); break; + case EcsOpcode.RDivI: RequireDivisor(R[c]); StoreFresh(ref R[a], TaggedValue.FromInt((R[b].I32 + R[c].I32 / 2) / R[c].I32)); break; + + case EcsOpcode.AddU: StoreFresh(ref R[a], TaggedValue.FromUInt(unchecked((uint)R[b].I32 + (uint)R[c].I32))); break; + case EcsOpcode.SubU: StoreFresh(ref R[a], TaggedValue.FromUInt(unchecked((uint)R[b].I32 - (uint)R[c].I32))); break; + case EcsOpcode.MulU: StoreFresh(ref R[a], TaggedValue.FromUInt(unchecked((uint)R[b].I32 * (uint)R[c].I32))); break; + case EcsOpcode.DivU: RequireDivisor(R[c]); StoreFresh(ref R[a], TaggedValue.FromUInt((uint)R[b].I32 / (uint)R[c].I32)); break; + case EcsOpcode.ModU: RequireDivisor(R[c]); StoreFresh(ref R[a], TaggedValue.FromUInt((uint)R[b].I32 % (uint)R[c].I32)); break; + + case EcsOpcode.AddL: StoreFresh(ref R[a], TaggedValue.FromUInt64(unchecked((ulong)R[b].I64 + (ulong)R[c].I64))); break; + case EcsOpcode.SubL: StoreFresh(ref R[a], TaggedValue.FromUInt64(unchecked((ulong)R[b].I64 - (ulong)R[c].I64))); break; + case EcsOpcode.MulL: StoreFresh(ref R[a], TaggedValue.FromUInt64(unchecked((ulong)R[b].I64 * (ulong)R[c].I64))); break; + case EcsOpcode.DivL: RequireDivisor(R[c]); StoreFresh(ref R[a], TaggedValue.FromUInt64((ulong)R[b].I64 / (ulong)R[c].I64)); break; + case EcsOpcode.ModL: RequireDivisor(R[c]); StoreFresh(ref R[a], TaggedValue.FromUInt64((ulong)R[b].I64 % (ulong)R[c].I64)); break; + + case EcsOpcode.AddD: StoreFresh(ref R[a], TaggedValue.FromDouble(R[b].F64 + R[c].F64)); break; + case EcsOpcode.SubD: StoreFresh(ref R[a], TaggedValue.FromDouble(R[b].F64 - R[c].F64)); break; + case EcsOpcode.MulD: StoreFresh(ref R[a], TaggedValue.FromDouble(R[b].F64 * R[c].F64)); break; + case EcsOpcode.DivD: StoreFresh(ref R[a], TaggedValue.FromDouble(R[b].F64 / R[c].F64)); break; + + // ---- 位运算(S-03 掩码)---- + case EcsOpcode.BandI: StoreFresh(ref R[a], TaggedValue.FromInt(R[b].I32 & R[c].I32)); break; + case EcsOpcode.BorI: StoreFresh(ref R[a], TaggedValue.FromInt(R[b].I32 | R[c].I32)); break; + case EcsOpcode.BxorI: StoreFresh(ref R[a], TaggedValue.FromInt(R[b].I32 ^ R[c].I32)); break; + case EcsOpcode.ShlI: StoreFresh(ref R[a], TaggedValue.FromInt(R[b].I32 << (R[c].I32 & 31))); break; + case EcsOpcode.ShrI: StoreFresh(ref R[a], TaggedValue.FromInt(R[b].I32 >> (R[c].I32 & 31))); break; + case EcsOpcode.BnotI: StoreFresh(ref R[a], TaggedValue.FromInt(~R[b].I32)); break; + + // ---- 比较 ---- + // 比较族直写(P0):委托间接调用 + CompareTo 是热点路径纯开销; + // 浮点直接用 C# 比较符(NaN → false),与 C VM 的 C 语义一致 + //(原 CmpD 的 CompareTo 把 NaN 排在最前,与 C VM 存在潜在分歧) + case EcsOpcode.EqI: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].I32 == R[c].I32)); break; + case EcsOpcode.LtI: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].I32 < R[c].I32)); break; + case EcsOpcode.LeI: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].I32 <= R[c].I32)); break; + case EcsOpcode.GtI: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].I32 > R[c].I32)); break; + case EcsOpcode.GeI: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].I32 >= R[c].I32)); break; + case EcsOpcode.EqU: StoreFresh(ref R[a], TaggedValue.FromBool((uint)R[b].I32 == (uint)R[c].I32)); break; + case EcsOpcode.LtU: StoreFresh(ref R[a], TaggedValue.FromBool((uint)R[b].I32 < (uint)R[c].I32)); break; + case EcsOpcode.LeU: StoreFresh(ref R[a], TaggedValue.FromBool((uint)R[b].I32 <= (uint)R[c].I32)); break; + case EcsOpcode.GtU: StoreFresh(ref R[a], TaggedValue.FromBool((uint)R[b].I32 > (uint)R[c].I32)); break; + case EcsOpcode.GeU: StoreFresh(ref R[a], TaggedValue.FromBool((uint)R[b].I32 >= (uint)R[c].I32)); break; + case EcsOpcode.EqD: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].F64 == R[c].F64)); break; + case EcsOpcode.LtD: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].F64 < R[c].F64)); break; + case EcsOpcode.LeD: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].F64 <= R[c].F64)); break; + case EcsOpcode.GtD: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].F64 > R[c].F64)); break; + case EcsOpcode.GeD: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].F64 >= R[c].F64)); break; + case EcsOpcode.EqL: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].I64 == R[c].I64)); break; + case EcsOpcode.LtL: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].I64 < R[c].I64)); break; + case EcsOpcode.LeL: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].I64 <= R[c].I64)); break; + case EcsOpcode.GtL: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].I64 > R[c].I64)); break; + case EcsOpcode.GeL: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].I64 >= R[c].I64)); break; + case EcsOpcode.EqS: StoreFresh(ref R[a], TaggedValue.FromBool(string.Equals(StrOrNull(R[b].Handle) ?? "", StrOrNull(R[c].Handle) ?? "", StringComparison.Ordinal))); break; + case EcsOpcode.EqP: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].I64 == R[c].I64)); break; + + case EcsOpcode.Not: StoreFresh(ref R[a], TaggedValue.FromBool(R[b].I32 == 0)); break; + case EcsOpcode.NegI: StoreFresh(ref R[a], TaggedValue.FromInt(-R[b].I32)); break; + case EcsOpcode.NegD: StoreFresh(ref R[a], TaggedValue.FromDouble(-R[b].F64)); break; + case EcsOpcode.Conv: StoreFresh(ref R[a], Conv((EcsConvKind)c, R[b])); break; + + // ---- 数组/字符串 ---- + case EcsOpcode.NewArrV: + { + var arr = new SimArray { ElemTag = (byte)ext }; + for (int i = 0; i < b; i++) + { + arr.Items.Add(R[c + i]); + Retain(R[c + i]); // 新容器持有元素引用 + } + StoreFresh(ref R[a], TaggedValue.FromArrayHandle(Store(arr))); + break; + } + case EcsOpcode.NewArrE: // ABx:元素类型码在 Bx + StoreFresh(ref R[a], TaggedValue.FromArrayHandle(Store(new SimArray { ElemTag = (byte)b }))); + break; + case EcsOpcode.GetI: + { + var container = R[b]; + var idx = R[c].I32; + if (container.IsString) + { + var s = StrOrNull(container.Handle) ?? ""; + if (idx < 0 || idx >= s.Length) throw new SimError(ERR_INDEX, $"字符串索引越界 {idx}/{s.Length}"); + StoreFresh(ref R[a], NewString(s[idx].ToString())); + } + else + { + var arr = Arr(container.Handle); + if (idx < 0 || idx >= arr.Items.Count) throw new SimError(ERR_INDEX, $"数组索引越界 {idx}/{arr.Items.Count}"); + var item = CoerceRead(arr.ElemTag, arr.Items[idx]); + Retain(item); // S-10 读共享:接收槽补引用 + StoreFresh(ref R[a], item); + } + break; + } + case EcsOpcode.SetI: + { + var arr = Arr(R[b].Handle); + var idx = R[c].I32; + if (idx < 0 || idx >= arr.Items.Count) throw new SimError(ERR_INDEX, $"数组索引越界 {idx}/{arr.Items.Count}"); + OverwriteItem(arr.Items, idx, CoerceWrite(arr.ElemTag, R[a])); + break; + } + case EcsOpcode.Slice: + { + var container = R[b]; + int start = R[c].I32; + int end; + if (container.IsString) + { + var s = StrOrNull(container.Handle) ?? ""; + end = ext == 0xFFFFFFFF ? s.Length : R[(int)ext].I32; + if (start > s.Length || end > s.Length || start > end) throw new SimError(ERR_INDEX, "切片越界"); + StoreFresh(ref R[a], NewString(s[start..end])); + } + else + { + var arr = Arr(container.Handle); + end = ext == 0xFFFFFFFF ? arr.Items.Count : R[(int)ext].I32; + if (start > arr.Items.Count || end > arr.Items.Count || start > end) throw new SimError(ERR_INDEX, "切片越界"); + var sliced = new SimArray { ElemTag = arr.ElemTag, Items = arr.Items.GetRange(start, end - start) }; + for (int i = 0; i < sliced.Items.Count; i++) + Retain(sliced.Items[i]); // 新容器持有元素引用 + StoreFresh(ref R[a], TaggedValue.FromArrayHandle(Store(sliced))); + } + break; + } + case EcsOpcode.Cont: + { + var item = R[b]; + var container = R[c]; + if (container.IsString) + { + var s = StrOrNull(container.Handle) ?? ""; + var sub = StrOrNull(item.Handle) ?? throw new SimError(ERR_TYPE, "in 运算类型不匹配"); + StoreFresh(ref R[a], TaggedValue.FromBool(s.Contains(sub, StringComparison.Ordinal))); + } + else if (container.IsArray) + { + var arr = Arr(container.Handle); + if (item.Tag != arr.ElemTag) + { + StoreFresh(ref R[a], TaggedValue.FromBool(false)); // S-09 元素类型不匹配 → false + } + else + { + bool found = false; + foreach (var it in arr.Items) + if (ValueEquals(it, item)) { found = true; break; } + StoreFresh(ref R[a], TaggedValue.FromBool(found)); + } + } + else + { + throw new SimError(ERR_TYPE, "in 容器类型非法"); + } + break; + } + case EcsOpcode.Append: + { + var src = Arr(R[b].Handle); + var dst = new SimArray { ElemTag = src.ElemTag, Items = new List(src.Items) { R[c] } }; + for (int i = 0; i < dst.Items.Count; i++) + Retain(dst.Items[i]); // 新容器持有元素引用 + StoreFresh(ref R[a], TaggedValue.FromArrayHandle(Store(dst))); + break; + } + case EcsOpcode.Cat: + { + var l = R[b]; + var r = R[c]; + if (l.IsString || r.IsString) + { + StoreFresh(ref R[a], NewString(ToStringTop(l) + ToStringTop(r))); + } + else if (l.IsArray && r.IsArray) + { + var la = Arr(l.Handle); + var ra = Arr(r.Handle); + if (la.ElemTag != ra.ElemTag) throw new SimError(ERR_TYPE, "拼接元素类型不一致"); + var merged = new SimArray { ElemTag = la.ElemTag, Items = new List(la.Items) }; + merged.Items.AddRange(ra.Items); + for (int i = 0; i < merged.Items.Count; i++) + Retain(merged.Items[i]); // 新容器持有元素引用 + StoreFresh(ref R[a], TaggedValue.FromArrayHandle(Store(merged))); + } + else + { + throw new SimError(ERR_TYPE, "CAT 仅支持字符串/数组"); + } + break; + } + case EcsOpcode.Len: + { + var v = R[b]; + if (v.IsString) StoreFresh(ref R[a], TaggedValue.FromInt((StrOrNull(v.Handle) ?? "").Length)); + else if (v.IsArray) StoreFresh(ref R[a], TaggedValue.FromInt(Arr(v.Handle).Items.Count)); + else throw new SimError(ERR_TYPE, "LEN 类型非法"); + break; + } + + // ---- 结构体 ---- + case EcsOpcode.NewSt: // ABx:类型表索引在 Bx + { + var layout = _image.Structs[b]; + var st = new SimStruct { Layout = layout, Slots = new TaggedValue[layout.SlotCount] }; + ZeroFill(layout, st.Slots); + StoreFresh(ref R[a], TaggedValue.FromStructHandle(Store(st))); + break; + } + case EcsOpcode.GetF: + StoreFresh(ref R[a], GetField(R[b], c)); // GetField 返回值恒携带一个接收槽引用 + break; + case EcsOpcode.PutF: PutField(R[b], c, R[a]); break; + case EcsOpcode.GetFI: + { + var st = St(R[b].Handle); + var f = st.Layout.Fields[c]; + if (f.Kind != EcsFieldKind.FixedArray) throw new SimError(ERR_TYPE, "GETFI 仅支持固定数组字段"); + var item = CoerceRead((byte)f.Type, st.Slots[f.SlotOffset + R[(int)ext].I32]); + Retain(item); // 读共享:接收槽补引用(标量 no-op) + StoreFresh(ref R[a], item); + break; + } + case EcsOpcode.PutFI: + { + var st = St(R[b].Handle); + var f = st.Layout.Fields[c]; + if (f.Kind != EcsFieldKind.FixedArray) throw new SimError(ERR_TYPE, "PUTFI 仅支持固定数组字段"); + WriteStructSlot(st, f.SlotOffset + R[(int)ext].I32, CoerceWrite((byte)f.Type, R[a])); + break; + } + + // ---- 域操作(宿主调用:返回取消状态,取消感知点)---- + case EcsOpcode.WaitI: _host.WaitMs(unchecked((int)ext)); return _token.IsCancellationRequested; + case EcsOpcode.WaitV: _host.WaitMs(R[a].I32); return _token.IsCancellationRequested; + case EcsOpcode.KeyI: _host.Key(a, unchecked((int)ext)); return _token.IsCancellationRequested; + case EcsOpcode.KeyV: _host.Key(a, R[b].I32); return _token.IsCancellationRequested; + case EcsOpcode.KeySt: _host.KeyState(a, b); return _token.IsCancellationRequested; + case EcsOpcode.StickSet: _host.StickSet(a, b, c); return _token.IsCancellationRequested; + case EcsOpcode.StickP: + { + _host.StickClick(a, b, c, unchecked((int)ext)); + return _token.IsCancellationRequested; + } + case EcsOpcode.StickPv: + { + _host.StickClick(a, (int)(ext & 0xFF), (int)((ext >> 16) & 0xFF), R[c].I32); + return _token.IsCancellationRequested; + } + case EcsOpcode.Img: // ABx:标签名 = 常量池[Bx](EcsOpcode.cs 注释为权威) + { + if (_host.StrictCaps) + throw new SimError(ERR_IL, $"图像标签能力缺失(strict_caps): {_image.Consts[b].Str}"); + var name = _image.Consts[b].Str ?? ""; + StoreFresh(ref R[a], TaggedValue.FromInt(_host.ImgLabel(name))); + return _token.IsCancellationRequested; + } + case EcsOpcode.Rand: + { + var max = R[b].I32; + if (max < 0) throw new SimError(ERR_INDEX, "RAND 参数为负"); + StoreFresh(ref R[a], TaggedValue.FromInt(max == 0 ? 0 : _host.Rand(max))); + return _token.IsCancellationRequested; + } + + case EcsOpcode.Halt: + throw new SimError(OK, "halt"); + default: + throw new SimError(ERR_OPCODE, $"未实现操作码 {op}"); + } + return false; + } + + void ZeroFill(EcsStructLayout layout, TaggedValue[] slots) + { + foreach (var f in layout.Fields) + { + switch (f.Kind) + { + case EcsFieldKind.FixedArray: + for (int i = 0; i < f.Count; i++) + slots[f.SlotOffset + i] = ZeroOf(f.ElementType); + break; + case EcsFieldKind.NestedStruct: + { + var nested = _image.Structs[f.NestedSid]; + var tmp = new TaggedValue[nested.SlotCount]; + ZeroFill(nested, tmp); + Array.Copy(tmp, 0, slots, f.SlotOffset, nested.SlotCount); + break; + } + default: + slots[f.SlotOffset] = ZeroOf(f.Type); + break; + } + } + } + + static TaggedValue ZeroOf(EcsTypeCode t) => t switch + { + EcsTypeCode.Double => TaggedValue.FromDouble(0), + EcsTypeCode.UInt64 => TaggedValue.FromUInt64(0), + EcsTypeCode.Ptr => TaggedValue.FromPtr(0), + EcsTypeCode.String => TaggedValue.FromStringHandle(0), + _ => TaggedValue.FromInt(0), + }; + + static TaggedValue CoerceRead(byte elemTag, TaggedValue v) + { + if (elemTag == EcsTag.Byte) return TaggedValue.FromByte((byte)v.I32); + if (elemTag == EcsTag.Bool) return TaggedValue.FromBool(v.I32 != 0); + return v; + } + + static TaggedValue CoerceWrite(byte elemTag, TaggedValue v) + { + if (elemTag == EcsTag.Bool) return TaggedValue.FromBool(v.I32 != 0); + return v; + } + + TaggedValue GetField(TaggedValue obj, int fieldIdx) + { + var st = St(obj.Handle); + var f = st.Layout.Fields[fieldIdx]; + switch (f.Kind) + { + case EcsFieldKind.Scalar: + { + var v = CoerceRead((byte)f.Type, st.Slots[f.SlotOffset]); + Retain(v); // 接收槽引用(标量 no-op;字符串字段共享 +1) + return v; + } + case EcsFieldKind.FixedArray: + { + var arr = new SimArray { ElemTag = (byte)f.ElementType, Items = new List(st.Slots[f.SlotOffset..(f.SlotOffset + f.Count)]) }; + for (int i = 0; i < arr.Items.Count; i++) + Retain(arr.Items[i]); // 新容器持有元素引用 + return TaggedValue.FromArrayHandle(Store(arr)); + } + case EcsFieldKind.NestedStruct: + { + var nested = _image.Structs[f.NestedSid]; + // 嵌套视图:切片拷贝 + 回写父槽(写穿透,v1 EcsStruct.GetNested 语义) + var view = new SimStruct + { + Layout = nested, + Slots = st.Slots[f.SlotOffset..(f.SlotOffset + nested.SlotCount)].ToArray(), + ViewParent = st, + ViewOffset = f.SlotOffset, + }; + for (int i = 0; i < view.Slots.Length; i++) + Retain(view.Slots[i]); // 视图按普通堆对象计数:槽区是拷贝快照,视图持有自己的引用 + return TaggedValue.FromStructHandle(Store(view)); + } + default: + throw new SimError(ERR_TYPE, "Boxed 字段访问不受支持"); + } + } + + void PutField(TaggedValue obj, int fieldIdx, TaggedValue value) + { + var st = St(obj.Handle); + var f = st.Layout.Fields[fieldIdx]; + switch (f.Kind) + { + case EcsFieldKind.Scalar: + WriteStructSlot(st, f.SlotOffset, CoerceWrite((byte)f.Type, value)); + break; + case EcsFieldKind.NestedStruct: + { + var src = St(value.Handle); + // src 布局须匹配字段的嵌套类型(而非父结构体总槽数;与 GetF 取嵌套布局一致) + var nested = _image.Structs[f.NestedSid]; + if (src.Slots.Length != nested.SlotCount) + throw new SimError(ERR_TYPE, "嵌套结构体布局不匹配"); + for (int i = 0; i < src.Slots.Length; i++) + MoveToSlot(ref st.Slots[f.SlotOffset + i], src.Slots[i]); + PropagateToParent(st); + break; + } + default: + throw new SimError(ERR_TYPE, "该字段种类不支持整体赋值"); + } + } + + /// 结构体槽写入;嵌套视图沿 ViewParent 链回写父槽区(写穿透,逐槽 release/retain 计数)。 + void WriteStructSlot(SimStruct st, int idx, TaggedValue v) + { + MoveToSlot(ref st.Slots[idx], v); + PropagateToParent(st); + } + + /// 视图槽区整体回写父槽区(逐槽 move_to;同柄槽 no-op 快路径,多数槽无变化)。 + /// 视图槽区整体回写父槽区(逐槽 move_to;同柄槽 no-op 快路径,多数槽无变化)。 + /// 目标偏移 = cur.ViewOffset(对齐改造前 Array.Copy 的写穿透位置)。 + void PropagateToParent(SimStruct view) + { + for (var cur = view; cur.ViewParent is { } parent; cur = parent) + { + var region = cur.Slots; + var parentSlots = parent.Slots; + for (int i = 0; i < region.Length; i++) + MoveToSlot(ref parentSlots[cur.ViewOffset + i], region[i]); + } + } + + int LayoutSlotCount(SimStruct st) => st.Layout.SlotCount; + + TaggedValue Conv(EcsConvKind kind, TaggedValue v) + { + switch (kind) + { + case EcsConvKind.IntToDouble: return TaggedValue.FromDouble(v.I32); + case EcsConvKind.DoubleToInt: return TaggedValue.FromInt(SaturateD2I(v.F64)); + case EcsConvKind.IntToUInt: return TaggedValue.FromUInt(unchecked((uint)v.I32)); + case EcsConvKind.UIntToInt: return TaggedValue.FromInt(v.I32); + case EcsConvKind.IntToByte: return TaggedValue.FromByte((byte)v.I32); + case EcsConvKind.BoolToInt: return TaggedValue.FromInt(v.I32); + case EcsConvKind.IntToUInt64: return TaggedValue.FromUInt64(unchecked((ulong)(long)v.I32)); + case EcsConvKind.UIntToUInt64: return TaggedValue.FromUInt64(unchecked((uint)v.I32)); + case EcsConvKind.UInt64ToInt: return TaggedValue.FromInt(unchecked((int)v.I64)); + case EcsConvKind.IntToPtr: return TaggedValue.FromPtr(v.I32); + case EcsConvKind.PtrToInt: return TaggedValue.FromInt(unchecked((int)v.I64)); + case EcsConvKind.UInt64ToPtr: return TaggedValue.FromPtr(v.I64); + case EcsConvKind.PtrToUInt64: return TaggedValue.FromUInt64(unchecked((ulong)v.I64)); + case EcsConvKind.DoubleToUInt64: return TaggedValue.FromUInt64(unchecked((ulong)SaturateD2L(v.F64))); + case EcsConvKind.UInt64ToDouble: return TaggedValue.FromDouble(unchecked((double)(ulong)v.I64)); + case EcsConvKind.ToStr: return NewString(ToStringTop(v)); + case EcsConvKind.ToInt: + if (v.Tag is EcsTag.Bool or EcsTag.Byte or EcsTag.Int or EcsTag.UInt) + return TaggedValue.FromInt(v.I32); + if (v.Tag == EcsTag.Double) + return TaggedValue.FromInt(SaturateD2I(v.F64)); + if (v.Tag == EcsTag.String) + return TaggedValue.FromInt(EcsConvText.ParseIntLiteral(StrOrNull(v.Handle))); // PC 端:数字字符串解析,失败 0 + throw new SimError(ERR_TYPE, "无法转换为 int"); + default: + throw new SimError(ERR_TYPE, $"未知转换 {kind}"); + } + } + + static int SaturateD2I(double d) + { + if (double.IsNaN(d)) return 0; + if (d >= int.MaxValue) return int.MaxValue; + if (d <= int.MinValue) return int.MinValue; + return (int)d; + } + + static long SaturateD2L(double d) + { + if (double.IsNaN(d)) return 0; + if (d >= long.MaxValue) return long.MaxValue; + if (d <= long.MinValue) return long.MinValue; + return (long)d; + } + + static void RequireDivisor(TaggedValue v) + { + if (v.I32 == 0 && v.Tag != EcsTag.Double) + throw new SimError(ERR_DIVZERO, "整数除零"); + } + + bool ValueEquals(TaggedValue x, TaggedValue y) + { + if (x.Tag != y.Tag) + return false; + if (x.IsString) + { + var sx = StrOrNull(x.Handle); + var sy = StrOrNull(y.Handle); + return string.Equals(sx, sy, StringComparison.Ordinal); + } + if (x.Tag == EcsTag.Double) + return x.F64 == y.F64; + return x.I64 == y.I64; + } + + TaggedValue HostNative(string name, TaggedValue[] args) + { + // S-21:miss(无处理器/未实现名)→ strict 恢复响亮;非 strict 查缺省值表(表外未知名仍响亮) + if (_host.Native is { } native && native(name, args, _nativeCtx) is { } ret) + return ret; + if (_host.StrictCaps) + throw new SimError(ERR_NOSUCHNATIVE, $"原生函数未实现: {name}"); + return EcsCapabilityDefaults.GetNativeDefault(name, _nativeCtx) + ?? throw new SimError(ERR_NOSUCHNATIVE, $"原生函数未注册: {name}"); + } + + /// 堆上下文实现:宿主原生回调读写解释器堆的唯一通道。 + sealed class NativeContext(InstructionExecutor vm) : EcxNativeContext + { + public override string Str(TaggedValue v) => v.IsString ? vm.StrOrNull(v.Handle) ?? "" : ""; + + public override TaggedValue Str(string s) => vm.NewString(s); + + public override Value ToValue(TaggedValue v) + { + if (v.IsString) + return Value.FromString(vm.StrOrNull(v.Handle) ?? ""); + if (v.IsArray) + { + var arr = vm.Arr(v.Handle); + var items = new List(arr.Items.Count); + foreach (var item in arr.Items) + items.Add(ToValue(item)); + var elemType = ElemScriptType(arr.ElemTag); + return Value.FromArray(ScriptArray.Create(elemType, items), elemType); + } + return v.ToValue(); + } + + public override TaggedValue FromValue(Value v) + { + var t = v.Type; + if (t.Equals(ScriptType.String)) + return vm.NewString(v.AsString()); + return TaggedValue.FromValue(v); // 标量;数组/结构体原生不返回 + } + + static ScriptType ElemScriptType(byte tag) => tag switch + { + EcsTag.Bool => ScriptType.Bool, + EcsTag.Byte => ScriptType.Byte, + EcsTag.UInt => ScriptType.UInt, + EcsTag.Double => ScriptType.Double, + EcsTag.UInt64 => ScriptType.UInt64, + EcsTag.String => ScriptType.String, + EcsTag.Ptr => ScriptType.Ptr, + _ => ScriptType.Int, + }; + } + + /// 入口返回值转公共 Value:字符串解引用、数组转 ScriptArray、结构体重建为 + /// EcsStruct(堆在返回前仍存活)。堆外无句柄语义,转换必须在此完成。 + Value ToPublicValue(TaggedValue v) => ToPublicValue(v, new Dictionary()); + + Value ToPublicValue(TaggedValue v, Dictionary structs) + { + if (v.IsString) + return Value.FromString(StrOrNull(v.Handle) ?? ""); + if (v.IsArray) + { + var arr = Arr(v.Handle); + var items = new List(arr.Items.Count); + foreach (var item in arr.Items) + items.Add(ToPublicValue(item, structs)); + var elemType = ElemScriptTypeOf(arr.ElemTag); + return Value.FromArray(ScriptArray.Create(elemType, items, new SimpleStringStore()), elemType); + } + if (v.IsStruct) + return Value.FromStruct(PublicStruct(St(v.Handle), structs)); + return v.ToValue(); + } + + static ScriptType ElemScriptTypeOf(byte tag) => tag switch + { + EcsTag.Bool => ScriptType.Bool, + EcsTag.Byte => ScriptType.Byte, + EcsTag.UInt => ScriptType.UInt, + EcsTag.Double => ScriptType.Double, + EcsTag.String => ScriptType.String, + EcsTag.Ptr => ScriptType.Ptr, + _ => ScriptType.Int, + }; + + /// SimStruct → EcsStruct(按镜像类型表重建定义;句柄槽逐字段展开)。 + EcsStruct PublicStruct(SimStruct st, Dictionary structs) + { + if (structs.TryGetValue(st, out var cached)) + return cached; + var def = PublicStructDef(st.Layout, []); + var es = new EcsStruct(def); + structs[st] = es; + for (int i = 0; i < st.Layout.Fields.Length; i++) + { + var f = st.Layout.Fields[i]; + var defField = def.Fields[i]; + switch (f.Kind) + { + case EcsFieldKind.FixedArray: + for (int e = 0; e < f.Count; e++) + es.SetFieldElement(defField, e, PublicRaw(st.Slots[f.SlotOffset + e], TypeLayout.GetElementType(defField.FieldType))); + break; + case EcsFieldKind.NestedStruct: + { + var nestedLayout = _image.Structs[f.NestedSid]; + var nested = new SimStruct + { + Layout = nestedLayout, + Slots = st.Slots[f.SlotOffset..(f.SlotOffset + nestedLayout.SlotCount)].ToArray(), + }; + es.SetField(defField, PublicStruct(nested, structs)); + break; + } + default: + es.SetField(defField, PublicRaw(st.Slots[f.SlotOffset], defField.FieldType)); + break; + } + } + return es; + } + + EcsStructDef PublicStructDef(EcsStructLayout layout, Dictionary defs) + { + if (defs.TryGetValue(layout, out var cached)) + return cached; + var def = new EcsStructDef { Name = layout.Name }; + defs[layout] = def; + foreach (var f in layout.Fields) + { + var fieldType = f.Kind switch + { + EcsFieldKind.FixedArray => new ArrayType(ElemScriptTypeOf((byte)f.ElementType), f.Count), + EcsFieldKind.NestedStruct => new StructType(PublicStructDef(_image.Structs[f.NestedSid], defs)), + _ => ElemScriptTypeOf((byte)f.Type), + }; + def.Fields.Add(new EcsFieldDef { Name = f.Name, FieldType = fieldType }); + } + StructLayout.Calculate(def); + return def; + } + + object PublicRaw(TaggedValue v, ScriptType type) => type switch + { + _ when type.Equals(ScriptType.Bool) => v.I32 != 0, + _ when type.Equals(ScriptType.Byte) => (byte)v.I32, + _ when type.Equals(ScriptType.UInt) => unchecked((uint)v.I32), + _ when type.Equals(ScriptType.UInt64) => unchecked((ulong)v.I64), + _ when type.Equals(ScriptType.Ptr) => new IntPtr(v.I64), + _ when type.Equals(ScriptType.Double) => v.F64, + _ when type.Equals(ScriptType.String) => StrOrNullPub(v), + _ => v.I32, + }; + + string? StrOrNullPub(TaggedValue v) => v.IsString ? StrOrNull(v.Handle) : null; + + + sealed class SimError : Exception + { + public readonly int Code; + public SimError(int code, string message) : base(message) { Code = code; } + } +} \ No newline at end of file diff --git a/src/EasyCon.Script/Bytecode/PcInterpreter.cs b/src/EasyCon.Script/Bytecode/PcInterpreter.cs new file mode 100644 index 00000000..dec9218b --- /dev/null +++ b/src/EasyCon.Script/Bytecode/PcInterpreter.cs @@ -0,0 +1,27 @@ +using EasyCon.Script.Symbols; + +namespace EasyCon.Script.Bytecode; + +/// +/// PC 扩展指令执行入口:仅消费 Desktop 内存计划,保留 int 操作数和动态帧。 +/// 不依赖 InstructionCodec,不生成或加载扩展 ECX;MCU 格式与 C VM 无须修改。 +/// +public static class PcInterpreter +{ + public static int Run(EcxImage program, EcxHost host, out Value result, CancellationToken token = default) + => Run(program, host, out _, out _, out _, out result, token); + + public static int Run(EcxImage program, EcxHost host, out TaggedValue rawResult, out int errorFunc, + out int errorPc, out Value result, CancellationToken token = default) + { + if (program.Target != ScriptTarget.Desktop) + { + rawResult = default; + result = Value.Void; + errorFunc = -1; + errorPc = -1; + return EcxInterpreter.ERR_IMAGE; + } + return InstructionExecutor.Run(program, host, out rawResult, out errorFunc, out errorPc, out result, token); + } +} \ No newline at end of file diff --git a/src/EasyCon.Script/Bytecode/SlotAllocator.cs b/src/EasyCon.Script/Bytecode/SlotAllocator.cs index 9197d5d3..136dcdfd 100644 --- a/src/EasyCon.Script/Bytecode/SlotAllocator.cs +++ b/src/EasyCon.Script/Bytecode/SlotAllocator.cs @@ -380,8 +380,8 @@ void AssignSlots() _stagingCount = Math.Max(_stagingCount, count); } _allocBase = _stagingBase + _stagingCount; - if (_allocBase > EcsRegisters.MaxRegisters) - throw Fail($"寄存器溢出:函数 '{_symbol.Name}' 的参数窗和出站窗口需求 {_allocBase} 超过帧上限 {EcsRegisters.MaxRegisters}"); + if (_allocBase > RegisterLimit) + throw Fail($"寄存器溢出:函数 '{_symbol.Name}' 的参数窗和出站窗口需求 {_allocBase} 超过帧上限 {RegisterLimit}"); // 0. 存活局部重编号(编码期槽位回收):局部读值全走 SSA(构建期 mem2reg), // StoreLocal 仅参数 store 发射(EmitInst;TRE 回边写参数帧槽,entry 循环头重读)。 @@ -742,8 +742,8 @@ int PoolAlloc() return s; } var fresh = _poolNext++; - if (fresh >= EcsRegisters.MaxRegisters) - throw Fail($"寄存器溢出:函数 '{_symbol.Name}' 的寄存器需求超过帧上限 {EcsRegisters.MaxRegisters}(拆分函数或简化深表达式)"); + if (fresh >= RegisterLimit) + throw Fail($"寄存器溢出:函数 '{_symbol.Name}' 的寄存器需求超过帧上限 {RegisterLimit}(拆分函数或简化深表达式)"); return fresh; } diff --git a/src/EasyCon.Script/Compilation.cs b/src/EasyCon.Script/Compilation.cs index ad5424f0..6c8738f7 100644 --- a/src/EasyCon.Script/Compilation.cs +++ b/src/EasyCon.Script/Compilation.cs @@ -39,7 +39,7 @@ public sealed class CompileResult public bool KeyAction { get; } public bool NeedIL { get; } public CompilationTiming? Timing { get; } - /// 链接后的可执行镜像(统一链路产物:桌面 EcxInterpreter 与 MCU .ecx 共用;失败为 null)。 + /// 链接后的内存计划;Desktop 仅供 PC 执行,Portable 可写 ECX;失败为 null。 public EcxImage? Image { get; } /// 链接序模块产物(std → vision → 依赖拓扑序 → main)。 public List Artifacts { get; } @@ -77,9 +77,18 @@ public string FormatCode() } } +/// PC 内存执行与可分发 ECX 是不同目标;默认保持原来的 ECX 编码约束。 +public enum ScriptTarget +{ + Portable, + Desktop, +} + /// 统一编译链路入口选项(Compilation.CompileSource/CompileFile)。 public sealed class CompileOptions { + /// Desktop 允许扩展寄存器操作数,仅能在 PC 执行,不能序列化为 ECX。 + public ScriptTarget Target { get; set; } /// 外部变量名集合(识图标签等)。 public ImmutableHashSet? ExtVars { get; set; } /// 启用 obj/ 磁盘缓存(默认 true:compile/MCU 分发路径收益;现编宿主可置 false, @@ -112,7 +121,8 @@ public string ProductFingerprint() // (无旧版缓存、仅开发测试;旧履历 M4–M7 删除)。M2:参数/出站窗口按函数需求分配。 // M5:声音接口改为 AUDIO_LOAD/AUDIO_SCORE/AUDIO_UNLOAD。M6:AUDIO_SCORE 返回 INT。 // M7:合入 SSA 全局提升修复,使旧音频分支的编译缓存失效。 - return $"O={(Optimize ? '1' : '0')}|E={Modules.ModuleCacheKeys.ExtVarsKey(ExtVars)}|B={Bytecode.EcsSyscall.AbiRevision}|M=7"; + // M8:PC 扩展执行目标与可分发 ECX 分离,失败缓存也必须按目标隔离。 + return $"O={(Optimize ? '1' : '0')}|T={Target}|E={Modules.ModuleCacheKeys.ExtVarsKey(ExtVars)}|B={Bytecode.EcsSyscall.AbiRevision}|M=8"; } } diff --git a/src/EasyCon.Script/Modules/ModuleCompilePipeline.cs b/src/EasyCon.Script/Modules/ModuleCompilePipeline.cs index 18562cab..24d998e2 100644 --- a/src/EasyCon.Script/Modules/ModuleCompilePipeline.cs +++ b/src/EasyCon.Script/Modules/ModuleCompilePipeline.cs @@ -215,7 +215,7 @@ bool CompileOne(ModuleNode node, ConcurrentDictionary failedModule return false; } _cache?.Store(artifact, cacheKey); - if (_cache == null && _options.UseProcessCache) + if (_cache == null && _options.UseProcessCache && !artifact.RequiresPcExecution) ProcessModuleCache.Store(artifact, cacheKey); } node.Artifact = artifact; @@ -347,7 +347,7 @@ bool EnsureParsed(ModuleNode node, DiagnosticBag sink) bool hasInit = isMain || (node.Interface?.HasInit ?? false); var artifact = EcxModuleEncoder.CompileWholeProgramAsModule(ssa, node.Name, externalFunctions, - hasInit: hasInit, isMain: isMain); + hasInit: hasInit, isMain: isMain, target: _options.Target); artifact.Ssa = _options.KeepSsa ? ssa : null; // 接口区:声明收集层提取;依赖表 = 图边(含隐式 std/vision,§4.4) diff --git a/src/EasyCon.Script/Modules/ProjectCompiler.cs b/src/EasyCon.Script/Modules/ProjectCompiler.cs index 8a2d4512..9e8d3a71 100644 --- a/src/EasyCon.Script/Modules/ProjectCompiler.cs +++ b/src/EasyCon.Script/Modules/ProjectCompiler.cs @@ -100,7 +100,7 @@ static ModuleProjectResult CompileCore(SyntaxTree mainTree, string? scriptDir, s CompileOptions options, ModuleProjectResult result) { ModuleCache? cache = null; - if (options.UseDiskCache && objDir.Length > 0) + if (options.Target == ScriptTarget.Portable && options.UseDiskCache && objDir.Length > 0) cache = new ModuleCache(objDir); // 主树语法错误:编译前短路(不建图、不触碰缓存统计) @@ -163,7 +163,7 @@ static ModuleProjectResult CompileCore(SyntaxTree mainTree, string? scriptDir, s { result.Image = EcxPipeline.Link(result.Artifacts, result.Artifacts.Any(a => a.KeyAction), - result.Artifacts.Any(a => a.NeedIL)); + result.Artifacts.Any(a => a.NeedIL), options.Target); result.Success = true; } catch (BytecodeException ex) diff --git a/test/EasyCon.Tests/Bytecode/ArgumentWindowTests.cs b/test/EasyCon.Tests/Bytecode/ArgumentWindowTests.cs index 4e72e83f..0ff1499f 100644 --- a/test/EasyCon.Tests/Bytecode/ArgumentWindowTests.cs +++ b/test/EasyCon.Tests/Bytecode/ArgumentWindowTests.cs @@ -21,7 +21,10 @@ internal static string WeightedFunction(int count) internal static CompileResult Compile(string source, bool optimize = true) { - CompileResult result = Compilation.CompileSource(source, ScriptCompileProfiles.Interactive(optimize: optimize)); + CompileOptions options = ScriptCompileProfiles.Distributable(); + options.Optimize = optimize; + options.UseDiskCache = false; + CompileResult result = Compilation.CompileSource(source, options); Assert.That(result.Diagnostics.Where(d => d.IsError), Is.Empty, string.Join("\n", result.Diagnostics)); Assert.That(result.Image, Is.Not.Null); return result; @@ -194,7 +197,9 @@ public void CombinedParameterAndOutgoingWindows_OverFrameBudget_ReportsOverflow( string forwarded = string.Join(", ", Enumerable.Range(0, 128).Select(i => $"$p{i}")); string source = $"FUNC relay({Parameters(128)}):INT\nRETURN relay({forwarded})\nENDFUNC\n" + $"RETURN relay({Arguments(128)})\n"; - CompileResult result = Compilation.CompileSource(source, ScriptCompileProfiles.Interactive(optimize: false)); + CompileOptions options = ScriptCompileProfiles.Distributable(); + options.Optimize = false; + CompileResult result = Compilation.CompileSource(source, options); 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); diff --git a/test/EasyCon.Tests/Bytecode/CorpusCrossValidationTests.cs b/test/EasyCon.Tests/Bytecode/CorpusCrossValidationTests.cs index fb2738ba..2c5da0cd 100644 --- a/test/EasyCon.Tests/Bytecode/CorpusCrossValidationTests.cs +++ b/test/EasyCon.Tests/Bytecode/CorpusCrossValidationTests.cs @@ -1,3 +1,4 @@ +using EasyCon.Core.Hosting; using EasyCon.Script; using EasyCon.Script.Bytecode; using EasyCon.Tests.Support; @@ -51,13 +52,15 @@ public void Corpus_Case(string ecsPath) var eventsPath = Path.Combine(CorpusAssert.CorpusDir(), tag + ".events"); var expectedEvents = File.Exists(eventsPath) ? File.ReadAllLines(eventsPath) : []; - // ① 统一编译链路 → EcxInterpreter + // ① 分别编译 PC 执行计划与 Portable 镜像,比较真正的两个目标。 var result = Compilation.CompileSource(File.ReadAllText(ecsPath), new CompileOptions { UseDiskCache = false }); Assert.That(result.Diagnostics.Where(d => d.IsError).ToList(), Is.Empty, $"[{tag}] 编译失败:" + string.Join("\n", result.Diagnostics)); + CompileResult pc = Compilation.CompileSource(File.ReadAllText(ecsPath), ScriptCompileProfiles.Interactive()); + Assert.That(pc.Diagnostics.Where(d => d.IsError), Is.Empty, $"[{tag}] PC 编译失败:" + string.Join("\n", pc.Diagnostics)); var host = EcsTestHost.CreateRecording(); - var code = EcxInterpreter.Run(result.Image!, host); + var code = PcInterpreter.Run(pc.Image!, host, out _); Assert.That(code, Is.EqualTo(EcxInterpreter.OK), $"[{tag}] 解释器执行失败:{code}"); Assert.That(host.Lines, Is.EqualTo(expectedLines), $"[{tag}] 输出行与期望不一致"); Assert.That(EcsTestHost.AllEvents(host), Is.EqualTo(expectedEvents), $"[{tag}] 事件与期望不一致"); diff --git a/test/EasyCon.Tests/Bytecode/ModuleProjectTests.cs b/test/EasyCon.Tests/Bytecode/ModuleProjectTests.cs index 11c90b35..b8a04c77 100644 --- a/test/EasyCon.Tests/Bytecode/ModuleProjectTests.cs +++ b/test/EasyCon.Tests/Bytecode/ModuleProjectTests.cs @@ -375,7 +375,7 @@ public void ExplicitImportCycle_IsRejected(string firstPath, string secondPath, Assert.Multiple(() => { Assert.That(cycle.IsError, Is.True); - Assert.That(cycle.FileName, Is.EqualTo(Path.Combine(_dir, secondPath))); + Assert.That(cycle.FileName, Is.EqualTo(Path.GetFullPath(Path.Combine(_dir, secondPath)))); Assert.That(cycle.Location.StartLine, Is.EqualTo(1)); Assert.That(cycle.Location.Span.Length, Is.GreaterThan(0)); Assert.That(project.Artifacts, Is.Empty, "Cycle detection must precede module compilation"); diff --git a/test/EasyCon.Tests/Bytecode/PcExtensionRuntimeTests.cs b/test/EasyCon.Tests/Bytecode/PcExtensionRuntimeTests.cs new file mode 100644 index 00000000..fb503718 --- /dev/null +++ b/test/EasyCon.Tests/Bytecode/PcExtensionRuntimeTests.cs @@ -0,0 +1,157 @@ +using EasyCon.Core.Hosting; +using EasyCon.Core.Runner; +using EasyCon.Core.Script; +using EasyCon.Script; +using EasyCon.Script.Bytecode; +using EasyCon.Script.Modules; +using EasyCon.Script.Symbols; +using System.Text; + +namespace EasyCon.Tests.Bytecode; + +[TestFixture] +public sealed class PcExtensionRuntimeTests +{ + static string WideFunction(int count) + { + StringBuilder source = new("FUNC wide($offset:INT):INT\n"); + for (int i = 0; i < count; i++) + source.AppendLine($"$v{i} = RAND(1000) + $offset + {i}"); + source.AppendLine("$sum = 0"); + for (int i = 0; i < count; i++) + source.AppendLine($"$sum += $v{i}"); + source.AppendLine("RETURN $sum\nENDFUNC\nRETURN wide(7) + wide(11)"); + return source.ToString(); + } + + static CompileResult CompilePc(string source, bool optimize = true) + { + CompileResult result = Compilation.CompileSource(source, ScriptCompileProfiles.Interactive(optimize: optimize)); + Assert.That(result.Diagnostics.Where(d => d.IsError), Is.Empty, string.Join("\n", result.Diagnostics)); + Assert.That(result.Image!.Target, Is.EqualTo(ScriptTarget.Desktop)); + return result; + } + + [TestCase(256, true)] + [TestCase(270, true)] + [TestCase(1024, true)] + [TestCase(270, false)] + public void WideFunction_CompilesAndExecutesTwice_WithoutTruncatingSlots(int count, bool optimize) + { + CompileResult compiled = CompilePc(WideFunction(count), optimize); + Assert.That(compiled.Image!.Functions.Single(f => f.Name == "wide").NSlots, Is.GreaterThan(255)); + EcxHost host = new() { Rand = _ => 3 }; + int status = PcInterpreter.Run(compiled.Image, host, out Value result); + Assert.That(status, Is.EqualTo(EcxInterpreter.OK)); + Assert.That(result.AsInt(), Is.EqualTo(count * (count - 1) + count * 24)); + } + + [TestCase(false)] + [TestCase(true)] + public void PcPlan_CannotBeWrittenAsEcx_EvenWhenSmall(bool stripDebug) + { + CompileResult small = CompilePc("RETURN 42\n"); + CompileResult wide = CompilePc(WideFunction(270)); + Assert.Throws(() => EcsContainer.WriteImage(small.Image!, stripDebug)); + Assert.Throws(() => EcsContainer.WriteImage(wide.Image!, stripDebug)); + foreach (ModuleArtifact artifact in wide.Artifacts.Where(a => a.RequiresPcExecution)) + { + Assert.Throws(() => EcsContainer.WriteModule(artifact)); + } + Assert.Throws(() => EcxPipeline.Link(wide.Artifacts, false, false)); + } + + [Test] + public void McuExport_StillRejectsWideFrames_AndSmallContainerIsUnchanged() + { + EcxExportResult wide = EcxExportService.Export(WideFunction(270), null, []); + Assert.That(wide.Success, Is.False); + Assert.That(wide.Bytes, Is.Null); + Assert.That(wide.Diagnostics.Any(d => d.Message.Contains("寄存器溢出")), Is.True); + EcxExportResult small = EcxExportService.Export("RETURN 42\n", null, []); + Assert.That(small.Success, Is.True); + EcxImage roundTrip = EcsContainer.ReadImage(small.Bytes!); + Assert.That(roundTrip.Target, Is.EqualTo(ScriptTarget.Portable)); + Assert.That(EcxInterpreter.Run(roundTrip, new EcxHost(), out Value result), Is.EqualTo(EcxInterpreter.OK)); + Assert.That(result.AsInt(), Is.EqualTo(42)); + } + + [Test] + public void PcPlan_DoesNotCreateDiskCache_EvenIfCallerRequestsIt() + { + string directory = Path.Combine(Path.GetTempPath(), $"EasyConPcCache_{Guid.NewGuid():N}"); + CompileOptions options = ScriptCompileProfiles.Interactive(); + options.UseDiskCache = true; + options.ObjDir = directory; + for (int i = 0; i < 2; i++) + { + CompileResult result = Compilation.CompileSource(WideFunction(270), options); + Assert.That(result.Diagnostics.Where(d => d.IsError), Is.Empty); + Assert.That(result.Image!.MaxSlots, Is.GreaterThan(255)); + } + Assert.That(Directory.Exists(directory), Is.False); + } + + [Test] + public void PcExecution_PreservesCancellationAndErrorSourceLocation() + { + CompileResult loop = CompilePc("FOR $i = 1 TO 2000000000\n$x = RAND(2)\nNEXT\n"); + using CancellationTokenSource cancellation = new(TimeSpan.FromMilliseconds(50)); + Assert.That(PcInterpreter.Run(loop.Image!, new EcxHost(), out _, cancellation.Token), Is.EqualTo(EcxInterpreter.CANCELLED)); + CompileResult fault = CompilePc("$zero = RAND(1)\nRETURN 8 / $zero\n"); + int code = PcInterpreter.Run(fault.Image!, new EcxHost(), out _, out int errorFunc, out int errorPc, out _); + Assert.That(code, Is.EqualTo(EcxInterpreter.ERR_DIVZERO)); + Assert.That(fault.Image!.Functions[errorFunc].LineAt(errorPc), Is.EqualTo(2)); + } + + [Test] + public void PcEntry_RejectsPortableImages() + { + CompileResult result = Compilation.CompileSource("RETURN 1\n"); + Assert.That(PcInterpreter.Run(result.Image!, new EcxHost(), out _), Is.EqualTo(EcxInterpreter.ERR_IMAGE)); + } + + [Test] + public void CoreRuntime_ExecutesWidePlanWithTheAssembledHost() + { + CompileResult compiled = CompilePc(WideFunction(270).Replace("RAND(1000)", "RAND(1)")); + using CapabilityLease lease = ScriptHostAssembler.Assemble(new ScriptHostContext + { + EnableOcr = false, + EnableInference = false, + }); + Value result = EcxVm.Run(compiled.Image!, lease.Capabilities, CancellationToken.None, nativeSymbols: compiled.NativeSymbols); + Assert.That(result.AsInt(), Is.EqualTo(270 * 269 + 270 * 18)); + } + + [Test] + public void WideLibrary_LinksAndInitializesOnce_DespiteCachedMcuFailure() + { + string directory = Path.Combine(Path.GetTempPath(), $"EasyConPcModules_{Guid.NewGuid():N}"); + Directory.CreateDirectory(Path.Combine(directory, "lib")); + try + { + string library = WideFunction(270).Replace("RETURN wide(7) + wide(11)", ""); + library += "\n$count = 0\n$count += 1\nFUNC initCount():INT\nRETURN $count\nENDFUNC\n"; + File.WriteAllText(Path.Combine(directory, "lib", "wide.ecs"), library); + string path = Path.Combine(directory, "main.ecs"); + File.WriteAllText(path, "RETURN wide(7) + wide(11) + initCount()\n"); + CompileResult portable = Compilation.CompileFile(path, ScriptCompileProfiles.Distributable()); + Assert.That(portable.Diagnostics.Any(d => d.Message.Contains("寄存器溢出")), Is.True, string.Join("\n", portable.Diagnostics)); + string[] diskFiles = Directory.GetFiles(Path.Combine(directory, "obj")); + Assert.That(diskFiles.Any(p => p.EndsWith(".err", StringComparison.Ordinal)), Is.True); + for (int i = 0; i < 2; i++) + { + CompileResult pc = Compilation.CompileFile(path, ScriptCompileProfiles.Interactive()); + Assert.That(pc.Diagnostics.Where(d => d.IsError), Is.Empty, string.Join("\n", pc.Diagnostics)); + Assert.That(PcInterpreter.Run(pc.Image!, new EcxHost { Rand = _ => 3 }, out Value result), Is.EqualTo(EcxInterpreter.OK)); + Assert.That(result.AsInt(), Is.EqualTo(270 * 269 + 270 * 24 + 1)); + } + Assert.That(Directory.GetFiles(Path.Combine(directory, "obj")), Is.EquivalentTo(diskFiles)); + } + finally + { + Directory.Delete(directory, true); + } + } +} \ No newline at end of file diff --git a/test/EasyCon.Tests/Capabilities/AudioCapabilityTests.cs b/test/EasyCon.Tests/Capabilities/AudioCapabilityTests.cs index 9a26c681..604bf914 100644 --- a/test/EasyCon.Tests/Capabilities/AudioCapabilityTests.cs +++ b/test/EasyCon.Tests/Capabilities/AudioCapabilityTests.cs @@ -209,7 +209,7 @@ public void McuRejectsAudioAtLoadEvenWithoutStrictCapabilities(string call) string? binary = CvmRunner.EnsureBuilt(_directory); if (binary == null) Assert.Ignore("No C compiler available."); - CompileResult script = Compile("A\n" + call); + CompileResult script = Compilation.CompileSource("A\n" + call, ScriptCompileProfiles.Distributable()); (int exit, string output, string error) = CvmRunner.Run(binary!, EcsContainer.WriteImage(script.Image!), "audio", _directory); Assert.That(exit, Is.EqualTo(14), error); Assert.That(output, Is.Empty); diff --git a/test/EasyCon.Tests/Hosting/ScriptHostAssemblerTests.cs b/test/EasyCon.Tests/Hosting/ScriptHostAssemblerTests.cs index 60509976..cb7a9730 100644 --- a/test/EasyCon.Tests/Hosting/ScriptHostAssemblerTests.cs +++ b/test/EasyCon.Tests/Hosting/ScriptHostAssemblerTests.cs @@ -314,8 +314,7 @@ public void FreeTensor(int handle) { } } /// -/// 编译档位锁定(档位塌缩后,SingleStreamFormat §2 R-5):产物同形态, -/// 剩余两档只是缓存策略(与产物内容无关,不进缓存键)。 +/// 编译档位锁定:PC 执行与可分发 ECX 的目标及缓存键必须隔离。 /// [TestFixture] public class ScriptCompileProfilesTests @@ -328,6 +327,7 @@ public void Interactive_SkipsDiskCache() Assert.Multiple(() => { Assert.That(options.UseDiskCache, Is.False, "现编档不落盘(run/编辑器高频重编)"); + Assert.That(options.Target, Is.EqualTo(ScriptTarget.Desktop)); Assert.That(options.UseProcessCache, Is.True, "进程缓存兜底"); Assert.That(options.Optimize, Is.False); Assert.That(options.ExtVars, Is.EquivalentTo(new[] { "lbl" })); @@ -342,17 +342,18 @@ public void Distributable_EnablesDiskCache() Assert.Multiple(() => { Assert.That(options.UseDiskCache, Is.True, "obj/ 内容寻址缓存服务分发产物"); + Assert.That(options.Target, Is.EqualTo(ScriptTarget.Portable)); Assert.That(options.Optimize, Is.True); Assert.That(options.ExtVars, Is.Null); }); } [Test] - public void Profiles_ShareProductFingerprint() + public void Profiles_IsolateProductFingerprint() { - // 档位塌缩:两档产物同形态(差异仅在缓存策略,而缓存策略不进键)→ 共享缓存条目 + // 同源码的 MCU 溢出诊断不能命中 PC 编译缓存。 Assert.That( ScriptCompileProfiles.Interactive([]).ProductFingerprint(), - Is.EqualTo(ScriptCompileProfiles.Distributable([]).ProductFingerprint())); + Is.Not.EqualTo(ScriptCompileProfiles.Distributable([]).ProductFingerprint())); } } \ No newline at end of file