Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions docs/ModuleSystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 预声明接口缓存也包含相同路径上下文,避免复用其他项目目录解析出的原生库绝对路径。
文件名 = `<模块名>-<key 前 8 位>.ecm`,不同版本并存免锁竞争。
Expand All @@ -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 原生库路径
Expand All @@ -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`。
`LibConstant_Parity`、`Cache_StdVisionSharedAcrossProjects`。
25 changes: 17 additions & 8 deletions docs/Pipeline.md
Original file line number Diff line number Diff line change
@@ -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/`)。

Expand All @@ -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 直接解释器。

入口函数 `<main>` = 依赖模块 `<init:module>` 调用序列按拓扑序前插 `$eval` 本体,
顶层 RETURN 经 r0 透传;`<init>` 承载各模块顶层语句与模块私有全局初始化。

Expand Down Expand Up @@ -61,17 +70,17 @@
| 内置函数能力清单(路由 + 特征位 + 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) |

## 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)/ 取消检查三分点
Expand Down
20 changes: 14 additions & 6 deletions docs/VM2.md
Original file line number Diff line number Diff line change
@@ -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`。冲突时以代码为准并修本文。
Expand Down Expand Up @@ -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 回调) |
Expand Down Expand Up @@ -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)。
Expand All @@ -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]` = 帧槽):
Expand Down Expand Up @@ -497,4 +505,4 @@ void ecs_vm_ret_str(ecs_vm *vm, ecs_value *ret, const uint16_t *units, int32_
→ ECX <main>:Call <init:std> → ... Call PRINT → PRINT 体内 CallN 0x80000001
执行 桌面:EcxHost.Syscall(1) → 行断协议 → Print 委托 → 控制台
MCU :h_syscall(1) → --print 通道开则输出;恒返回 len
```
```
13 changes: 6 additions & 7 deletions src/EasyCon.Core/Hosting/ScriptCompileProfiles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,12 @@ namespace EasyCon.Core.Hosting;
/// 避免 GUI / CLI 各自手写 <see cref="CompileOptions"/> 字面量而悄悄漂移。
///
/// <para>
/// 单流化后(docs/VM2.md §3.1):所有产物都是同一形态(ECX1 平铺 + v3 定长指令编码),
/// **没有宽窄之分**——原 Desktop/Portable 的格式维度随 <c>EnablePcWideSlots</c>
/// 一起删除。当前定长指令编码的函数帧预算为 255 槽;参数与出站窗口按需分配,
/// 桌面与 MCU 共用该预算及同一条可序列化指令流。
/// 「能不能装进设备」是资源问题,由烧录前的容量预检判定(docs/McuBytecodeDelivery.md,计划中),
/// 不再由编译档位表达。剩余的两档只是**缓存策略**(与产物内容无关,不进缓存键):
/// PC 与 MCU 共用语言语义和完整操作数指令表示,但不共用编码容量限制。
/// Interactive 生成 PC 内存执行计划;Distributable 生成可序列化的 ECX。
/// 两种目标进入不同缓存键,PC 扩展模块不写入 ECM,避免混入 MCU 分发链。
/// </para>
/// <list type="bullet">
/// <item><see cref="Interactive"/>:现编(不落盘,进程级缓存兜底)——run/编辑器等高频重编路径。</item>
/// <item><see cref="Interactive"/>:PC 现编,不落盘;可编码的小模块仍可使用进程缓存。</item>
/// <item><see cref="Distributable"/>:obj/ 内容寻址缓存——compile/分发产物路径,重复编译零成本。</item>
/// </list>
/// </summary>
Expand All @@ -30,6 +27,7 @@ public static class ScriptCompileProfiles
/// <param name="optimize">false 时跳过 SSA 优化(IR 观测用)。</param>
public static CompileOptions Interactive(IEnumerable<string>? extVars = null, bool optimize = true) => new()
{
Target = ScriptTarget.Desktop,
ExtVars = ToSet(extVars),
UseDiskCache = false,
UseProcessCache = true,
Expand All @@ -42,6 +40,7 @@ public static class ScriptCompileProfiles
/// <param name="extVars">外部变量名集合(识图标签名等),null = 无。</param>
public static CompileOptions Distributable(IEnumerable<string>? extVars = null) => new()
{
Target = ScriptTarget.Portable,
ExtVars = ToSet(extVars),
UseDiskCache = true,
UseProcessCache = true,
Expand Down
7 changes: 6 additions & 1 deletion src/EasyCon.Core/Runner/EcxVm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
21 changes: 13 additions & 8 deletions src/EasyCon.Script/Bytecode/BytecodeEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public int Add(EcsConst c)
/// </summary>
public sealed class ModuleEncodeContext
{
public ScriptTarget Target;
public required SsaProgram Program;
public required string ModuleName;
/// <summary>本模块函数 → 模块局部 fid(值相等键,A-07)。</summary>
Expand Down Expand Up @@ -297,14 +298,16 @@ void EmitExt(EcsOpcode op, int a, int b, int c, uint ext)
Emit(new EcsInstruction(op, a, b, c, ext));
}

/// <summary>Iabc 头部操作数字节域守卫(v3 定长):槽位 u8 ≤254(255 = 无接收哨兵);
/// StickSet/StickP 的 B/C 是摇杆坐标(满 u8 0..255),不按槽位收紧。</summary>
static void CheckIabcOperands(EcsOpcode op, int a, int b, int c)
/// <summary>Portable 槽位受 v3 编码约束;Desktop 保留整数槽位编号。
/// StickSet/StickP 的 B/C 恒为摇杆坐标(u8 0..255),不因 PC 扩展而放宽。</summary>
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)
Expand Down Expand Up @@ -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
{
Expand Down
Loading
Loading